diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 0000000..d8a755d --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"56e89c33-b844-4e6a-8df3-2210b2fb4a4d","pid":47993,"acquiredAt":1775696579277} \ No newline at end of file diff --git a/.claude/skills/add-gchat-v2/SKILL.md b/.claude/skills/add-gchat-v2/SKILL.md index cf1a573..aa4a740 100644 --- a/.claude/skills/add-gchat-v2/SKILL.md +++ b/.claude/skills/add-gchat-v2/SKILL.md @@ -9,7 +9,7 @@ This skill adds Google Chat support to NanoClaw v2 using the Chat SDK bridge. ## Phase 1: Pre-flight -Check if `src/channels/gchat-v2.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. +Check if `src/channels/gchat.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. ## Phase 2: Apply Code Changes @@ -24,7 +24,7 @@ npm install @chat-adapter/gchat Uncomment the Google Chat import in `src/channels/index.ts`: ```typescript -import './gchat-v2.js'; +import './gchat.js'; ``` ### Build @@ -72,7 +72,7 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS ## Removal -1. Comment out `import './gchat-v2.js'` in `src/channels/index.ts` +1. Comment out `import './gchat.js'` in `src/channels/index.ts` 2. Remove `GCHAT_CREDENTIALS` from `.env` 3. `npm uninstall @chat-adapter/gchat` 4. Rebuild and restart diff --git a/.claude/skills/add-github-v2/SKILL.md b/.claude/skills/add-github-v2/SKILL.md index 44e7a41..f2e7276 100644 --- a/.claude/skills/add-github-v2/SKILL.md +++ b/.claude/skills/add-github-v2/SKILL.md @@ -9,7 +9,7 @@ This skill adds GitHub support to NanoClaw v2 using the Chat SDK bridge. The age ## Phase 1: Pre-flight -Check if `src/channels/github-v2.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. +Check if `src/channels/github.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. ## Phase 2: Apply Code Changes @@ -24,7 +24,7 @@ npm install @chat-adapter/github Uncomment the GitHub import in `src/channels/index.ts`: ```typescript -import './github-v2.js'; +import './github.js'; ``` ### Build @@ -74,7 +74,7 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS ## Removal -1. Comment out `import './github-v2.js'` in `src/channels/index.ts` +1. Comment out `import './github.js'` in `src/channels/index.ts` 2. Remove `GITHUB_TOKEN` and `GITHUB_WEBHOOK_SECRET` from `.env` 3. `npm uninstall @chat-adapter/github` 4. Rebuild and restart diff --git a/.claude/skills/add-imessage-v2/SKILL.md b/.claude/skills/add-imessage-v2/SKILL.md index 33121ee..6ac1a6f 100644 --- a/.claude/skills/add-imessage-v2/SKILL.md +++ b/.claude/skills/add-imessage-v2/SKILL.md @@ -9,7 +9,7 @@ This skill adds iMessage support to NanoClaw v2 using the Chat SDK bridge. Suppo ## Phase 1: Pre-flight -Check if `src/channels/imessage-v2.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. +Check if `src/channels/imessage.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. ## Phase 2: Apply Code Changes @@ -24,7 +24,7 @@ npm install chat-adapter-imessage Uncomment the iMessage import in `src/channels/index.ts`: ```typescript -import './imessage-v2.js'; +import './imessage.js'; ``` ### Build @@ -80,7 +80,7 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS ## Removal -1. Comment out `import './imessage-v2.js'` in `src/channels/index.ts` +1. Comment out `import './imessage.js'` in `src/channels/index.ts` 2. Remove iMessage env vars from `.env` 3. `npm uninstall chat-adapter-imessage` 4. Rebuild and restart diff --git a/.claude/skills/add-linear-v2/SKILL.md b/.claude/skills/add-linear-v2/SKILL.md index 9ba6f8a..d4b1933 100644 --- a/.claude/skills/add-linear-v2/SKILL.md +++ b/.claude/skills/add-linear-v2/SKILL.md @@ -9,7 +9,7 @@ This skill adds Linear support to NanoClaw v2 using the Chat SDK bridge. The age ## Phase 1: Pre-flight -Check if `src/channels/linear-v2.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. +Check if `src/channels/linear.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. ## Phase 2: Apply Code Changes @@ -24,7 +24,7 @@ npm install @chat-adapter/linear Uncomment the Linear import in `src/channels/index.ts`: ```typescript -import './linear-v2.js'; +import './linear.js'; ``` ### Build @@ -71,7 +71,7 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS ## Removal -1. Comment out `import './linear-v2.js'` in `src/channels/index.ts` +1. Comment out `import './linear.js'` in `src/channels/index.ts` 2. Remove `LINEAR_API_KEY` and `LINEAR_WEBHOOK_SECRET` from `.env` 3. `npm uninstall @chat-adapter/linear` 4. Rebuild and restart diff --git a/.claude/skills/add-matrix-v2/SKILL.md b/.claude/skills/add-matrix-v2/SKILL.md index 1e4848f..8684cf1 100644 --- a/.claude/skills/add-matrix-v2/SKILL.md +++ b/.claude/skills/add-matrix-v2/SKILL.md @@ -9,7 +9,7 @@ This skill adds Matrix support to NanoClaw v2 using the Chat SDK bridge. Works w ## Phase 1: Pre-flight -Check if `src/channels/matrix-v2.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. +Check if `src/channels/matrix.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. ## Phase 2: Apply Code Changes @@ -24,7 +24,7 @@ npm install @beeper/chat-adapter-matrix Uncomment the Matrix import in `src/channels/index.ts`: ```typescript -import './matrix-v2.js'; +import './matrix.js'; ``` ### Build @@ -71,7 +71,7 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS ## Removal -1. Comment out `import './matrix-v2.js'` in `src/channels/index.ts` +1. Comment out `import './matrix.js'` in `src/channels/index.ts` 2. Remove `MATRIX_BASE_URL`, `MATRIX_ACCESS_TOKEN`, `MATRIX_USER_ID`, `MATRIX_BOT_USERNAME` from `.env` 3. `npm uninstall @beeper/chat-adapter-matrix` 4. Rebuild and restart diff --git a/.claude/skills/add-resend-v2/SKILL.md b/.claude/skills/add-resend-v2/SKILL.md index f858037..ae25e3f 100644 --- a/.claude/skills/add-resend-v2/SKILL.md +++ b/.claude/skills/add-resend-v2/SKILL.md @@ -9,7 +9,7 @@ This skill adds email support via Resend to NanoClaw v2 using the Chat SDK bridg ## Phase 1: Pre-flight -Check if `src/channels/resend-v2.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. +Check if `src/channels/resend.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. ## Phase 2: Apply Code Changes @@ -24,7 +24,7 @@ npm install @resend/chat-sdk-adapter Uncomment the Resend import in `src/channels/index.ts`: ```typescript -import './resend-v2.js'; +import './resend.js'; ``` ### Build @@ -73,7 +73,7 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS ## Removal -1. Comment out `import './resend-v2.js'` in `src/channels/index.ts` +1. Comment out `import './resend.js'` in `src/channels/index.ts` 2. Remove `RESEND_API_KEY`, `RESEND_FROM_ADDRESS`, `RESEND_FROM_NAME`, `RESEND_WEBHOOK_SECRET` from `.env` 3. `npm uninstall @resend/chat-sdk-adapter` 4. Rebuild and restart diff --git a/.claude/skills/add-slack-v2/SKILL.md b/.claude/skills/add-slack-v2/SKILL.md index c5b5a17..2d03afe 100644 --- a/.claude/skills/add-slack-v2/SKILL.md +++ b/.claude/skills/add-slack-v2/SKILL.md @@ -9,7 +9,7 @@ This skill adds Slack support to NanoClaw v2 using the Chat SDK bridge. ## Phase 1: Pre-flight -Check if `src/channels/slack-v2.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. +Check if `src/channels/slack.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. ## Phase 2: Apply Code Changes @@ -24,7 +24,7 @@ npm install @chat-adapter/slack Uncomment the Slack import in `src/channels/index.ts`: ```typescript -import './slack-v2.js'; +import './slack.js'; ``` ### Build @@ -75,7 +75,7 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS ## Removal -1. Comment out `import './slack-v2.js'` in `src/channels/index.ts` +1. Comment out `import './slack.js'` in `src/channels/index.ts` 2. Remove `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET` from `.env` 3. `npm uninstall @chat-adapter/slack` 4. Rebuild and restart diff --git a/.claude/skills/add-teams-v2/SKILL.md b/.claude/skills/add-teams-v2/SKILL.md index 78f9650..2976883 100644 --- a/.claude/skills/add-teams-v2/SKILL.md +++ b/.claude/skills/add-teams-v2/SKILL.md @@ -9,7 +9,7 @@ This skill adds Microsoft Teams support to NanoClaw v2 using the Chat SDK bridge ## Phase 1: Pre-flight -Check if `src/channels/teams-v2.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. +Check if `src/channels/teams.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. ## Phase 2: Apply Code Changes @@ -24,7 +24,7 @@ npm install @chat-adapter/teams Uncomment the Teams import in `src/channels/index.ts`: ```typescript -import './teams-v2.js'; +import './teams.js'; ``` ### Build @@ -69,7 +69,7 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS ## Removal -1. Comment out `import './teams-v2.js'` in `src/channels/index.ts` +1. Comment out `import './teams.js'` in `src/channels/index.ts` 2. Remove `TEAMS_APP_ID` and `TEAMS_APP_PASSWORD` from `.env` 3. `npm uninstall @chat-adapter/teams` 4. Rebuild and restart diff --git a/.claude/skills/add-telegram-v2/SKILL.md b/.claude/skills/add-telegram-v2/SKILL.md index 7bcc079..754a948 100644 --- a/.claude/skills/add-telegram-v2/SKILL.md +++ b/.claude/skills/add-telegram-v2/SKILL.md @@ -9,7 +9,7 @@ This skill adds Telegram support to NanoClaw v2 using the Chat SDK bridge. ## Phase 1: Pre-flight -Check if `src/channels/telegram-v2.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. +Check if `src/channels/telegram.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. ## Phase 2: Apply Code Changes @@ -24,7 +24,7 @@ npm install @chat-adapter/telegram Uncomment the Telegram import in `src/channels/index.ts`: ```typescript -import './telegram-v2.js'; +import './telegram.js'; ``` ### Build @@ -76,7 +76,7 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS ## Removal -1. Comment out `import './telegram-v2.js'` in `src/channels/index.ts` +1. Comment out `import './telegram.js'` in `src/channels/index.ts` 2. Remove `TELEGRAM_BOT_TOKEN` from `.env` 3. `npm uninstall @chat-adapter/telegram` 4. Rebuild and restart diff --git a/.claude/skills/add-webex-v2/SKILL.md b/.claude/skills/add-webex-v2/SKILL.md index 65f0dcf..a11da4c 100644 --- a/.claude/skills/add-webex-v2/SKILL.md +++ b/.claude/skills/add-webex-v2/SKILL.md @@ -9,7 +9,7 @@ This skill adds Cisco Webex support to NanoClaw v2 using the Chat SDK bridge. ## Phase 1: Pre-flight -Check if `src/channels/webex-v2.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. +Check if `src/channels/webex.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. ## Phase 2: Apply Code Changes @@ -24,7 +24,7 @@ npm install @bitbasti/chat-adapter-webex Uncomment the Webex import in `src/channels/index.ts`: ```typescript -import './webex-v2.js'; +import './webex.js'; ``` ### Build @@ -69,7 +69,7 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS ## Removal -1. Comment out `import './webex-v2.js'` in `src/channels/index.ts` +1. Comment out `import './webex.js'` in `src/channels/index.ts` 2. Remove `WEBEX_BOT_TOKEN` and `WEBEX_WEBHOOK_SECRET` from `.env` 3. `npm uninstall @bitbasti/chat-adapter-webex` 4. Rebuild and restart diff --git a/.claude/skills/add-whatsapp-cloud-v2/SKILL.md b/.claude/skills/add-whatsapp-cloud-v2/SKILL.md index 32a08ae..0ebc0c0 100644 --- a/.claude/skills/add-whatsapp-cloud-v2/SKILL.md +++ b/.claude/skills/add-whatsapp-cloud-v2/SKILL.md @@ -9,7 +9,7 @@ This skill adds WhatsApp support via the official Meta WhatsApp Business Cloud A ## Phase 1: Pre-flight -Check if `src/channels/whatsapp-cloud-v2.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. +Check if `src/channels/whatsapp-cloud.ts` exists and the import is uncommented in `src/channels/index.ts`. If both are in place, skip to Phase 3. ## Phase 2: Apply Code Changes @@ -24,7 +24,7 @@ npm install @chat-adapter/whatsapp Uncomment the WhatsApp Cloud API import in `src/channels/index.ts`: ```typescript -import './whatsapp-cloud-v2.js'; +import './whatsapp-cloud.js'; ``` ### Build @@ -76,7 +76,7 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS ## Removal -1. Comment out `import './whatsapp-cloud-v2.js'` in `src/channels/index.ts` +1. Comment out `import './whatsapp-cloud.js'` in `src/channels/index.ts` 2. Remove `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_PHONE_NUMBER_ID`, `WHATSAPP_APP_SECRET`, `WHATSAPP_VERIFY_TOKEN` from `.env` 3. `npm uninstall @chat-adapter/whatsapp` 4. Rebuild and restart diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index 200938d..77f8341 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -242,26 +242,43 @@ Verify the proxy starts: `npm run dev` should show "Credential proxy listening" ## 5. Set Up Channels AskUserQuestion (multiSelect): Which messaging channels do you want to enable? -- WhatsApp (authenticates via QR code or pairing code) -- Telegram (authenticates via bot token from @BotFather) -- Slack (authenticates via Slack app with Socket Mode) -- Discord (authenticates via Discord bot token) +- Discord (bot token + public key) +- Slack (bot token + signing secret) +- Telegram (bot token from @BotFather) +- GitHub (PR/issue comment threads) +- Linear (issue comment threads) +- Microsoft Teams (Azure Bot) +- Google Chat (service account) +- WhatsApp Cloud API (Meta Business API) +- WhatsApp Baileys (QR code / pairing code) +- Resend (email) +- Matrix (any homeserver) +- Webex (bot token) +- iMessage (macOS local or Photon API) -**Delegate to each selected channel's own skill.** Each channel skill handles its own code installation, authentication, registration, and JID resolution. This avoids duplicating channel-specific logic and ensures JIDs are always correct. +**Delegate to each selected channel's own skill.** Each channel skill handles its own package installation, authentication, registration, and configuration. This avoids duplicating channel-specific logic. For each selected channel, invoke its skill: -- **WhatsApp:** Invoke `/add-whatsapp` -- **Telegram:** Invoke `/add-telegram` -- **Slack:** Invoke `/add-slack` - **Discord:** Invoke `/add-discord` +- **Slack:** Invoke `/add-slack-v2` +- **Telegram:** Invoke `/add-telegram-v2` +- **GitHub:** Invoke `/add-github-v2` +- **Linear:** Invoke `/add-linear-v2` +- **Microsoft Teams:** Invoke `/add-teams-v2` +- **Google Chat:** Invoke `/add-gchat-v2` +- **WhatsApp Cloud API:** Invoke `/add-whatsapp-cloud-v2` +- **WhatsApp Baileys:** Invoke `/add-whatsapp` +- **Resend:** Invoke `/add-resend-v2` +- **Matrix:** Invoke `/add-matrix-v2` +- **Webex:** Invoke `/add-webex-v2` +- **iMessage:** Invoke `/add-imessage-v2` Each skill will: -1. Install the channel code (via `git merge` of the skill branch) -2. Collect credentials/tokens and write to `.env` -3. Authenticate (WhatsApp QR/pairing, or verify token-based connection) -4. Register the chat with the correct JID format -5. Build and verify +1. Install the Chat SDK adapter package +2. Uncomment the channel import in `src/channels/index.ts` +3. Collect credentials/tokens and write to `.env` +4. Build and verify **After all channel skills complete**, install dependencies and rebuild — channel merges may introduce new packages: diff --git a/container/agent-runner/src/index-v2.ts b/container/agent-runner/src/index-v2.ts deleted file mode 100644 index db6523a..0000000 --- a/container/agent-runner/src/index-v2.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * 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 { - 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 = { - ...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); -}); diff --git a/container/agent-runner/src/index.ts b/container/agent-runner/src/index.ts index 7e739c7..db6523a 100644 --- a/container/agent-runner/src/index.ts +++ b/container/agent-runner/src/index.ts @@ -1,736 +1,96 @@ /** - * NanoClaw Agent Runner - * Runs inside a container, receives config via stdin, outputs result to stdout + * NanoClaw Agent Runner v2 * - * Input protocol: - * Stdin: Full ContainerInput JSON (read until EOF, like before) - * IPC: Follow-up messages written as JSON files to /workspace/ipc/input/ - * Files: {type:"message", text:"..."}.json — polled and consumed - * Sentinel: /workspace/ipc/input/_close — signals session end + * Runs inside a container. All IO goes through the session DB. + * No stdin, no stdout markers, no IPC files. * - * Stdout protocol: - * Each result is wrapped in OUTPUT_START_MARKER / OUTPUT_END_MARKER pairs. - * Multiple results may be emitted (one per agent teams result). - * Final marker after loop ends signals completion. + * 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 { execFile } from 'child_process'; -import { - query, - HookCallback, - PreCompactHookInput, -} from '@anthropic-ai/claude-agent-sdk'; import { fileURLToPath } from 'url'; -interface ContainerInput { - prompt: string; - sessionId?: string; - groupFolder: string; - chatJid: string; - isMain: boolean; - isScheduledTask?: boolean; - assistantName?: string; - script?: string; +import { createProvider, type ProviderName } from './providers/factory.js'; +import { runPollLoop } from './poll-loop.js'; + +function log(msg: string): void { + console.error(`[agent-runner] ${msg}`); } -interface ContainerOutput { - status: 'success' | 'error'; - result: string | null; - newSessionId?: string; - error?: string; -} +const CWD = '/workspace/agent'; +const GLOBAL_CLAUDE_MD = '/workspace/global/CLAUDE.md'; -interface SessionEntry { - sessionId: string; - fullPath: string; - summary: string; - firstPrompt: string; -} +async function main(): Promise { + const providerName = (process.env.AGENT_PROVIDER || 'claude') as ProviderName; + const assistantName = process.env.NANOCLAW_ASSISTANT_NAME; -interface SessionsIndex { - entries: SessionEntry[]; -} + log(`Starting v2 agent-runner (provider: ${providerName})`); -interface SDKUserMessage { - type: 'user'; - message: { role: 'user'; content: string }; - parent_tool_use_id: null; - session_id: string; -} + const provider = createProvider(providerName, { assistantName }); -const IPC_INPUT_DIR = '/workspace/ipc/input'; -const IPC_INPUT_CLOSE_SENTINEL = path.join(IPC_INPUT_DIR, '_close'); -const IPC_POLL_MS = 500; - -/** - * Push-based async iterable for streaming user messages to the SDK. - * Keeps the iterable alive until end() is called, preventing isSingleUserTurn. - */ -class MessageStream { - private queue: SDKUserMessage[] = []; - private waiting: (() => void) | null = null; - private done = false; - - push(text: string): void { - this.queue.push({ - type: 'user', - message: { role: 'user', content: text }, - parent_tool_use_id: null, - session_id: '', - }); - this.waiting?.(); - } - - end(): void { - this.done = true; - this.waiting?.(); - } - - async *[Symbol.asyncIterator](): AsyncGenerator { - while (true) { - while (this.queue.length > 0) { - yield this.queue.shift()!; - } - if (this.done) return; - await new Promise((r) => { - this.waiting = r; - }); - this.waiting = null; - } - } -} - -async function readStdin(): Promise { - return new Promise((resolve, reject) => { - let data = ''; - process.stdin.setEncoding('utf8'); - process.stdin.on('data', (chunk) => { - data += chunk; - }); - process.stdin.on('end', () => resolve(data)); - process.stdin.on('error', reject); - }); -} - -const OUTPUT_START_MARKER = '---NANOCLAW_OUTPUT_START---'; -const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---'; - -function writeOutput(output: ContainerOutput): void { - console.log(OUTPUT_START_MARKER); - console.log(JSON.stringify(output)); - console.log(OUTPUT_END_MARKER); -} - -function log(message: string): void { - console.error(`[agent-runner] ${message}`); -} - -function getSessionSummary( - sessionId: string, - transcriptPath: string, -): string | null { - const projectDir = path.dirname(transcriptPath); - const indexPath = path.join(projectDir, 'sessions-index.json'); - - if (!fs.existsSync(indexPath)) { - log(`Sessions index not found at ${indexPath}`); - return null; - } - - try { - const index: SessionsIndex = JSON.parse( - fs.readFileSync(indexPath, 'utf-8'), - ); - const entry = index.entries.find((e) => e.sessionId === sessionId); - if (entry?.summary) { - return entry.summary; - } - } catch (err) { - log( - `Failed to read sessions index: ${err instanceof Error ? err.message : String(err)}`, - ); - } - - return null; -} - -/** - * Archive the full transcript to conversations/ before compaction. - */ -function createPreCompactHook(assistantName?: string): HookCallback { - return async (input, _toolUseId, _context) => { - const preCompact = input as PreCompactHookInput; - const transcriptPath = preCompact.transcript_path; - const sessionId = preCompact.session_id; - - if (!transcriptPath || !fs.existsSync(transcriptPath)) { - log('No transcript found for archiving'); - return {}; - } - - try { - const content = fs.readFileSync(transcriptPath, 'utf-8'); - const messages = parseTranscript(content); - - if (messages.length === 0) { - log('No messages to archive'); - return {}; - } - - const summary = getSessionSummary(sessionId, transcriptPath); - const name = summary ? sanitizeFilename(summary) : generateFallbackName(); - - const conversationsDir = '/workspace/group/conversations'; - fs.mkdirSync(conversationsDir, { recursive: true }); - - const date = new Date().toISOString().split('T')[0]; - const filename = `${date}-${name}.md`; - const filePath = path.join(conversationsDir, filename); - - const markdown = formatTranscriptMarkdown( - messages, - summary, - assistantName, - ); - fs.writeFileSync(filePath, markdown); - - log(`Archived conversation to ${filePath}`); - } catch (err) { - log( - `Failed to archive transcript: ${err instanceof Error ? err.message : String(err)}`, - ); - } - - return {}; - }; -} - -function sanitizeFilename(summary: string): string { - return summary - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') - .slice(0, 50); -} - -function generateFallbackName(): string { - const time = new Date(); - return `conversation-${time.getHours().toString().padStart(2, '0')}${time.getMinutes().toString().padStart(2, '0')}`; -} - -interface ParsedMessage { - role: 'user' | 'assistant'; - content: string; -} - -function parseTranscript(content: string): ParsedMessage[] { - const messages: ParsedMessage[] = []; - - for (const line of content.split('\n')) { - if (!line.trim()) continue; - try { - const entry = JSON.parse(line); - if (entry.type === 'user' && entry.message?.content) { - const text = - typeof entry.message.content === 'string' - ? entry.message.content - : entry.message.content - .map((c: { text?: string }) => c.text || '') - .join(''); - if (text) messages.push({ role: 'user', content: text }); - } else if (entry.type === 'assistant' && entry.message?.content) { - const textParts = entry.message.content - .filter((c: { type: string }) => c.type === 'text') - .map((c: { text: string }) => c.text); - const text = textParts.join(''); - if (text) messages.push({ role: 'assistant', content: text }); - } - } catch {} - } - - return messages; -} - -function formatTranscriptMarkdown( - messages: ParsedMessage[], - title?: string | null, - assistantName?: string, -): string { - const now = new Date(); - const formatDateTime = (d: Date) => - d.toLocaleString('en-US', { - month: 'short', - day: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: true, - }); - - const lines: string[] = []; - lines.push(`# ${title || 'Conversation'}`); - lines.push(''); - lines.push(`Archived: ${formatDateTime(now)}`); - lines.push(''); - lines.push('---'); - lines.push(''); - - for (const msg of messages) { - const sender = msg.role === 'user' ? 'User' : assistantName || 'Assistant'; - const content = - msg.content.length > 2000 - ? msg.content.slice(0, 2000) + '...' - : msg.content; - lines.push(`**${sender}**: ${content}`); - lines.push(''); - } - - return lines.join('\n'); -} - -/** - * Check for _close sentinel. - */ -function shouldClose(): boolean { - if (fs.existsSync(IPC_INPUT_CLOSE_SENTINEL)) { - try { - fs.unlinkSync(IPC_INPUT_CLOSE_SENTINEL); - } catch { - /* ignore */ - } - return true; - } - return false; -} - -/** - * Drain all pending IPC input messages. - * Returns messages found, or empty array. - */ -function drainIpcInput(): string[] { - try { - fs.mkdirSync(IPC_INPUT_DIR, { recursive: true }); - const files = fs - .readdirSync(IPC_INPUT_DIR) - .filter((f) => f.endsWith('.json')) - .sort(); - - const messages: string[] = []; - for (const file of files) { - const filePath = path.join(IPC_INPUT_DIR, file); - try { - const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); - fs.unlinkSync(filePath); - if (data.type === 'message' && data.text) { - messages.push(data.text); - } - } catch (err) { - log( - `Failed to process input file ${file}: ${err instanceof Error ? err.message : String(err)}`, - ); - try { - fs.unlinkSync(filePath); - } catch { - /* ignore */ - } - } - } - return messages; - } catch (err) { - log(`IPC drain error: ${err instanceof Error ? err.message : String(err)}`); - return []; - } -} - -/** - * Wait for a new IPC message or _close sentinel. - * Returns the messages as a single string, or null if _close. - */ -function waitForIpcMessage(): Promise { - return new Promise((resolve) => { - const poll = () => { - if (shouldClose()) { - resolve(null); - return; - } - const messages = drainIpcInput(); - if (messages.length > 0) { - resolve(messages.join('\n')); - return; - } - setTimeout(poll, IPC_POLL_MS); - }; - poll(); - }); -} - -/** - * Run a single query and stream results via writeOutput. - * Uses MessageStream (AsyncIterable) to keep isSingleUserTurn=false, - * allowing agent teams subagents to run to completion. - * Also pipes IPC messages into the stream during the query. - */ -async function runQuery( - prompt: string, - sessionId: string | undefined, - mcpServerPath: string, - containerInput: ContainerInput, - sdkEnv: Record, - resumeAt?: string, -): Promise<{ - newSessionId?: string; - lastAssistantUuid?: string; - closedDuringQuery: boolean; -}> { - const stream = new MessageStream(); - stream.push(prompt); - - // Poll IPC for follow-up messages and _close sentinel during the query - let ipcPolling = true; - let closedDuringQuery = false; - const pollIpcDuringQuery = () => { - if (!ipcPolling) return; - if (shouldClose()) { - log('Close sentinel detected during query, ending stream'); - closedDuringQuery = true; - stream.end(); - ipcPolling = false; - return; - } - const messages = drainIpcInput(); - for (const text of messages) { - log(`Piping IPC message into active query (${text.length} chars)`); - stream.push(text); - } - setTimeout(pollIpcDuringQuery, IPC_POLL_MS); - }; - setTimeout(pollIpcDuringQuery, IPC_POLL_MS); - - let newSessionId: string | undefined; - let lastAssistantUuid: string | undefined; - let messageCount = 0; - let resultCount = 0; - - // Load global CLAUDE.md as additional system context (shared across all groups) - const globalClaudeMdPath = '/workspace/global/CLAUDE.md'; - let globalClaudeMd: string | undefined; - if (!containerInput.isMain && fs.existsSync(globalClaudeMdPath)) { - globalClaudeMd = fs.readFileSync(globalClaudeMdPath, 'utf-8'); + // 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/* - // These are passed to the SDK so their CLAUDE.md files are loaded automatically - const extraDirs: string[] = []; + 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()) { - extraDirs.push(fullPath); + additionalDirectories.push(fullPath); } } - } - if (extraDirs.length > 0) { - log(`Additional directories: ${extraDirs.join(', ')}`); - } - - for await (const message of query({ - prompt: stream, - options: { - cwd: '/workspace/group', - additionalDirectories: extraDirs.length > 0 ? extraDirs : undefined, - resume: sessionId, - resumeSessionAt: resumeAt, - systemPrompt: globalClaudeMd - ? { - type: 'preset' as const, - preset: 'claude_code' as const, - append: globalClaudeMd, - } - : undefined, - allowedTools: [ - 'Bash', - 'Read', - 'Write', - 'Edit', - 'Glob', - 'Grep', - 'WebSearch', - 'WebFetch', - 'Task', - 'TaskOutput', - 'TaskStop', - 'TeamCreate', - 'TeamDelete', - 'SendMessage', - 'TodoWrite', - 'ToolSearch', - 'Skill', - 'NotebookEdit', - 'mcp__nanoclaw__*', - ], - env: sdkEnv, - permissionMode: 'bypassPermissions', - allowDangerouslySkipPermissions: true, - settingSources: ['project', 'user'], - mcpServers: { - nanoclaw: { - command: 'node', - args: [mcpServerPath], - env: { - NANOCLAW_CHAT_JID: containerInput.chatJid, - NANOCLAW_GROUP_FOLDER: containerInput.groupFolder, - NANOCLAW_IS_MAIN: containerInput.isMain ? '1' : '0', - }, - }, - }, - hooks: { - PreCompact: [ - { hooks: [createPreCompactHook(containerInput.assistantName)] }, - ], - }, - }, - })) { - messageCount++; - const msgType = - message.type === 'system' - ? `system/${(message as { subtype?: string }).subtype}` - : message.type; - log(`[msg #${messageCount}] type=${msgType}`); - - if (message.type === 'assistant' && 'uuid' in message) { - lastAssistantUuid = (message as { uuid: string }).uuid; - } - - if (message.type === 'system' && message.subtype === 'init') { - newSessionId = message.session_id; - log(`Session initialized: ${newSessionId}`); - } - - if ( - message.type === 'system' && - (message as { subtype?: string }).subtype === 'task_notification' - ) { - const tn = message as { - task_id: string; - status: string; - summary: string; - }; - log( - `Task notification: task=${tn.task_id} status=${tn.status} summary=${tn.summary}`, - ); - } - - if (message.type === 'result') { - resultCount++; - const textResult = - 'result' in message ? (message as { result?: string }).result : null; - log( - `Result #${resultCount}: subtype=${message.subtype}${textResult ? ` text=${textResult.slice(0, 200)}` : ''}`, - ); - writeOutput({ - status: 'success', - result: textResult || null, - newSessionId, - }); + if (additionalDirectories.length > 0) { + log(`Additional directories: ${additionalDirectories.join(', ')}`); } } - ipcPolling = false; - log( - `Query done. Messages: ${messageCount}, results: ${resultCount}, lastAssistantUuid: ${lastAssistantUuid || 'none'}, closedDuringQuery: ${closedDuringQuery}`, - ); - return { newSessionId, lastAssistantUuid, closedDuringQuery }; -} + // MCP server path + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const mcpServerPath = path.join(__dirname, 'mcp-tools', 'index.js'); -interface ScriptResult { - wakeAgent: boolean; - data?: unknown; -} - -const SCRIPT_TIMEOUT_MS = 30_000; - -async function runScript(script: string): Promise { - const scriptPath = '/tmp/task-script.sh'; - fs.writeFileSync(scriptPath, script, { mode: 0o755 }); - - return new Promise((resolve) => { - execFile( - 'bash', - [scriptPath], - { - timeout: SCRIPT_TIMEOUT_MS, - maxBuffer: 1024 * 1024, - env: process.env, - }, - (error, stdout, stderr) => { - if (stderr) { - log(`Script stderr: ${stderr.slice(0, 500)}`); - } - - if (error) { - log(`Script error: ${error.message}`); - return resolve(null); - } - - // Parse last non-empty line of stdout as JSON - const lines = stdout.trim().split('\n'); - const lastLine = lines[lines.length - 1]; - if (!lastLine) { - log('Script produced no output'); - return resolve(null); - } - - try { - const result = JSON.parse(lastLine); - if (typeof result.wakeAgent !== 'boolean') { - log( - `Script output missing wakeAgent boolean: ${lastLine.slice(0, 200)}`, - ); - return resolve(null); - } - resolve(result as ScriptResult); - } catch { - log(`Script output is not valid JSON: ${lastLine.slice(0, 200)}`); - resolve(null); - } - }, - ); - }); -} - -async function main(): Promise { - let containerInput: ContainerInput; - - try { - const stdinData = await readStdin(); - containerInput = JSON.parse(stdinData); - try { - fs.unlinkSync('/tmp/input.json'); - } catch { - /* may not exist */ - } - log(`Received input for group: ${containerInput.groupFolder}`); - } catch (err) { - writeOutput({ - status: 'error', - result: null, - error: `Failed to parse input: ${err instanceof Error ? err.message : String(err)}`, - }); - process.exit(1); - } - - // Credentials are injected by the host's credential proxy via ANTHROPIC_BASE_URL. - // No real secrets exist in the container environment. - const sdkEnv: Record = { + // SDK env + const env: Record = { ...process.env, CLAUDE_CODE_AUTO_COMPACT_WINDOW: '165000', }; - const __dirname = path.dirname(fileURLToPath(import.meta.url)); - const mcpServerPath = path.join(__dirname, 'ipc-mcp-stdio.js'); - - let sessionId = containerInput.sessionId; - fs.mkdirSync(IPC_INPUT_DIR, { recursive: true }); - - // Clean up stale _close sentinel from previous container runs - try { - fs.unlinkSync(IPC_INPUT_CLOSE_SENTINEL); - } catch { - /* ignore */ - } - - // Build initial prompt (drain any pending IPC messages too) - let prompt = containerInput.prompt; - if (containerInput.isScheduledTask) { - prompt = `[SCHEDULED TASK - The following message was sent automatically and is not coming directly from the user or group.]\n\n${prompt}`; - } - const pending = drainIpcInput(); - if (pending.length > 0) { - log(`Draining ${pending.length} pending IPC messages into initial prompt`); - prompt += '\n' + pending.join('\n'); - } - - // Script phase: run script before waking agent - if (containerInput.script && containerInput.isScheduledTask) { - log('Running task script...'); - const scriptResult = await runScript(containerInput.script); - - if (!scriptResult || !scriptResult.wakeAgent) { - const reason = scriptResult - ? 'wakeAgent=false' - : 'script error/no output'; - log(`Script decided not to wake agent: ${reason}`); - writeOutput({ - status: 'success', - result: null, - }); - return; - } - - // Script says wake agent — enrich prompt with script data - log(`Script wakeAgent=true, enriching prompt with data`); - prompt = `[SCHEDULED TASK]\n\nScript output:\n${JSON.stringify(scriptResult.data, null, 2)}\n\nInstructions:\n${containerInput.prompt}`; - } - - // Query loop: run query → wait for IPC message → run new query → repeat - let resumeAt: string | undefined; - try { - while (true) { - log( - `Starting query (session: ${sessionId || 'new'}, resumeAt: ${resumeAt || 'latest'})...`, - ); - - const queryResult = await runQuery( - prompt, - sessionId, - mcpServerPath, - containerInput, - sdkEnv, - resumeAt, - ); - if (queryResult.newSessionId) { - sessionId = queryResult.newSessionId; - } - if (queryResult.lastAssistantUuid) { - resumeAt = queryResult.lastAssistantUuid; - } - - // If _close was consumed during the query, exit immediately. - // Don't emit a session-update marker (it would reset the host's - // idle timer and cause a 30-min delay before the next _close). - if (queryResult.closedDuringQuery) { - log('Close sentinel consumed during query, exiting'); - break; - } - - // Emit session update so host can track it - writeOutput({ status: 'success', result: null, newSessionId: sessionId }); - - log('Query ended, waiting for next IPC message...'); - - // Wait for the next message or _close sentinel - const nextMessage = await waitForIpcMessage(); - if (nextMessage === null) { - log('Close sentinel received, exiting'); - break; - } - - log(`Got new message (${nextMessage.length} chars), starting new query`); - prompt = nextMessage; - } - } catch (err) { - const errorMessage = err instanceof Error ? err.message : String(err); - log(`Agent error: ${errorMessage}`); - writeOutput({ - status: 'error', - result: null, - newSessionId: sessionId, - error: errorMessage, - }); - process.exit(1); - } + 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(); +main().catch((err) => { + log(`Fatal error: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); +}); diff --git a/container/agent-runner/src/v1/index.ts b/container/agent-runner/src/v1/index.ts new file mode 100644 index 0000000..7e739c7 --- /dev/null +++ b/container/agent-runner/src/v1/index.ts @@ -0,0 +1,736 @@ +/** + * NanoClaw Agent Runner + * Runs inside a container, receives config via stdin, outputs result to stdout + * + * Input protocol: + * Stdin: Full ContainerInput JSON (read until EOF, like before) + * IPC: Follow-up messages written as JSON files to /workspace/ipc/input/ + * Files: {type:"message", text:"..."}.json — polled and consumed + * Sentinel: /workspace/ipc/input/_close — signals session end + * + * Stdout protocol: + * Each result is wrapped in OUTPUT_START_MARKER / OUTPUT_END_MARKER pairs. + * Multiple results may be emitted (one per agent teams result). + * Final marker after loop ends signals completion. + */ + +import fs from 'fs'; +import path from 'path'; +import { execFile } from 'child_process'; +import { + query, + HookCallback, + PreCompactHookInput, +} from '@anthropic-ai/claude-agent-sdk'; +import { fileURLToPath } from 'url'; + +interface ContainerInput { + prompt: string; + sessionId?: string; + groupFolder: string; + chatJid: string; + isMain: boolean; + isScheduledTask?: boolean; + assistantName?: string; + script?: string; +} + +interface ContainerOutput { + status: 'success' | 'error'; + result: string | null; + newSessionId?: string; + error?: string; +} + +interface SessionEntry { + sessionId: string; + fullPath: string; + summary: string; + firstPrompt: string; +} + +interface SessionsIndex { + entries: SessionEntry[]; +} + +interface SDKUserMessage { + type: 'user'; + message: { role: 'user'; content: string }; + parent_tool_use_id: null; + session_id: string; +} + +const IPC_INPUT_DIR = '/workspace/ipc/input'; +const IPC_INPUT_CLOSE_SENTINEL = path.join(IPC_INPUT_DIR, '_close'); +const IPC_POLL_MS = 500; + +/** + * Push-based async iterable for streaming user messages to the SDK. + * Keeps the iterable alive until end() is called, preventing isSingleUserTurn. + */ +class MessageStream { + private queue: SDKUserMessage[] = []; + private waiting: (() => void) | null = null; + private done = false; + + push(text: string): void { + this.queue.push({ + type: 'user', + message: { role: 'user', content: text }, + parent_tool_use_id: null, + session_id: '', + }); + this.waiting?.(); + } + + end(): void { + this.done = true; + this.waiting?.(); + } + + async *[Symbol.asyncIterator](): AsyncGenerator { + while (true) { + while (this.queue.length > 0) { + yield this.queue.shift()!; + } + if (this.done) return; + await new Promise((r) => { + this.waiting = r; + }); + this.waiting = null; + } + } +} + +async function readStdin(): Promise { + return new Promise((resolve, reject) => { + let data = ''; + process.stdin.setEncoding('utf8'); + process.stdin.on('data', (chunk) => { + data += chunk; + }); + process.stdin.on('end', () => resolve(data)); + process.stdin.on('error', reject); + }); +} + +const OUTPUT_START_MARKER = '---NANOCLAW_OUTPUT_START---'; +const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---'; + +function writeOutput(output: ContainerOutput): void { + console.log(OUTPUT_START_MARKER); + console.log(JSON.stringify(output)); + console.log(OUTPUT_END_MARKER); +} + +function log(message: string): void { + console.error(`[agent-runner] ${message}`); +} + +function getSessionSummary( + sessionId: string, + transcriptPath: string, +): string | null { + const projectDir = path.dirname(transcriptPath); + const indexPath = path.join(projectDir, 'sessions-index.json'); + + if (!fs.existsSync(indexPath)) { + log(`Sessions index not found at ${indexPath}`); + return null; + } + + try { + const index: SessionsIndex = JSON.parse( + fs.readFileSync(indexPath, 'utf-8'), + ); + const entry = index.entries.find((e) => e.sessionId === sessionId); + if (entry?.summary) { + return entry.summary; + } + } catch (err) { + log( + `Failed to read sessions index: ${err instanceof Error ? err.message : String(err)}`, + ); + } + + return null; +} + +/** + * Archive the full transcript to conversations/ before compaction. + */ +function createPreCompactHook(assistantName?: string): HookCallback { + return async (input, _toolUseId, _context) => { + const preCompact = input as PreCompactHookInput; + const transcriptPath = preCompact.transcript_path; + const sessionId = preCompact.session_id; + + if (!transcriptPath || !fs.existsSync(transcriptPath)) { + log('No transcript found for archiving'); + return {}; + } + + try { + const content = fs.readFileSync(transcriptPath, 'utf-8'); + const messages = parseTranscript(content); + + if (messages.length === 0) { + log('No messages to archive'); + return {}; + } + + const summary = getSessionSummary(sessionId, transcriptPath); + const name = summary ? sanitizeFilename(summary) : generateFallbackName(); + + const conversationsDir = '/workspace/group/conversations'; + fs.mkdirSync(conversationsDir, { recursive: true }); + + const date = new Date().toISOString().split('T')[0]; + const filename = `${date}-${name}.md`; + const filePath = path.join(conversationsDir, filename); + + const markdown = formatTranscriptMarkdown( + messages, + summary, + assistantName, + ); + fs.writeFileSync(filePath, markdown); + + log(`Archived conversation to ${filePath}`); + } catch (err) { + log( + `Failed to archive transcript: ${err instanceof Error ? err.message : String(err)}`, + ); + } + + return {}; + }; +} + +function sanitizeFilename(summary: string): string { + return summary + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 50); +} + +function generateFallbackName(): string { + const time = new Date(); + return `conversation-${time.getHours().toString().padStart(2, '0')}${time.getMinutes().toString().padStart(2, '0')}`; +} + +interface ParsedMessage { + role: 'user' | 'assistant'; + content: string; +} + +function parseTranscript(content: string): ParsedMessage[] { + const messages: ParsedMessage[] = []; + + for (const line of content.split('\n')) { + if (!line.trim()) continue; + try { + const entry = JSON.parse(line); + if (entry.type === 'user' && entry.message?.content) { + const text = + typeof entry.message.content === 'string' + ? entry.message.content + : entry.message.content + .map((c: { text?: string }) => c.text || '') + .join(''); + if (text) messages.push({ role: 'user', content: text }); + } else if (entry.type === 'assistant' && entry.message?.content) { + const textParts = entry.message.content + .filter((c: { type: string }) => c.type === 'text') + .map((c: { text: string }) => c.text); + const text = textParts.join(''); + if (text) messages.push({ role: 'assistant', content: text }); + } + } catch {} + } + + return messages; +} + +function formatTranscriptMarkdown( + messages: ParsedMessage[], + title?: string | null, + assistantName?: string, +): string { + const now = new Date(); + const formatDateTime = (d: Date) => + d.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true, + }); + + const lines: string[] = []; + lines.push(`# ${title || 'Conversation'}`); + lines.push(''); + lines.push(`Archived: ${formatDateTime(now)}`); + lines.push(''); + lines.push('---'); + lines.push(''); + + for (const msg of messages) { + const sender = msg.role === 'user' ? 'User' : assistantName || 'Assistant'; + const content = + msg.content.length > 2000 + ? msg.content.slice(0, 2000) + '...' + : msg.content; + lines.push(`**${sender}**: ${content}`); + lines.push(''); + } + + return lines.join('\n'); +} + +/** + * Check for _close sentinel. + */ +function shouldClose(): boolean { + if (fs.existsSync(IPC_INPUT_CLOSE_SENTINEL)) { + try { + fs.unlinkSync(IPC_INPUT_CLOSE_SENTINEL); + } catch { + /* ignore */ + } + return true; + } + return false; +} + +/** + * Drain all pending IPC input messages. + * Returns messages found, or empty array. + */ +function drainIpcInput(): string[] { + try { + fs.mkdirSync(IPC_INPUT_DIR, { recursive: true }); + const files = fs + .readdirSync(IPC_INPUT_DIR) + .filter((f) => f.endsWith('.json')) + .sort(); + + const messages: string[] = []; + for (const file of files) { + const filePath = path.join(IPC_INPUT_DIR, file); + try { + const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + fs.unlinkSync(filePath); + if (data.type === 'message' && data.text) { + messages.push(data.text); + } + } catch (err) { + log( + `Failed to process input file ${file}: ${err instanceof Error ? err.message : String(err)}`, + ); + try { + fs.unlinkSync(filePath); + } catch { + /* ignore */ + } + } + } + return messages; + } catch (err) { + log(`IPC drain error: ${err instanceof Error ? err.message : String(err)}`); + return []; + } +} + +/** + * Wait for a new IPC message or _close sentinel. + * Returns the messages as a single string, or null if _close. + */ +function waitForIpcMessage(): Promise { + return new Promise((resolve) => { + const poll = () => { + if (shouldClose()) { + resolve(null); + return; + } + const messages = drainIpcInput(); + if (messages.length > 0) { + resolve(messages.join('\n')); + return; + } + setTimeout(poll, IPC_POLL_MS); + }; + poll(); + }); +} + +/** + * Run a single query and stream results via writeOutput. + * Uses MessageStream (AsyncIterable) to keep isSingleUserTurn=false, + * allowing agent teams subagents to run to completion. + * Also pipes IPC messages into the stream during the query. + */ +async function runQuery( + prompt: string, + sessionId: string | undefined, + mcpServerPath: string, + containerInput: ContainerInput, + sdkEnv: Record, + resumeAt?: string, +): Promise<{ + newSessionId?: string; + lastAssistantUuid?: string; + closedDuringQuery: boolean; +}> { + const stream = new MessageStream(); + stream.push(prompt); + + // Poll IPC for follow-up messages and _close sentinel during the query + let ipcPolling = true; + let closedDuringQuery = false; + const pollIpcDuringQuery = () => { + if (!ipcPolling) return; + if (shouldClose()) { + log('Close sentinel detected during query, ending stream'); + closedDuringQuery = true; + stream.end(); + ipcPolling = false; + return; + } + const messages = drainIpcInput(); + for (const text of messages) { + log(`Piping IPC message into active query (${text.length} chars)`); + stream.push(text); + } + setTimeout(pollIpcDuringQuery, IPC_POLL_MS); + }; + setTimeout(pollIpcDuringQuery, IPC_POLL_MS); + + let newSessionId: string | undefined; + let lastAssistantUuid: string | undefined; + let messageCount = 0; + let resultCount = 0; + + // Load global CLAUDE.md as additional system context (shared across all groups) + const globalClaudeMdPath = '/workspace/global/CLAUDE.md'; + let globalClaudeMd: string | undefined; + if (!containerInput.isMain && fs.existsSync(globalClaudeMdPath)) { + globalClaudeMd = fs.readFileSync(globalClaudeMdPath, 'utf-8'); + } + + // Discover additional directories mounted at /workspace/extra/* + // These are passed to the SDK so their CLAUDE.md files are loaded automatically + const extraDirs: 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()) { + extraDirs.push(fullPath); + } + } + } + if (extraDirs.length > 0) { + log(`Additional directories: ${extraDirs.join(', ')}`); + } + + for await (const message of query({ + prompt: stream, + options: { + cwd: '/workspace/group', + additionalDirectories: extraDirs.length > 0 ? extraDirs : undefined, + resume: sessionId, + resumeSessionAt: resumeAt, + systemPrompt: globalClaudeMd + ? { + type: 'preset' as const, + preset: 'claude_code' as const, + append: globalClaudeMd, + } + : undefined, + allowedTools: [ + 'Bash', + 'Read', + 'Write', + 'Edit', + 'Glob', + 'Grep', + 'WebSearch', + 'WebFetch', + 'Task', + 'TaskOutput', + 'TaskStop', + 'TeamCreate', + 'TeamDelete', + 'SendMessage', + 'TodoWrite', + 'ToolSearch', + 'Skill', + 'NotebookEdit', + 'mcp__nanoclaw__*', + ], + env: sdkEnv, + permissionMode: 'bypassPermissions', + allowDangerouslySkipPermissions: true, + settingSources: ['project', 'user'], + mcpServers: { + nanoclaw: { + command: 'node', + args: [mcpServerPath], + env: { + NANOCLAW_CHAT_JID: containerInput.chatJid, + NANOCLAW_GROUP_FOLDER: containerInput.groupFolder, + NANOCLAW_IS_MAIN: containerInput.isMain ? '1' : '0', + }, + }, + }, + hooks: { + PreCompact: [ + { hooks: [createPreCompactHook(containerInput.assistantName)] }, + ], + }, + }, + })) { + messageCount++; + const msgType = + message.type === 'system' + ? `system/${(message as { subtype?: string }).subtype}` + : message.type; + log(`[msg #${messageCount}] type=${msgType}`); + + if (message.type === 'assistant' && 'uuid' in message) { + lastAssistantUuid = (message as { uuid: string }).uuid; + } + + if (message.type === 'system' && message.subtype === 'init') { + newSessionId = message.session_id; + log(`Session initialized: ${newSessionId}`); + } + + if ( + message.type === 'system' && + (message as { subtype?: string }).subtype === 'task_notification' + ) { + const tn = message as { + task_id: string; + status: string; + summary: string; + }; + log( + `Task notification: task=${tn.task_id} status=${tn.status} summary=${tn.summary}`, + ); + } + + if (message.type === 'result') { + resultCount++; + const textResult = + 'result' in message ? (message as { result?: string }).result : null; + log( + `Result #${resultCount}: subtype=${message.subtype}${textResult ? ` text=${textResult.slice(0, 200)}` : ''}`, + ); + writeOutput({ + status: 'success', + result: textResult || null, + newSessionId, + }); + } + } + + ipcPolling = false; + log( + `Query done. Messages: ${messageCount}, results: ${resultCount}, lastAssistantUuid: ${lastAssistantUuid || 'none'}, closedDuringQuery: ${closedDuringQuery}`, + ); + return { newSessionId, lastAssistantUuid, closedDuringQuery }; +} + +interface ScriptResult { + wakeAgent: boolean; + data?: unknown; +} + +const SCRIPT_TIMEOUT_MS = 30_000; + +async function runScript(script: string): Promise { + const scriptPath = '/tmp/task-script.sh'; + fs.writeFileSync(scriptPath, script, { mode: 0o755 }); + + return new Promise((resolve) => { + execFile( + 'bash', + [scriptPath], + { + timeout: SCRIPT_TIMEOUT_MS, + maxBuffer: 1024 * 1024, + env: process.env, + }, + (error, stdout, stderr) => { + if (stderr) { + log(`Script stderr: ${stderr.slice(0, 500)}`); + } + + if (error) { + log(`Script error: ${error.message}`); + return resolve(null); + } + + // Parse last non-empty line of stdout as JSON + const lines = stdout.trim().split('\n'); + const lastLine = lines[lines.length - 1]; + if (!lastLine) { + log('Script produced no output'); + return resolve(null); + } + + try { + const result = JSON.parse(lastLine); + if (typeof result.wakeAgent !== 'boolean') { + log( + `Script output missing wakeAgent boolean: ${lastLine.slice(0, 200)}`, + ); + return resolve(null); + } + resolve(result as ScriptResult); + } catch { + log(`Script output is not valid JSON: ${lastLine.slice(0, 200)}`); + resolve(null); + } + }, + ); + }); +} + +async function main(): Promise { + let containerInput: ContainerInput; + + try { + const stdinData = await readStdin(); + containerInput = JSON.parse(stdinData); + try { + fs.unlinkSync('/tmp/input.json'); + } catch { + /* may not exist */ + } + log(`Received input for group: ${containerInput.groupFolder}`); + } catch (err) { + writeOutput({ + status: 'error', + result: null, + error: `Failed to parse input: ${err instanceof Error ? err.message : String(err)}`, + }); + process.exit(1); + } + + // Credentials are injected by the host's credential proxy via ANTHROPIC_BASE_URL. + // No real secrets exist in the container environment. + const sdkEnv: Record = { + ...process.env, + CLAUDE_CODE_AUTO_COMPACT_WINDOW: '165000', + }; + + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const mcpServerPath = path.join(__dirname, 'ipc-mcp-stdio.js'); + + let sessionId = containerInput.sessionId; + fs.mkdirSync(IPC_INPUT_DIR, { recursive: true }); + + // Clean up stale _close sentinel from previous container runs + try { + fs.unlinkSync(IPC_INPUT_CLOSE_SENTINEL); + } catch { + /* ignore */ + } + + // Build initial prompt (drain any pending IPC messages too) + let prompt = containerInput.prompt; + if (containerInput.isScheduledTask) { + prompt = `[SCHEDULED TASK - The following message was sent automatically and is not coming directly from the user or group.]\n\n${prompt}`; + } + const pending = drainIpcInput(); + if (pending.length > 0) { + log(`Draining ${pending.length} pending IPC messages into initial prompt`); + prompt += '\n' + pending.join('\n'); + } + + // Script phase: run script before waking agent + if (containerInput.script && containerInput.isScheduledTask) { + log('Running task script...'); + const scriptResult = await runScript(containerInput.script); + + if (!scriptResult || !scriptResult.wakeAgent) { + const reason = scriptResult + ? 'wakeAgent=false' + : 'script error/no output'; + log(`Script decided not to wake agent: ${reason}`); + writeOutput({ + status: 'success', + result: null, + }); + return; + } + + // Script says wake agent — enrich prompt with script data + log(`Script wakeAgent=true, enriching prompt with data`); + prompt = `[SCHEDULED TASK]\n\nScript output:\n${JSON.stringify(scriptResult.data, null, 2)}\n\nInstructions:\n${containerInput.prompt}`; + } + + // Query loop: run query → wait for IPC message → run new query → repeat + let resumeAt: string | undefined; + try { + while (true) { + log( + `Starting query (session: ${sessionId || 'new'}, resumeAt: ${resumeAt || 'latest'})...`, + ); + + const queryResult = await runQuery( + prompt, + sessionId, + mcpServerPath, + containerInput, + sdkEnv, + resumeAt, + ); + if (queryResult.newSessionId) { + sessionId = queryResult.newSessionId; + } + if (queryResult.lastAssistantUuid) { + resumeAt = queryResult.lastAssistantUuid; + } + + // If _close was consumed during the query, exit immediately. + // Don't emit a session-update marker (it would reset the host's + // idle timer and cause a 30-min delay before the next _close). + if (queryResult.closedDuringQuery) { + log('Close sentinel consumed during query, exiting'); + break; + } + + // Emit session update so host can track it + writeOutput({ status: 'success', result: null, newSessionId: sessionId }); + + log('Query ended, waiting for next IPC message...'); + + // Wait for the next message or _close sentinel + const nextMessage = await waitForIpcMessage(); + if (nextMessage === null) { + log('Close sentinel received, exiting'); + break; + } + + log(`Got new message (${nextMessage.length} chars), starting new query`); + prompt = nextMessage; + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + log(`Agent error: ${errorMessage}`); + writeOutput({ + status: 'error', + result: null, + newSessionId: sessionId, + error: errorMessage, + }); + process.exit(1); + } +} + +main(); diff --git a/container/agent-runner/src/ipc-mcp-stdio.ts b/container/agent-runner/src/v1/ipc-mcp-stdio.ts similarity index 100% rename from container/agent-runner/src/ipc-mcp-stdio.ts rename to container/agent-runner/src/v1/ipc-mcp-stdio.ts diff --git a/container/agent-runner/src/mcp-tools.ts b/container/agent-runner/src/v1/mcp-tools.ts similarity index 100% rename from container/agent-runner/src/mcp-tools.ts rename to container/agent-runner/src/v1/mcp-tools.ts diff --git a/package-lock.json b/package-lock.json index 97b055e..6a1e28c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,22 @@ "name": "nanoclaw", "version": "1.2.52", "dependencies": { + "@beeper/chat-adapter-matrix": "^0.2.0", + "@bitbasti/chat-adapter-webex": "^0.1.0", "@chat-adapter/discord": "^4.24.0", + "@chat-adapter/gchat": "^4.24.0", + "@chat-adapter/github": "^4.24.0", + "@chat-adapter/linear": "^4.24.0", + "@chat-adapter/slack": "^4.24.0", "@chat-adapter/state-memory": "^4.24.0", + "@chat-adapter/teams": "^4.24.0", + "@chat-adapter/telegram": "^4.24.0", + "@chat-adapter/whatsapp": "^4.24.0", "@onecli-sh/sdk": "^0.2.0", + "@resend/chat-sdk-adapter": "^0.1.1", "better-sqlite3": "11.10.0", "chat": "^4.24.0", + "chat-adapter-imessage": "^0.1.1", "cron-parser": "5.5.0" }, "devDependencies": { @@ -33,6 +44,67 @@ "node": ">=20" } }, + "node_modules/@azure/msal-common": { + "version": "15.17.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.17.0.tgz", + "integrity": "sha512-VQ5/gTLFADkwue+FohVuCqlzFPUq4xSrX8jeZe+iwZuY6moliNC8xt86qPVNYdtbQfELDf2Nu6LI+demFPHGgw==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "3.8.10", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.10.tgz", + "integrity": "sha512-0Hz7Kx4hs70KZWep/Rd7aw/qOLUF92wUOhn7ZsOuB5xNR/06NL1E2RAI9+UKH1FtvN8nD6mFjH7UKSjv6vOWvQ==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.17.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@beeper/chat-adapter-matrix": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@beeper/chat-adapter-matrix/-/chat-adapter-matrix-0.2.0.tgz", + "integrity": "sha512-eqKbU0iosIUkBn2dkRqyg+72a9c+v4vi85U81ZM8ETgjqHuZ34xWWntG2UL4ly6sHE/LiO4WL/k2Q+vlzLh8hw==", + "license": "MIT", + "dependencies": { + "@chat-adapter/state-memory": "^4.17.0", + "@chat-adapter/state-redis": "^4.17.0", + "chat": "^4.17.0", + "marked": "^15.0.12", + "matrix-js-sdk": "^41.0.0", + "node-html-parser": "^7.1.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@bitbasti/chat-adapter-webex": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@bitbasti/chat-adapter-webex/-/chat-adapter-webex-0.1.0.tgz", + "integrity": "sha512-Cl/gy3ifh18y0fs4f/qVNmHXfn+3v40x6QUjOUGZ5mEds2zYiqeVAalRkL+feBdjEz1tE8aQtzf1OUhRGPQeFw==", + "license": "MIT", + "dependencies": { + "@chat-adapter/shared": "^4.15.0" + }, + "peerDependencies": { + "chat": "^4.15.0" + } + }, "node_modules/@chat-adapter/discord": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/@chat-adapter/discord/-/discord-4.24.0.tgz", @@ -52,6 +124,41 @@ "integrity": "sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw==", "license": "MIT" }, + "node_modules/@chat-adapter/gchat": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/gchat/-/gchat-4.24.0.tgz", + "integrity": "sha512-60DAZMQ4EmnwruUP1CTkAOHnzuNM0Qjvh0ASa5c9Yxy1BTqFzWCtqVxssYL/VqBImgDkR9yO0vVlbvZjKTZ8gA==", + "license": "MIT", + "dependencies": { + "@chat-adapter/shared": "4.24.0", + "@googleapis/chat": "^44.6.0", + "@googleapis/workspaceevents": "^9.1.0", + "chat": "4.24.0" + } + }, + "node_modules/@chat-adapter/github": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/github/-/github-4.24.0.tgz", + "integrity": "sha512-iK2Wj0p8LH7aW6C53XxcR9ouzkkZrswaWY4DGlPT7+MBYd1u5HAMgruDcQFyoeiH4JZA4f0oCpccBidCnssDRQ==", + "license": "MIT", + "dependencies": { + "@chat-adapter/shared": "4.24.0", + "@octokit/auth-app": "^8.2.0", + "@octokit/rest": "^22.0.1", + "chat": "4.24.0" + } + }, + "node_modules/@chat-adapter/linear": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/linear/-/linear-4.24.0.tgz", + "integrity": "sha512-FrbIPyWMW5WWT4KFIO14Oc0iLwdUQG1R5eQ0oXLizVCXWb3COTwwNhhozO7eGL8ZDI+OrU7Tz8sWjNEakuBxSg==", + "license": "MIT", + "dependencies": { + "@chat-adapter/shared": "4.24.0", + "@linear/sdk": "^76.0.0", + "chat": "4.24.0" + } + }, "node_modules/@chat-adapter/shared": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/@chat-adapter/shared/-/shared-4.24.0.tgz", @@ -61,6 +168,17 @@ "chat": "4.24.0" } }, + "node_modules/@chat-adapter/slack": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/slack/-/slack-4.24.0.tgz", + "integrity": "sha512-K8QOYfYMVV8yQixspLAilhh2nou2sybW/M5+8WunegZZlpLqLfQHl78fAJsp+CRveo24bR4UlCcT92/EpGkwOA==", + "license": "MIT", + "dependencies": { + "@chat-adapter/shared": "4.24.0", + "@slack/web-api": "^7.14.0", + "chat": "4.24.0" + } + }, "node_modules/@chat-adapter/state-memory": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/@chat-adapter/state-memory/-/state-memory-4.24.0.tgz", @@ -70,6 +188,50 @@ "chat": "4.24.0" } }, + "node_modules/@chat-adapter/state-redis": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/state-redis/-/state-redis-4.24.0.tgz", + "integrity": "sha512-ne0jSUXSOuJUre0XP58F+JVwvMQXUdxoK0NVkKNKyKDSPfpyDkgnLUVnt1TVTihLrIFp+wPb1mpz/UZEv7NMJw==", + "license": "MIT", + "dependencies": { + "chat": "4.24.0", + "redis": "^5.11.0" + } + }, + "node_modules/@chat-adapter/teams": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/teams/-/teams-4.24.0.tgz", + "integrity": "sha512-h3+5ME25i47bBbgg3XekwuWZ7q3IorlyyvHiTrCnHzy4jFOrCW9of1fea+o4yzrQiwBtUgGBMbnxqFKV+Xg8+A==", + "license": "MIT", + "dependencies": { + "@chat-adapter/shared": "4.24.0", + "@microsoft/teams.api": "^2.0.6", + "@microsoft/teams.apps": "^2.0.6", + "@microsoft/teams.cards": "^2.0.6", + "@microsoft/teams.graph-endpoints": "^2.0.6", + "chat": "4.24.0" + } + }, + "node_modules/@chat-adapter/telegram": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/telegram/-/telegram-4.24.0.tgz", + "integrity": "sha512-xNsxQH2IFaOs9FEP8Yx5cI0qENl7P1slSNe1lH0nOqfHnOI65cVcUZqQ4i/RDNkS65E3XAxxWB6q9YS2ku7SSw==", + "license": "MIT", + "dependencies": { + "@chat-adapter/shared": "4.24.0", + "chat": "4.24.0" + } + }, + "node_modules/@chat-adapter/whatsapp": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@chat-adapter/whatsapp/-/whatsapp-4.24.0.tgz", + "integrity": "sha512-HEGwBDI+CXlZcaux2V/cX3YToRL2nsUx6cX0z9C40vovCicERP/Ax9M9qOTATqj4BWU9XTTCPniY6noGc3JgAg==", + "license": "MIT", + "dependencies": { + "@chat-adapter/shared": "4.24.0", + "chat": "4.24.0" + } + }, "node_modules/@discordjs/builders": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz", @@ -210,6 +372,16 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -810,6 +982,39 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@googleapis/chat": { + "version": "44.6.0", + "resolved": "https://registry.npmjs.org/@googleapis/chat/-/chat-44.6.0.tgz", + "integrity": "sha512-Bnqzev/bSTXSbE0/N2WS4Stnleo8j9bJJ1LkCBk1fXQnehcArVMv7q543rzPYU6MJql4D34On6diNGAuYtI9xQ==", + "license": "Apache-2.0", + "dependencies": { + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@googleapis/workspaceevents": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@googleapis/workspaceevents/-/workspaceevents-9.1.0.tgz", + "integrity": "sha512-aJiMrTi/YyUUaaTO0tnhTHDYU+N9CTD3l3FSfe0yzEHQl7DEc+1LISgdK1o2nurvCtguBEumify5kTkr6Cg5eA==", + "license": "Apache-2.0", + "dependencies": { + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -858,6 +1063,471 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -865,6 +1535,351 @@ "dev": true, "license": "MIT" }, + "node_modules/@linear/sdk": { + "version": "76.0.0", + "resolved": "https://registry.npmjs.org/@linear/sdk/-/sdk-76.0.0.tgz", + "integrity": "sha512-Xt0x5Kl6qBoWhGFypb8ykyP+c5kT7scmRPs1uJidSPOaRgkMJ/4y41QpmZCWCBUMmZtf/O0VktgQio6rLXT94w==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0" + }, + "engines": { + "node": ">=18.x" + } + }, + "node_modules/@matrix-org/matrix-sdk-crypto-wasm": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.0.0.tgz", + "integrity": "sha512-88+n+dvxLI1cjS10UIlKXVYK7TGWbpAnnaDC9fow7ch/hCvdu3dFhJ3tS3/13N9s9+1QFXB4FFuommj+tHJPhQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@microsoft/teams.api": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@microsoft/teams.api/-/teams.api-2.0.7.tgz", + "integrity": "sha512-SQu7d/alQ3ZKgBX2ur/0VbtxsDLMZb3HmGUVnzIWkvSzFkGcPQ8uPK//670gpEyFJVh2qqP0wFwOwH98/tO57w==", + "license": "MIT", + "dependencies": { + "@microsoft/teams.cards": "2.0.7", + "@microsoft/teams.common": "2.0.7", + "jwt-decode": "^4.0.0", + "qs": "^6.14.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@microsoft/teams.apps": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@microsoft/teams.apps/-/teams.apps-2.0.7.tgz", + "integrity": "sha512-1y7mLrM/HZfRn8tHK/vInMZCpMXjRPQ6QawboNXttJqEQxvlwNRK9nzDjnzuIyBF32oTVt/ro7Id38oNnhaXeQ==", + "license": "MIT", + "dependencies": { + "@azure/msal-node": "^3.8.1", + "@microsoft/teams.api": "2.0.7", + "@microsoft/teams.common": "2.0.7", + "@microsoft/teams.graph": "2.0.7", + "axios": "^1.12.0", + "cors": "^2.8.5", + "express": "^5.0.0", + "jsonwebtoken": "^9.0.2", + "jwks-rsa": "^3.2.0", + "reflect-metadata": "^0.2.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@microsoft/teams.cards": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@microsoft/teams.cards/-/teams.cards-2.0.7.tgz", + "integrity": "sha512-HUGw5OWKc6eCdinRLYqHgFyvScTplQs+PqUqHnf79wH1QNqAKCX+p7uF71YxTm383laJYOqDGYU6uvFEoTvOsA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@microsoft/teams.common": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@microsoft/teams.common/-/teams.common-2.0.7.tgz", + "integrity": "sha512-O3qWC/RbLbiJSAHyk1j5Ybx3GAxmM7DhFbfLW5a2sebEQ+Sn/hB/8rr+IsxlG2FAaUgrcKkir8B55wuKTlZPYw==", + "license": "MIT", + "dependencies": { + "axios": "^1.12.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@microsoft/teams.graph": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@microsoft/teams.graph/-/teams.graph-2.0.7.tgz", + "integrity": "sha512-hHX1gsCL7GFhAUz1CAT+PFar5U20/nA6sV4yJJaLygu0Wft10XgX3tJh1FckXBQlO1vCaDRtmcMJ9Eey0Z/wRg==", + "license": "MIT", + "dependencies": { + "@microsoft/teams.common": "2.0.7", + "qs": "^6.14.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@microsoft/teams.graph-endpoints": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@microsoft/teams.graph-endpoints/-/teams.graph-endpoints-2.0.7.tgz", + "integrity": "sha512-VYx2CeSqZnjsp8fvVgt0f5PahXk2OKBKUHo1ICPLX/pvzsxjB8+RYU/5dvXVzPweNRTbIJR5gAugzyZwL/1miQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@octokit/auth-app": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.2.0.tgz", + "integrity": "sha512-vVjdtQQwomrZ4V46B9LaCsxsySxGoHsyw6IYBov/TqJVROrlYdyNgw5q6tQbB7KZt53v1l1W53RiqTvpzL907g==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^9.0.3", + "@octokit/auth-oauth-user": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "toad-cache": "^3.7.0", + "universal-github-app-jwt": "^2.2.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-app": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.3.tgz", + "integrity": "sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^8.0.3", + "@octokit/auth-oauth-user": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-device": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.3.tgz", + "integrity": "sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-user": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.2.tgz", + "integrity": "sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^8.0.3", + "@octokit/oauth-methods": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/oauth-authorization-url": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", + "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/oauth-methods": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.2.tgz", + "integrity": "sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-authorization-url": "^8.0.0", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", + "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", + "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, "node_modules/@onecli-sh/sdk": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@onecli-sh/sdk/-/sdk-0.2.0.tgz", @@ -873,6 +1888,476 @@ "node": ">=20" } }, + "node_modules/@photon-ai/advanced-imessage-kit": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@photon-ai/advanced-imessage-kit/-/advanced-imessage-kit-1.14.3.tgz", + "integrity": "sha512-i/WqwhvI9CwL9sd78YkV7PJmGftR2Z03GyIpRfMb6P6WKisHja+72wErSu66HCTLzRheDwazO44tJUbNobGoig==", + "license": "MIT", + "dependencies": { + "axios": "^1.13.2", + "consola": "^3.4.2", + "form-data": "^4.0.4", + "reflect-metadata": "^0.2.2", + "sharp": "^0.34.5", + "socket.io-client": "^4.8.1", + "zod": "^4.3.6" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "typescript": "^5.9.3" + } + }, + "node_modules/@photon-ai/imessage-kit": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@photon-ai/imessage-kit/-/imessage-kit-2.1.2.tgz", + "integrity": "sha512-xteMkPqqWkPLv40M9gA1HJGS/fHXIWzzXNCwRfnC4+bj120KMXMacT9zOSoEcGk4MA0pGXcUMQPE16MdB+Bf/g==", + "license": "MIT", + "dependencies": { + "node-typedstream": "^1.4.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "better-sqlite3": "^12.5.0" + } + }, + "node_modules/@photon-ai/imessage-kit/node_modules/better-sqlite3": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/@react-email/body": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.2.1.tgz", + "integrity": "sha512-ljDiQiJDu/Fq//vSIIP0z5Nuvt4+DX1RqGasstChDGJB/14ogd4VdNS9aacoede/ZjGy3o3Qb+cxyS+XgM6SwQ==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/button": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.2.1.tgz", + "integrity": "sha512-qXyj7RZLE7POy9BMKSoqQ00tOXThjOZSUnI2Yu9i29IHngPlmrNayIWBoVKtElES7OWwypUcpiajwi1mUWx6/A==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/code-block": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.2.1.tgz", + "integrity": "sha512-M3B7JpVH4ytgn83/ujRR1k1DQHvTeABiDM61OvAbjLRPhC/5KLHU5KkzIbbuGIrjWwxAbL1kSQzU8MhLEtSxyw==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/code-inline": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.6.tgz", + "integrity": "sha512-jfhebvv3dVsp3OdPgKXnk8+e2pBiDVZejDOBFzBa/IblrAJ9cQDkN6rBD5IyEg8hTOxwbw3iaI/yZFmDmIguIA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/column": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.14.tgz", + "integrity": "sha512-f+W+Bk2AjNO77zynE33rHuQhyqVICx4RYtGX9NKsGUg0wWjdGP0qAuIkhx9Rnmk4/hFMo1fUrtYNqca9fwJdHg==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/components": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.8.tgz", + "integrity": "sha512-zY81ED6o5MWMzBkr9uZFuT24lWarT+xIbOZxI6C9dsFmCWBczM8IE1BgOI8rhpUK4JcYVDy1uKxYAFqsx2Bc4w==", + "license": "MIT", + "dependencies": { + "@react-email/body": "0.2.1", + "@react-email/button": "0.2.1", + "@react-email/code-block": "0.2.1", + "@react-email/code-inline": "0.0.6", + "@react-email/column": "0.0.14", + "@react-email/container": "0.0.16", + "@react-email/font": "0.0.10", + "@react-email/head": "0.0.13", + "@react-email/heading": "0.0.16", + "@react-email/hr": "0.0.12", + "@react-email/html": "0.0.12", + "@react-email/img": "0.0.12", + "@react-email/link": "0.0.13", + "@react-email/markdown": "0.0.18", + "@react-email/preview": "0.0.14", + "@react-email/render": "2.0.4", + "@react-email/row": "0.0.13", + "@react-email/section": "0.0.17", + "@react-email/tailwind": "2.0.5", + "@react-email/text": "0.1.6" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/container": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.16.tgz", + "integrity": "sha512-QWBB56RkkU0AJ9h+qy33gfT5iuZknPC7Un/IjZv9B0QmMIK+WWacc0cH6y2SV5Cv/b99hU94fjEMOOO4enpkbQ==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/font": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.10.tgz", + "integrity": "sha512-0urVSgCmQIfx5r7Xc586miBnQUVnGp3OTYUm8m5pwtQRdTRO5XrTtEfNJ3JhYhSOruV0nD8fd+dXtKXobum6tA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/head": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.13.tgz", + "integrity": "sha512-AJg6le/08Gz4tm+6MtKXqtNNyKHzmooOCdmtqmWxD7FxoAdU1eVcizhtQ0gcnVaY6ethEyE/hnEzQxt1zu5Kog==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/heading": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.16.tgz", + "integrity": "sha512-jmsKnQm1ykpBzw4hCYHwBkt5pW2jScXffPeEH5ZRF5tZeF5b1pvlFTO9han7C0pCkZYo1kEvWiRtx69yfCIwuw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/hr": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.12.tgz", + "integrity": "sha512-TwmOmBDibavUQpXBxpmZYi2Iks/yeZOzFYh+di9EltMSnEabH8dMZXrl+pxNXzCgZ2XE8HY7VmUL65Lenfu5PA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/html": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.12.tgz", + "integrity": "sha512-KTShZesan+UsreU7PDUV90afrZwU5TLwYlALuCSU0OT+/U8lULNNbAUekg+tGwCnOfIKYtpDPKkAMRdYlqUznw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/img": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.12.tgz", + "integrity": "sha512-sRCpEARNVTf3FQhZOC+JTvu5r6ubiYWkT0ucYXg8ctkyi4G8QG+jgYPiNUqVeTLA2STOfmPM/nrk1nb84y6CPQ==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/link": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.13.tgz", + "integrity": "sha512-lkWc/NjOcefRZMkQoSDDbuKBEBDES9aXnFEOuPH845wD3TxPwh+QTf0fStuzjoRLUZWpHnio4z7qGGRYusn/sw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/markdown": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.18.tgz", + "integrity": "sha512-gSuYK5fsMbGk87jDebqQ6fa2fKcWlkf2Dkva8kMONqLgGCq8/0d+ZQYMEJsdidIeBo3kmsnHZPrwdFB4HgjUXg==", + "license": "MIT", + "dependencies": { + "marked": "^15.0.12" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/preview": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.14.tgz", + "integrity": "sha512-aYK8q0IPkBXyMsbpMXgxazwHxYJxTrXrV95GFuu2HbEiIToMwSyUgb8HDFYwPqqfV03/jbwqlsXmFxsOd+VNaw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/render": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.4.tgz", + "integrity": "sha512-kht2oTFQ1SwrLpd882ahTvUtNa9s53CERHstiTbzhm6aR2Hbykp/mQ4tpPvsBGkKAEvKRlDEoooh60Uk6nHK1g==", + "license": "MIT", + "dependencies": { + "html-to-text": "^9.0.5", + "prettier": "^3.5.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/row": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.13.tgz", + "integrity": "sha512-bYnOac40vIKCId7IkwuLAAsa3fKfSfqCvv6epJKmPE0JBuu5qI4FHFCl9o9dVpIIS08s/ub+Y/txoMt0dYziGw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/section": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.17.tgz", + "integrity": "sha512-qNl65ye3W0Rd5udhdORzTV9ezjb+GFqQQSae03NDzXtmJq6sqVXNWNiVolAjvJNypim+zGXmv6J9TcV5aNtE/w==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/tailwind": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.5.tgz", + "integrity": "sha512-7Ey+kiWliJdxPMCLYsdDts8ffp4idlP//w4Ui3q/A5kokVaLSNKG8DOg/8qAuzWmRiGwNQVOKBk7PXNlK5W+sg==", + "license": "MIT", + "dependencies": { + "tailwindcss": "^4.1.18" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@react-email/body": "0.2.1", + "@react-email/button": "0.2.1", + "@react-email/code-block": "0.2.1", + "@react-email/code-inline": "0.0.6", + "@react-email/container": "0.0.16", + "@react-email/heading": "0.0.16", + "@react-email/hr": "0.0.12", + "@react-email/img": "0.0.12", + "@react-email/link": "0.0.13", + "@react-email/preview": "0.0.14", + "@react-email/text": "0.1.6", + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@react-email/body": { + "optional": true + }, + "@react-email/button": { + "optional": true + }, + "@react-email/code-block": { + "optional": true + }, + "@react-email/code-inline": { + "optional": true + }, + "@react-email/container": { + "optional": true + }, + "@react-email/heading": { + "optional": true + }, + "@react-email/hr": { + "optional": true + }, + "@react-email/img": { + "optional": true + }, + "@react-email/link": { + "optional": true + }, + "@react-email/preview": { + "optional": true + } + } + }, + "node_modules/@react-email/text": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.6.tgz", + "integrity": "sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@redis/bloom": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.11.0.tgz", + "integrity": "sha512-KYiVilAhAFN3057afUb/tfYJpsEyTkQB+tQcn5gVVA7DgcNOAj8lLxe4j8ov8BF6I9C1Fe/kwlbuAICcTMX8Lw==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.11.0" + } + }, + "node_modules/@redis/client": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.11.0.tgz", + "integrity": "sha512-GHoprlNQD51Xq2Ztd94HHV94MdFZQ3CVrpA04Fz8MVoHM0B7SlbmPEVIjwTbcv58z8QyjnrOuikS0rWF03k5dQ==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@node-rs/xxhash": "^1.1.0" + }, + "peerDependenciesMeta": { + "@node-rs/xxhash": { + "optional": true + } + } + }, + "node_modules/@redis/json": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.11.0.tgz", + "integrity": "sha512-1iAy9kAtcD0quB21RbPTbUqqy+T2Uu2JxucwE+B4A+VaDbIRvpZR6DMqV8Iqaws2YxJYB3GC5JVNzPYio2ErUg==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.11.0" + } + }, + "node_modules/@redis/search": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.11.0.tgz", + "integrity": "sha512-g1l7f3Rnyk/xI99oGHIgWHSKFl45Re5YTIcO8j/JE8olz389yUFyz2+A6nqVy/Zi031VgPDWscbbgOk8hlhZ3g==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.11.0" + } + }, + "node_modules/@redis/time-series": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.11.0.tgz", + "integrity": "sha512-TWFeOcU4xkj0DkndnOyhtxvX1KWD+78UHT3XX3x3XRBUGWeQrKo3jqzDsZwxbggUgf9yLJr/akFHXru66X5UQA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.11.0" + } + }, + "node_modules/@resend/chat-sdk-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@resend/chat-sdk-adapter/-/chat-sdk-adapter-0.1.1.tgz", + "integrity": "sha512-8rGteBhvmIOU38zUun6Jwfgw3hfxKmyhvz329lJ6XIKidHy9wTWMoV9DpuR6typiiKhNnSZeDEB6/3kE3S2J3A==", + "license": "MIT", + "dependencies": { + "@react-email/components": "1.0.8", + "@react-email/render": "2.0.4", + "hast-util-to-html": "^9.0.5", + "mdast-util-to-hast": "^13.2.1", + "resend": "6.9.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@chat-adapter/shared": "^4.15.0", + "chat": "^4.15.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", @@ -1256,6 +2741,78 @@ "npm": ">=7.0.0" } }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/@slack/logger": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.1.tgz", + "integrity": "sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==", + "license": "MIT", + "dependencies": { + "@types/node": ">=18" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/types": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.20.1.tgz", + "integrity": "sha512-eWX2mdt1ktpn8+40iiMc404uGrih+2fxiky3zBcPjtXKj6HLRdYlmhrPkJi7JTJm8dpXR6BWVWEDBXtaWMKD6A==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/web-api": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.15.0.tgz", + "integrity": "sha512-va7zYIt3QHG1x9M/jqXXRPFMoOVlVSSRHC5YH+DzKYsrz5xUKOA3lR4THsu/Zxha9N1jOndbKFKLtr0WOPW1Vw==", + "license": "MIT", + "dependencies": { + "@slack/logger": "^4.0.1", + "@slack/types": "^2.20.1", + "@types/node": ">=18", + "@types/retry": "0.12.0", + "axios": "^1.13.5", + "eventemitter3": "^5.0.1", + "form-data": "^4.0.4", + "is-electron": "2.2.2", + "is-stream": "^2", + "p-queue": "^6", + "p-retry": "^4", + "retry": "^0.13.1" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1307,12 +2864,37 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/events": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", + "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -1337,6 +2919,12 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -1619,6 +3207,12 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@vitest/expect": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", @@ -1746,6 +3340,19 @@ "integrity": "sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==", "license": "Apache-2.0" }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -1767,6 +3374,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -1783,6 +3399,12 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/another-json": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz", + "integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==", + "license": "Apache-2.0" + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1814,6 +3436,23 @@ "node": ">=12" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -1830,6 +3469,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1850,6 +3495,12 @@ ], "license": "MIT" }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "license": "Apache-2.0" + }, "node_modules/better-sqlite3": { "version": "11.10.0", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", @@ -1861,6 +3512,24 @@ "prebuild-install": "^7.1.1" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -1881,6 +3550,48 @@ "readable-stream": "^3.4.0" } }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", @@ -1892,6 +3603,15 @@ "concat-map": "0.0.1" } }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1916,6 +3636,50 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1971,6 +3735,26 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chat": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/chat/-/chat-4.24.0.tgz", @@ -1986,12 +3770,35 @@ "unified": "^11.0.5" } }, + "node_modules/chat-adapter-imessage": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/chat-adapter-imessage/-/chat-adapter-imessage-0.1.1.tgz", + "integrity": "sha512-Lq4FZqvV8QnwtD3CVUPF56L6J4aIEaOY08+uuSWBsxKKtTBH/rbJltJiiz2QRGvvWRyuBsjJ0RzXn4kiDG0LaQ==", + "license": "MIT", + "dependencies": { + "@chat-adapter/shared": "^4.15.0", + "@photon-ai/advanced-imessage-kit": "^1.14.3", + "@photon-ai/imessage-kit": "^2.1.2" + }, + "peerDependencies": { + "chat": "^4.14.0" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2010,12 +3817,100 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cron-parser": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-5.5.0.tgz", @@ -2042,6 +3937,43 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2102,6 +4034,33 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2178,6 +4137,99 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -2187,6 +4239,79 @@ "once": "^1.4.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -2194,6 +4319,33 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -2236,6 +4388,12 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2413,6 +4571,30 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -2432,12 +4614,71 @@ "node": ">=12.0.0" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2455,6 +4696,12 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2473,6 +4720,29 @@ } } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2491,6 +4761,27 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2526,6 +4817,93 @@ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -2547,6 +4925,80 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-tsconfig": { "version": "4.13.6", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", @@ -2590,6 +5042,70 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis-common": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.1.tgz", + "integrity": "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^7.0.0-rc.4", + "google-auth-library": "^10.1.0", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphql": { + "version": "16.13.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", + "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2600,6 +5116,168 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -2616,6 +5294,22 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2682,6 +5376,21 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==", + "license": "MIT" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2703,6 +5412,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-network-error": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", + "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -2715,12 +5436,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -2733,6 +5481,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2751,6 +5508,80 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-with-bigint": { + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", + "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.2.tgz", + "integrity": "sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.4", + "debug": "^4.3.4", + "jose": "^4.15.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2760,6 +5591,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2773,6 +5613,11 @@ "node": ">= 0.8.0" } }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2794,18 +5639,79 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lodash.snakecase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", "license": "MIT" }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -2816,6 +5722,28 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, "node_modules/luxon": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", @@ -2851,6 +5779,96 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/matrix-events-sdk": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz", + "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==", + "license": "Apache-2.0" + }, + "node_modules/matrix-js-sdk": { + "version": "41.3.0", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-41.3.0.tgz", + "integrity": "sha512-QTNHpBQEKPH3WS4O92CBfFj6GxeyijT8osI/QxNvOrM3rE6CySXRtRRKnzR0ntFSdrk1CxrDGV6h2wmk7B3peQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@matrix-org/matrix-sdk-crypto-wasm": "^18.0.0", + "another-json": "^0.2.0", + "bs58": "^6.0.0", + "content-type": "^1.0.4", + "jwt-decode": "^4.0.0", + "loglevel": "^1.9.2", + "matrix-events-sdk": "0.0.1", + "matrix-widget-api": "^1.16.1", + "oidc-client-ts": "^3.0.1", + "p-retry": "7", + "sdp-transform": "^3.0.0", + "unhomoglyph": "^1.0.6", + "uuid": "13" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/matrix-js-sdk/node_modules/p-retry": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz", + "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==", + "license": "MIT", + "dependencies": { + "is-network-error": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/matrix-js-sdk/node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/matrix-widget-api": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.17.0.tgz", + "integrity": "sha512-5FHoo3iEP3Bdlv5jsYPWOqj+pGdFQNLWnJLiB0V7Ygne7bb+Gsj3ibyFyHWC6BVw+Z+tSW4ljHpO17I9TwStwQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/events": "^3.0.0", + "events": "^3.2.0" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -3018,6 +6036,27 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-markdown": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", @@ -3052,6 +6091,27 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -3615,6 +6675,31 @@ ], "license": "MIT" }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -3691,6 +6776,15 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-abi": { "version": "3.87.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", @@ -3703,6 +6797,96 @@ "node": ">=10" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-html-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.1.0.tgz", + "integrity": "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/node-typedstream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/node-typedstream/-/node-typedstream-1.4.1.tgz", + "integrity": "sha512-W9zcPlI3RRPOmwaDjwRyr7aYLoJFbvLIIHluFM3I+KZjAlbyhG4L3jSTEJlQmDqrMRQlFVTmivgJWgFlvWXx2Q==", + "license": "MIT", + "dependencies": { + "bplist-parser": "^0.3.2" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -3714,6 +6898,30 @@ ], "license": "MIT" }, + "node_modules/oidc-client-ts": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.5.0.tgz", + "integrity": "sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==", + "license": "Apache-2.0", + "dependencies": { + "jwt-decode": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3740,6 +6948,15 @@ "node": ">= 0.8.0" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3770,6 +6987,53 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3782,6 +7046,28 @@ "node": ">=6" } }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3800,6 +7086,16 @@ "node": ">=8" } }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3807,6 +7103,15 @@ "dev": true, "license": "MIT" }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3827,6 +7132,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postal-mime": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.3.tgz", + "integrity": "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==", + "license": "MIT-0" + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -3895,7 +7206,6 @@ "version": "3.8.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", - "dev": true, "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -3907,6 +7217,47 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -3926,6 +7277,45 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -3950,6 +7340,29 @@ "node": ">=0.10.0" } }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -3964,6 +7377,28 @@ "node": ">= 6" } }, + "node_modules/redis": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-5.11.0.tgz", + "integrity": "sha512-YwXjATVDT+AuxcyfOwZn046aml9jMlQPvU1VXIlLDVAExe0u93aTfPYSeRgG4p9Q/Jlkj+LXJ1XEoFV+j2JKcQ==", + "license": "MIT", + "dependencies": { + "@redis/bloom": "5.11.0", + "@redis/client": "5.11.0", + "@redis/json": "5.11.0", + "@redis/search": "5.11.0", + "@redis/time-series": "5.11.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", @@ -4019,6 +7454,27 @@ "integrity": "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw==", "license": "Apache-2.0" }, + "node_modules/resend": { + "version": "6.9.2", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.9.2.tgz", + "integrity": "sha512-uIM6CQ08tS+hTCRuKBFbOBvHIGaEhqZe8s4FOgqsVXSbQLAhmNWpmUhG3UAtRnmcwTWFUqnHa/+Vux8YGPyDBA==", + "license": "MIT", + "dependencies": { + "postal-mime": "2.7.3", + "svix": "1.84.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4038,6 +7494,15 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/rollup": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", @@ -4083,6 +7548,22 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4103,6 +7584,40 @@ ], "license": "MIT" }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT", + "peer": true + }, + "node_modules/sdp-transform": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-3.0.0.tgz", + "integrity": "sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==", + "license": "MIT", + "bin": { + "sdp-verify": "checker.js" + } + }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -4115,6 +7630,101 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4136,6 +7746,78 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -4188,6 +7870,34 @@ "simple-concat": "^1.0.0" } }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4198,6 +7908,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -4205,6 +7925,25 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -4221,6 +7960,20 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4234,6 +7987,35 @@ "node": ">=8" } }, + "node_modules/svix": { + "version": "1.84.1", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.84.1.tgz", + "integrity": "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==", + "license": "MIT", + "dependencies": { + "standardwebhooks": "1.0.0", + "uuid": "^10.0.0" + } + }, + "node_modules/svix/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "license": "MIT" + }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -4306,6 +8088,34 @@ "node": ">=14.0.0" } }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", @@ -4384,11 +8194,24 @@ "node": ">= 0.8.0" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -4436,6 +8259,12 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unhomoglyph": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz", + "integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==", + "license": "MIT" + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -4468,6 +8297,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", @@ -4510,6 +8352,27 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universal-github-app-jwt": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz", + "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==", + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4519,12 +8382,36 @@ "punycode": "^2.1.0" } }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -4706,6 +8593,15 @@ } } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4774,6 +8670,20 @@ } } }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -4786,6 +8696,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 91bbfbb..1997774 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,22 @@ "test:watch": "vitest" }, "dependencies": { + "@beeper/chat-adapter-matrix": "^0.2.0", + "@bitbasti/chat-adapter-webex": "^0.1.0", "@chat-adapter/discord": "^4.24.0", + "@chat-adapter/gchat": "^4.24.0", + "@chat-adapter/github": "^4.24.0", + "@chat-adapter/linear": "^4.24.0", + "@chat-adapter/slack": "^4.24.0", "@chat-adapter/state-memory": "^4.24.0", + "@chat-adapter/teams": "^4.24.0", + "@chat-adapter/telegram": "^4.24.0", + "@chat-adapter/whatsapp": "^4.24.0", "@onecli-sh/sdk": "^0.2.0", + "@resend/chat-sdk-adapter": "^0.1.1", "better-sqlite3": "11.10.0", "chat": "^4.24.0", + "chat-adapter-imessage": "^0.1.1", "cron-parser": "5.5.0" }, "devDependencies": { diff --git a/setup/container.ts b/setup/container.ts index cc44350..100a884 100644 --- a/setup/container.ts +++ b/setup/container.ts @@ -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 { // 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'); } } diff --git a/setup/environment.ts b/setup/environment.ts index b9814ee..66f6fd0 100644 --- a/setup/environment.ts +++ b/setup/environment.ts @@ -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 { 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 { } } - logger.info( + log.info( + 'Environment check complete', { platform, wsl, @@ -76,7 +77,6 @@ export async function run(_args: string[]): Promise { hasAuth, hasRegisteredGroups, }, - 'Environment check complete', ); emitStatus('CHECK_ENVIRONMENT', { diff --git a/setup/groups.ts b/setup/groups.ts index 6697029..208bd75 100644 --- a/setup/groups.ts +++ b/setup/groups.ts @@ -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 { 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 { } // 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 { 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 { } // 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) diff --git a/setup/index.ts b/setup/index.ts index 7e10ddc..9975022 100644 --- a/setup/index.ts +++ b/setup/index.ts @@ -2,7 +2,7 @@ * Setup CLI entry point. * Usage: npx tsx setup/index.ts --step [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 { 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, diff --git a/setup/mounts.ts b/setup/mounts.ts index e14d23b..a456175 100644 --- a/setup/mounts.ts +++ b/setup/mounts.ts @@ -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 { 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 { 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 { 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 { 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 { 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 { nonMainReadOnly = parsed.nonMainReadOnly === false ? 'false' : 'true'; } - logger.info( - { configFile, allowedRoots, nonMainReadOnly }, + log.info( 'Allowlist configured', + { configFile, allowedRoots, nonMainReadOnly }, ); emitStatus('CONFIGURE_MOUNTS', { diff --git a/setup/register.ts b/setup/register.ts index c08d910..ee7854e 100644 --- a/setup/register.ts +++ b/setup/register.ts @@ -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 { 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 { 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 { : 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 { // 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 { `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 { } 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; } diff --git a/setup/service.ts b/setup/service.ts index c385267..9fd14d2 100644 --- a/setup/service.ts +++ b/setup/service.ts @@ -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 { 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( `; 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', diff --git a/setup/timezone.ts b/setup/timezone.ts index 22c0394..18b1443 100644 --- a/setup/timezone.ts +++ b/setup/timezone.ts @@ -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 { @@ -53,7 +53,7 @@ export async function run(args: string[]): Promise { } 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', { diff --git a/setup/verify.ts b/setup/verify.ts index e039e52..6b2077a 100644 --- a/setup/verify.ts +++ b/setup/verify.ts @@ -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 { 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 { } } } - 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 { ? 'success' : 'failed'; - logger.info({ status, channelAuth }, 'Verification complete'); + log.info('Verification complete', { status, channelAuth }); emitStatus('VERIFY', { SERVICE: service, diff --git a/src/channels/channel-registry.test.ts b/src/channels/channel-registry.test.ts index 1903791..d5d0fa0 100644 --- a/src/channels/channel-registry.test.ts +++ b/src/channels/channel-registry.test.ts @@ -8,7 +8,7 @@ import fs from 'fs'; import type { ChannelAdapter, ChannelSetup, InboundMessage, OutboundMessage } from './adapter.js'; // Mock container runner -vi.mock('../container-runner-v2.js', () => ({ +vi.mock('../container-runner.js', () => ({ wakeContainer: vi.fn().mockResolvedValue(undefined), resetContainerIdleTimer: vi.fn(), isContainerRunning: vi.fn().mockReturnValue(false), @@ -160,7 +160,7 @@ describe('channel + router integration', () => { }); it('should route inbound message from adapter to session DB', async () => { - const { routeInbound } = await import('../router-v2.js'); + const { routeInbound } = await import('../router.js'); const { findSession } = await import('../db/sessions.js'); const { sessionDbPath } = await import('../session-manager.js'); @@ -209,7 +209,7 @@ describe('channel + router integration', () => { onAction: () => {}, })); - // Set up delivery adapter bridge (same pattern as index-v2.ts) + // Set up delivery adapter bridge (same pattern as index.ts) setDeliveryAdapter({ async deliver(channelType, platformId, threadId, kind, content) { const adapter = getChannelAdapter(channelType); diff --git a/src/channels/discord-v2.ts b/src/channels/discord.ts similarity index 100% rename from src/channels/discord-v2.ts rename to src/channels/discord.ts diff --git a/src/channels/gchat-v2.ts b/src/channels/gchat.ts similarity index 100% rename from src/channels/gchat-v2.ts rename to src/channels/gchat.ts diff --git a/src/channels/github-v2.ts b/src/channels/github.ts similarity index 100% rename from src/channels/github-v2.ts rename to src/channels/github.ts diff --git a/src/channels/imessage-v2.ts b/src/channels/imessage.ts similarity index 90% rename from src/channels/imessage-v2.ts rename to src/channels/imessage.ts index a31a76d..8ab4215 100644 --- a/src/channels/imessage-v2.ts +++ b/src/channels/imessage.ts @@ -20,6 +20,6 @@ registerChannelAdapter('imessage', { serverUrl: env.IMESSAGE_SERVER_URL, apiKey: env.IMESSAGE_API_KEY, }); - return createChatSdkBridge({ adapter: imessageAdapter, concurrency: 'concurrent' }); + return createChatSdkBridge({ adapter: imessageAdapter as never, concurrency: 'concurrent' }); }, }); diff --git a/src/channels/index.ts b/src/channels/index.ts index bad8090..4b3b125 100644 --- a/src/channels/index.ts +++ b/src/channels/index.ts @@ -2,40 +2,40 @@ // Each import triggers the channel module's registerChannelAdapter() call. // discord -// import './discord-v2.js'; +// import './discord.js'; // slack -// import './slack-v2.js'; +// import './slack.js'; // telegram -// import './telegram-v2.js'; +// import './telegram.js'; // github -// import './github-v2.js'; +// import './github.js'; // linear -// import './linear-v2.js'; +// import './linear.js'; // google chat -// import './gchat-v2.js'; +// import './gchat.js'; // microsoft teams -// import './teams-v2.js'; +// import './teams.js'; // whatsapp cloud api -// import './whatsapp-cloud-v2.js'; +// import './whatsapp-cloud.js'; // resend (email) -// import './resend-v2.js'; +// import './resend.js'; // matrix -// import './matrix-v2.js'; +// import './matrix.js'; // webex -// import './webex-v2.js'; +// import './webex.js'; // imessage -// import './imessage-v2.js'; +// import './imessage.js'; // gmail (native, no Chat SDK) diff --git a/src/channels/linear-v2.ts b/src/channels/linear.ts similarity index 100% rename from src/channels/linear-v2.ts rename to src/channels/linear.ts diff --git a/src/channels/matrix-v2.ts b/src/channels/matrix.ts similarity index 100% rename from src/channels/matrix-v2.ts rename to src/channels/matrix.ts diff --git a/src/channels/resend-v2.ts b/src/channels/resend.ts similarity index 100% rename from src/channels/resend-v2.ts rename to src/channels/resend.ts diff --git a/src/channels/slack-v2.ts b/src/channels/slack.ts similarity index 100% rename from src/channels/slack-v2.ts rename to src/channels/slack.ts diff --git a/src/channels/teams-v2.ts b/src/channels/teams.ts similarity index 100% rename from src/channels/teams-v2.ts rename to src/channels/teams.ts diff --git a/src/channels/telegram-v2.ts b/src/channels/telegram.ts similarity index 100% rename from src/channels/telegram-v2.ts rename to src/channels/telegram.ts diff --git a/src/channels/webex-v2.ts b/src/channels/webex.ts similarity index 100% rename from src/channels/webex-v2.ts rename to src/channels/webex.ts diff --git a/src/channels/whatsapp-cloud-v2.ts b/src/channels/whatsapp-cloud.ts similarity index 84% rename from src/channels/whatsapp-cloud-v2.ts rename to src/channels/whatsapp-cloud.ts index 74b8160..e56eb99 100644 --- a/src/channels/whatsapp-cloud-v2.ts +++ b/src/channels/whatsapp-cloud.ts @@ -11,7 +11,12 @@ import { registerChannelAdapter } from './channel-registry.js'; registerChannelAdapter('whatsapp-cloud', { factory: () => { - const env = readEnvFile(['WHATSAPP_ACCESS_TOKEN', 'WHATSAPP_PHONE_NUMBER_ID', 'WHATSAPP_APP_SECRET', 'WHATSAPP_VERIFY_TOKEN']); + const env = readEnvFile([ + 'WHATSAPP_ACCESS_TOKEN', + 'WHATSAPP_PHONE_NUMBER_ID', + 'WHATSAPP_APP_SECRET', + 'WHATSAPP_VERIFY_TOKEN', + ]); if (!env.WHATSAPP_ACCESS_TOKEN) return null; const whatsappAdapter = createWhatsAppAdapter({ accessToken: env.WHATSAPP_ACCESS_TOKEN, diff --git a/src/container-runner-v2.ts b/src/container-runner-v2.ts deleted file mode 100644 index 81bbd50..0000000 --- a/src/container-runner-v2.ts +++ /dev/null @@ -1,277 +0,0 @@ -/** - * Container Runner v2 - * Spawns agent containers with session folder + agent group folder mounts. - * The container runs the v2 agent-runner which polls the session DB. - */ -import { ChildProcess, spawn } from 'child_process'; -import fs from 'fs'; -import path from 'path'; - -import { OneCLI } from '@onecli-sh/sdk'; - -import { CONTAINER_IMAGE, DATA_DIR, GROUPS_DIR, IDLE_TIMEOUT, ONECLI_URL, TIMEZONE } from './config.js'; -import { CONTAINER_RUNTIME_BIN, hostGatewayArgs, readonlyMountArgs, stopContainer } from './container-runtime.js'; -import { getAgentGroup } from './db/agent-groups.js'; -import { getMessagingGroup } from './db/messaging-groups.js'; -import { log } from './log.js'; -import { validateAdditionalMounts } from './mount-security.js'; -import { - markContainerIdle, - markContainerRunning, - markContainerStopped, - sessionDbPath, - sessionDir, -} from './session-manager.js'; -import type { AgentGroup, Session } from './types-v2.js'; - -const onecli = new OneCLI({ url: ONECLI_URL }); - -interface VolumeMount { - hostPath: string; - containerPath: string; - readonly: boolean; -} - -/** Active containers tracked by session ID. */ -const activeContainers = new Map(); - -export function getActiveContainerCount(): number { - return activeContainers.size; -} - -export function isContainerRunning(sessionId: string): boolean { - return activeContainers.has(sessionId); -} - -/** - * Wake up a container for a session. If already running, no-op. - * The container runs the v2 agent-runner which polls the session DB. - */ -export async function wakeContainer(session: Session): Promise { - if (activeContainers.has(session.id)) { - log.debug('Container already running', { sessionId: session.id }); - return; - } - - const agentGroup = getAgentGroup(session.agent_group_id); - if (!agentGroup) { - log.error('Agent group not found', { agentGroupId: session.agent_group_id }); - return; - } - - const mounts = buildMounts(agentGroup, session); - const containerName = `nanoclaw-v2-${agentGroup.folder}-${Date.now()}`; - const agentIdentifier = agentGroup.is_admin ? undefined : agentGroup.folder.toLowerCase().replace(/_/g, '-'); - const args = await buildContainerArgs(mounts, containerName, session, agentGroup, agentIdentifier); - - log.info('Spawning container', { sessionId: session.id, agentGroup: agentGroup.name, containerName }); - - const container = spawn(CONTAINER_RUNTIME_BIN, args, { stdio: ['ignore', 'pipe', 'pipe'] }); - - activeContainers.set(session.id, { process: container, containerName }); - markContainerRunning(session.id); - - // Log stderr - container.stderr?.on('data', (data) => { - for (const line of data.toString().trim().split('\n')) { - if (line) log.debug(line, { container: agentGroup.folder }); - } - }); - - // stdout is unused in v2 (all IO is via session DB) - container.stdout?.on('data', () => {}); - - // Idle timeout: kill container after IDLE_TIMEOUT of no activity - let idleTimer = setTimeout(() => killContainer(session.id, 'idle timeout'), IDLE_TIMEOUT); - - const resetIdle = () => { - clearTimeout(idleTimer); - idleTimer = setTimeout(() => killContainer(session.id, 'idle timeout'), IDLE_TIMEOUT); - }; - - // Reset idle timer when the host detects new messages_out (called by delivery.ts) - const entry = activeContainers.get(session.id); - if (entry) { - (entry as { resetIdle?: () => void }).resetIdle = resetIdle; - } - - container.on('close', (code) => { - clearTimeout(idleTimer); - activeContainers.delete(session.id); - markContainerStopped(session.id); - log.info('Container exited', { sessionId: session.id, code, containerName }); - }); - - container.on('error', (err) => { - clearTimeout(idleTimer); - activeContainers.delete(session.id); - markContainerStopped(session.id); - log.error('Container spawn error', { sessionId: session.id, err }); - }); -} - -/** Reset the idle timer for a session's container (called when messages_out are delivered). */ -export function resetContainerIdleTimer(sessionId: string): void { - const entry = activeContainers.get(sessionId) as { resetIdle?: () => void } | undefined; - entry?.resetIdle?.(); -} - -/** Kill a container for a session. */ -export function killContainer(sessionId: string, reason: string): void { - const entry = activeContainers.get(sessionId); - if (!entry) return; - - log.info('Killing container', { sessionId, reason, containerName: entry.containerName }); - try { - stopContainer(entry.containerName); - } catch { - entry.process.kill('SIGKILL'); - } -} - -function buildMounts(agentGroup: AgentGroup, session: Session): VolumeMount[] { - const mounts: VolumeMount[] = []; - const projectRoot = process.cwd(); - const sessDir = sessionDir(agentGroup.id, session.id); - const groupDir = path.resolve(GROUPS_DIR, agentGroup.folder); - - // Session folder at /workspace (contains session.db, outbox/, .claude/) - mounts.push({ hostPath: sessDir, containerPath: '/workspace', readonly: false }); - - // Agent group folder at /workspace/agent - fs.mkdirSync(groupDir, { recursive: true }); - mounts.push({ hostPath: groupDir, containerPath: '/workspace/agent', readonly: false }); - - // Global memory directory - const globalDir = path.join(GROUPS_DIR, 'global'); - if (fs.existsSync(globalDir)) { - mounts.push({ hostPath: globalDir, containerPath: '/workspace/global', readonly: !agentGroup.is_admin }); - } - - // Claude sessions directory (per agent group, shared across sessions) - const claudeDir = path.join(DATA_DIR, 'v2-sessions', agentGroup.id, '.claude-shared'); - fs.mkdirSync(claudeDir, { recursive: true }); - const settingsFile = path.join(claudeDir, 'settings.json'); - if (!fs.existsSync(settingsFile)) { - fs.writeFileSync( - settingsFile, - JSON.stringify( - { - env: { - CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1', - CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: '1', - CLAUDE_CODE_DISABLE_AUTO_MEMORY: '0', - }, - }, - null, - 2, - ) + '\n', - ); - } - - // Sync container skills - const skillsSrc = path.join(projectRoot, 'container', 'skills'); - const skillsDst = path.join(claudeDir, 'skills'); - if (fs.existsSync(skillsSrc)) { - for (const skillDir of fs.readdirSync(skillsSrc)) { - const srcDir = path.join(skillsSrc, skillDir); - if (fs.statSync(srcDir).isDirectory()) { - fs.cpSync(srcDir, path.join(skillsDst, skillDir), { recursive: true }); - } - } - } - mounts.push({ hostPath: claudeDir, containerPath: '/home/node/.claude', readonly: false }); - - // Agent-runner source (per agent group, recompiled on container startup) - const agentRunnerSrc = path.join(projectRoot, 'container', 'agent-runner', 'src'); - const groupRunnerDir = path.join(DATA_DIR, 'v2-sessions', agentGroup.id, 'agent-runner-src'); - if (fs.existsSync(agentRunnerSrc)) { - // Always copy — source files may have changed beyond just the index - fs.cpSync(agentRunnerSrc, groupRunnerDir, { recursive: true }); - } - mounts.push({ hostPath: groupRunnerDir, containerPath: '/app/src', readonly: false }); - - // Admin: mount project root read-only - if (agentGroup.is_admin) { - mounts.push({ hostPath: projectRoot, containerPath: '/workspace/project', readonly: true }); - const envFile = path.join(projectRoot, '.env'); - if (fs.existsSync(envFile)) { - mounts.push({ hostPath: '/dev/null', containerPath: '/workspace/project/.env', readonly: true }); - } - } - - // Additional mounts from container config - const containerConfig = agentGroup.container_config ? JSON.parse(agentGroup.container_config) : {}; - if (containerConfig.additionalMounts) { - const validated = validateAdditionalMounts( - containerConfig.additionalMounts, - agentGroup.name, - !!agentGroup.is_admin, - ); - mounts.push(...validated); - } - - return mounts; -} - -async function buildContainerArgs( - mounts: VolumeMount[], - containerName: string, - session: Session, - agentGroup: AgentGroup, - agentIdentifier?: string, -): Promise { - const args: string[] = ['run', '--rm', '--name', containerName]; - - // Environment - args.push('-e', `TZ=${TIMEZONE}`); - args.push('-e', `AGENT_PROVIDER=${session.agent_provider || agentGroup.agent_provider || 'claude'}`); - args.push('-e', `SESSION_DB_PATH=/workspace/session.db`); - - // Pass admin user ID and assistant name from messaging group/agent group - if (session.messaging_group_id) { - const mg = getMessagingGroup(session.messaging_group_id); - if (mg?.admin_user_id) { - args.push('-e', `NANOCLAW_ADMIN_USER_ID=${mg.admin_user_id}`); - } - } - if (agentGroup.name) { - args.push('-e', `NANOCLAW_ASSISTANT_NAME=${agentGroup.name}`); - } - - // OneCLI gateway - const onecliApplied = await onecli.applyContainerConfig(args, { addHostMapping: false, agent: agentIdentifier }); - if (onecliApplied) { - log.debug('OneCLI gateway applied', { containerName }); - } - - // Host gateway - args.push(...hostGatewayArgs()); - - // User mapping - const hostUid = process.getuid?.(); - const hostGid = process.getgid?.(); - if (hostUid != null && hostUid !== 0 && hostUid !== 1000) { - args.push('--user', `${hostUid}:${hostGid}`); - args.push('-e', 'HOME=/home/node'); - } - - // Volume mounts - for (const mount of mounts) { - if (mount.readonly) { - args.push(...readonlyMountArgs(mount.hostPath, mount.containerPath)); - } else { - args.push('-v', `${mount.hostPath}:${mount.containerPath}`); - } - } - - // Override entrypoint: compile agent-runner source, run v2 entry point (no stdin) - args.push('--entrypoint', 'bash'); - args.push(CONTAINER_IMAGE); - args.push( - '-c', - 'cd /app && npx tsc --outDir /tmp/dist 2>&1 >&2 && ln -sf /app/node_modules /tmp/dist/node_modules && node /tmp/dist/index-v2.js', - ); - - return args; -} diff --git a/src/container-runner.ts b/src/container-runner.ts index b04cc28..cdbfadc 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -1,150 +1,165 @@ /** - * Container Runner for NanoClaw - * Spawns agent execution in containers and handles IPC + * Container Runner v2 + * Spawns agent containers with session folder + agent group folder mounts. + * The container runs the v2 agent-runner which polls the session DB. */ import { ChildProcess, spawn } from 'child_process'; import fs from 'fs'; import path from 'path'; -import { - CONTAINER_IMAGE, - CONTAINER_MAX_OUTPUT_SIZE, - CONTAINER_TIMEOUT, - DATA_DIR, - GROUPS_DIR, - IDLE_TIMEOUT, - ONECLI_URL, - TIMEZONE, -} from './config.js'; -import { resolveGroupFolderPath, resolveGroupIpcPath } from './group-folder.js'; -import { logger } from './logger.js'; -import { CONTAINER_RUNTIME_BIN, hostGatewayArgs, readonlyMountArgs, stopContainer } from './container-runtime.js'; import { OneCLI } from '@onecli-sh/sdk'; + +import { CONTAINER_IMAGE, DATA_DIR, GROUPS_DIR, IDLE_TIMEOUT, ONECLI_URL, TIMEZONE } from './config.js'; +import { CONTAINER_RUNTIME_BIN, hostGatewayArgs, readonlyMountArgs, stopContainer } from './container-runtime.js'; +import { getAgentGroup } from './db/agent-groups.js'; +import { getMessagingGroup } from './db/messaging-groups.js'; +import { log } from './log.js'; import { validateAdditionalMounts } from './mount-security.js'; -import { RegisteredGroup } from './types.js'; +import { + markContainerIdle, + markContainerRunning, + markContainerStopped, + sessionDbPath, + sessionDir, +} from './session-manager.js'; +import type { AgentGroup, Session } from './types.js'; const onecli = new OneCLI({ url: ONECLI_URL }); -// Sentinel markers for robust output parsing (must match agent-runner) -const OUTPUT_START_MARKER = '---NANOCLAW_OUTPUT_START---'; -const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---'; - -export interface ContainerInput { - prompt: string; - sessionId?: string; - groupFolder: string; - chatJid: string; - isMain: boolean; - isScheduledTask?: boolean; - assistantName?: string; - script?: string; -} - -export interface ContainerOutput { - status: 'success' | 'error'; - result: string | null; - newSessionId?: string; - error?: string; -} - interface VolumeMount { hostPath: string; containerPath: string; readonly: boolean; } -function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount[] { - const mounts: VolumeMount[] = []; - const projectRoot = process.cwd(); - const groupDir = resolveGroupFolderPath(group.folder); +/** Active containers tracked by session ID. */ +const activeContainers = new Map(); - if (isMain) { - // Main gets the project root read-only. Writable paths the agent needs - // (store, group folder, IPC, .claude/) are mounted separately below. - // Read-only prevents the agent from modifying host application code - // (src/, dist/, package.json, etc.) which would bypass the sandbox - // entirely on next restart. - mounts.push({ - hostPath: projectRoot, - containerPath: '/workspace/project', - readonly: true, - }); +export function getActiveContainerCount(): number { + return activeContainers.size; +} - // Shadow .env so the agent cannot read secrets from the mounted project root. - // Credentials are injected by the OneCLI gateway, never exposed to containers. - const envFile = path.join(projectRoot, '.env'); - if (fs.existsSync(envFile)) { - mounts.push({ - hostPath: '/dev/null', - containerPath: '/workspace/project/.env', - readonly: true, - }); - } +export function isContainerRunning(sessionId: string): boolean { + return activeContainers.has(sessionId); +} - // Main gets writable access to the store (SQLite DB) so it can - // query and write to the database directly. - const storeDir = path.join(projectRoot, 'store'); - mounts.push({ - hostPath: storeDir, - containerPath: '/workspace/project/store', - readonly: false, - }); - - // Main also gets its group folder as the working directory - mounts.push({ - hostPath: groupDir, - containerPath: '/workspace/group', - readonly: false, - }); - - // Global memory directory — writable for main so it can update shared context - const globalDir = path.join(GROUPS_DIR, 'global'); - if (fs.existsSync(globalDir)) { - mounts.push({ - hostPath: globalDir, - containerPath: '/workspace/global', - readonly: false, - }); - } - } else { - // Other groups only get their own folder - mounts.push({ - hostPath: groupDir, - containerPath: '/workspace/group', - readonly: false, - }); - - // Global memory directory (read-only for non-main) - // Only directory mounts are supported, not file mounts - const globalDir = path.join(GROUPS_DIR, 'global'); - if (fs.existsSync(globalDir)) { - mounts.push({ - hostPath: globalDir, - containerPath: '/workspace/global', - readonly: true, - }); - } +/** + * Wake up a container for a session. If already running, no-op. + * The container runs the v2 agent-runner which polls the session DB. + */ +export async function wakeContainer(session: Session): Promise { + if (activeContainers.has(session.id)) { + log.debug('Container already running', { sessionId: session.id }); + return; } - // Per-group Claude sessions directory (isolated from other groups) - // Each group gets their own .claude/ to prevent cross-group session access - const groupSessionsDir = path.join(DATA_DIR, 'sessions', group.folder, '.claude'); - fs.mkdirSync(groupSessionsDir, { recursive: true }); - const settingsFile = path.join(groupSessionsDir, 'settings.json'); + const agentGroup = getAgentGroup(session.agent_group_id); + if (!agentGroup) { + log.error('Agent group not found', { agentGroupId: session.agent_group_id }); + return; + } + + const mounts = buildMounts(agentGroup, session); + const containerName = `nanoclaw-v2-${agentGroup.folder}-${Date.now()}`; + const agentIdentifier = agentGroup.is_admin ? undefined : agentGroup.folder.toLowerCase().replace(/_/g, '-'); + const args = await buildContainerArgs(mounts, containerName, session, agentGroup, agentIdentifier); + + log.info('Spawning container', { sessionId: session.id, agentGroup: agentGroup.name, containerName }); + + const container = spawn(CONTAINER_RUNTIME_BIN, args, { stdio: ['ignore', 'pipe', 'pipe'] }); + + activeContainers.set(session.id, { process: container, containerName }); + markContainerRunning(session.id); + + // Log stderr + container.stderr?.on('data', (data) => { + for (const line of data.toString().trim().split('\n')) { + if (line) log.debug(line, { container: agentGroup.folder }); + } + }); + + // stdout is unused in v2 (all IO is via session DB) + container.stdout?.on('data', () => {}); + + // Idle timeout: kill container after IDLE_TIMEOUT of no activity + let idleTimer = setTimeout(() => killContainer(session.id, 'idle timeout'), IDLE_TIMEOUT); + + const resetIdle = () => { + clearTimeout(idleTimer); + idleTimer = setTimeout(() => killContainer(session.id, 'idle timeout'), IDLE_TIMEOUT); + }; + + // Reset idle timer when the host detects new messages_out (called by delivery.ts) + const entry = activeContainers.get(session.id); + if (entry) { + (entry as { resetIdle?: () => void }).resetIdle = resetIdle; + } + + container.on('close', (code) => { + clearTimeout(idleTimer); + activeContainers.delete(session.id); + markContainerStopped(session.id); + log.info('Container exited', { sessionId: session.id, code, containerName }); + }); + + container.on('error', (err) => { + clearTimeout(idleTimer); + activeContainers.delete(session.id); + markContainerStopped(session.id); + log.error('Container spawn error', { sessionId: session.id, err }); + }); +} + +/** Reset the idle timer for a session's container (called when messages_out are delivered). */ +export function resetContainerIdleTimer(sessionId: string): void { + const entry = activeContainers.get(sessionId) as { resetIdle?: () => void } | undefined; + entry?.resetIdle?.(); +} + +/** Kill a container for a session. */ +export function killContainer(sessionId: string, reason: string): void { + const entry = activeContainers.get(sessionId); + if (!entry) return; + + log.info('Killing container', { sessionId, reason, containerName: entry.containerName }); + try { + stopContainer(entry.containerName); + } catch { + entry.process.kill('SIGKILL'); + } +} + +function buildMounts(agentGroup: AgentGroup, session: Session): VolumeMount[] { + const mounts: VolumeMount[] = []; + const projectRoot = process.cwd(); + const sessDir = sessionDir(agentGroup.id, session.id); + const groupDir = path.resolve(GROUPS_DIR, agentGroup.folder); + + // Session folder at /workspace (contains session.db, outbox/, .claude/) + mounts.push({ hostPath: sessDir, containerPath: '/workspace', readonly: false }); + + // Agent group folder at /workspace/agent + fs.mkdirSync(groupDir, { recursive: true }); + mounts.push({ hostPath: groupDir, containerPath: '/workspace/agent', readonly: false }); + + // Global memory directory + const globalDir = path.join(GROUPS_DIR, 'global'); + if (fs.existsSync(globalDir)) { + mounts.push({ hostPath: globalDir, containerPath: '/workspace/global', readonly: !agentGroup.is_admin }); + } + + // Claude sessions directory (per agent group, shared across sessions) + const claudeDir = path.join(DATA_DIR, 'v2-sessions', agentGroup.id, '.claude-shared'); + fs.mkdirSync(claudeDir, { recursive: true }); + const settingsFile = path.join(claudeDir, 'settings.json'); if (!fs.existsSync(settingsFile)) { fs.writeFileSync( settingsFile, JSON.stringify( { env: { - // Enable agent swarms (subagent orchestration) - // https://code.claude.com/docs/en/agent-teams#orchestrate-teams-of-claude-code-sessions CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1', - // Load CLAUDE.md from additional mounted directories - // https://code.claude.com/docs/en/memory#load-memory-from-additional-directories CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: '1', - // Enable Claude's memory feature (persists user preferences between sessions) - // https://code.claude.com/docs/en/memory#manage-auto-memory CLAUDE_CODE_DISABLE_AUTO_MEMORY: '0', }, }, @@ -154,61 +169,46 @@ function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount ); } - // Sync skills from container/skills/ into each group's .claude/skills/ - const skillsSrc = path.join(process.cwd(), 'container', 'skills'); - const skillsDst = path.join(groupSessionsDir, 'skills'); + // Sync container skills + const skillsSrc = path.join(projectRoot, 'container', 'skills'); + const skillsDst = path.join(claudeDir, 'skills'); if (fs.existsSync(skillsSrc)) { for (const skillDir of fs.readdirSync(skillsSrc)) { const srcDir = path.join(skillsSrc, skillDir); - if (!fs.statSync(srcDir).isDirectory()) continue; - const dstDir = path.join(skillsDst, skillDir); - fs.cpSync(srcDir, dstDir, { recursive: true }); + if (fs.statSync(srcDir).isDirectory()) { + fs.cpSync(srcDir, path.join(skillsDst, skillDir), { recursive: true }); + } } } - mounts.push({ - hostPath: groupSessionsDir, - containerPath: '/home/node/.claude', - readonly: false, - }); + mounts.push({ hostPath: claudeDir, containerPath: '/home/node/.claude', readonly: false }); - // Per-group IPC namespace: each group gets its own IPC directory - // This prevents cross-group privilege escalation via IPC - const groupIpcDir = resolveGroupIpcPath(group.folder); - fs.mkdirSync(path.join(groupIpcDir, 'messages'), { recursive: true }); - fs.mkdirSync(path.join(groupIpcDir, 'tasks'), { recursive: true }); - fs.mkdirSync(path.join(groupIpcDir, 'input'), { recursive: true }); - mounts.push({ - hostPath: groupIpcDir, - containerPath: '/workspace/ipc', - readonly: false, - }); - - // Copy agent-runner source into a per-group writable location so agents - // can customize it (add tools, change behavior) without affecting other - // groups. Recompiled on container startup via entrypoint.sh. + // Agent-runner source (per agent group, recompiled on container startup) const agentRunnerSrc = path.join(projectRoot, 'container', 'agent-runner', 'src'); - const groupAgentRunnerDir = path.join(DATA_DIR, 'sessions', group.folder, 'agent-runner-src'); + const groupRunnerDir = path.join(DATA_DIR, 'v2-sessions', agentGroup.id, 'agent-runner-src'); if (fs.existsSync(agentRunnerSrc)) { - const srcIndex = path.join(agentRunnerSrc, 'index.ts'); - const cachedIndex = path.join(groupAgentRunnerDir, 'index.ts'); - const needsCopy = - !fs.existsSync(groupAgentRunnerDir) || - !fs.existsSync(cachedIndex) || - (fs.existsSync(srcIndex) && fs.statSync(srcIndex).mtimeMs > fs.statSync(cachedIndex).mtimeMs); - if (needsCopy) { - fs.cpSync(agentRunnerSrc, groupAgentRunnerDir, { recursive: true }); + // Always copy — source files may have changed beyond just the index + fs.cpSync(agentRunnerSrc, groupRunnerDir, { recursive: true }); + } + mounts.push({ hostPath: groupRunnerDir, containerPath: '/app/src', readonly: false }); + + // Admin: mount project root read-only + if (agentGroup.is_admin) { + mounts.push({ hostPath: projectRoot, containerPath: '/workspace/project', readonly: true }); + const envFile = path.join(projectRoot, '.env'); + if (fs.existsSync(envFile)) { + mounts.push({ hostPath: '/dev/null', containerPath: '/workspace/project/.env', readonly: true }); } } - mounts.push({ - hostPath: groupAgentRunnerDir, - containerPath: '/app/src', - readonly: false, - }); - // Additional mounts validated against external allowlist (tamper-proof from containers) - if (group.containerConfig?.additionalMounts) { - const validatedMounts = validateAdditionalMounts(group.containerConfig.additionalMounts, group.name, isMain); - mounts.push(...validatedMounts); + // Additional mounts from container config + const containerConfig = agentGroup.container_config ? JSON.parse(agentGroup.container_config) : {}; + if (containerConfig.additionalMounts) { + const validated = validateAdditionalMounts( + containerConfig.additionalMounts, + agentGroup.name, + !!agentGroup.is_admin, + ); + mounts.push(...validated); } return mounts; @@ -217,31 +217,38 @@ function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount async function buildContainerArgs( mounts: VolumeMount[], containerName: string, + session: Session, + agentGroup: AgentGroup, agentIdentifier?: string, ): Promise { - const args: string[] = ['run', '-i', '--rm', '--name', containerName]; + const args: string[] = ['run', '--rm', '--name', containerName]; - // Pass host timezone so container's local time matches the user's + // Environment args.push('-e', `TZ=${TIMEZONE}`); + args.push('-e', `AGENT_PROVIDER=${session.agent_provider || agentGroup.agent_provider || 'claude'}`); + args.push('-e', `SESSION_DB_PATH=/workspace/session.db`); - // OneCLI gateway handles credential injection — containers never see real secrets. - // The gateway intercepts HTTPS traffic and injects API keys or OAuth tokens. - const onecliApplied = await onecli.applyContainerConfig(args, { - addHostMapping: false, // Nanoclaw already handles host gateway - agent: agentIdentifier, - }); - if (onecliApplied) { - logger.info({ containerName }, 'OneCLI gateway config applied'); - } else { - logger.warn({ containerName }, 'OneCLI gateway not reachable — container will have no credentials'); + // Pass admin user ID and assistant name from messaging group/agent group + if (session.messaging_group_id) { + const mg = getMessagingGroup(session.messaging_group_id); + if (mg?.admin_user_id) { + args.push('-e', `NANOCLAW_ADMIN_USER_ID=${mg.admin_user_id}`); + } + } + if (agentGroup.name) { + args.push('-e', `NANOCLAW_ASSISTANT_NAME=${agentGroup.name}`); } - // Runtime-specific args for host gateway resolution + // OneCLI gateway + const onecliApplied = await onecli.applyContainerConfig(args, { addHostMapping: false, agent: agentIdentifier }); + if (onecliApplied) { + log.debug('OneCLI gateway applied', { containerName }); + } + + // Host gateway args.push(...hostGatewayArgs()); - // Run as host user so bind-mounted files are accessible. - // Skip when running as root (uid 0), as the container's node user (uid 1000), - // or when getuid is unavailable (native Windows without WSL). + // User mapping const hostUid = process.getuid?.(); const hostGid = process.getgid?.(); if (hostUid != null && hostUid !== 0 && hostUid !== 1000) { @@ -249,6 +256,7 @@ async function buildContainerArgs( args.push('-e', 'HOME=/home/node'); } + // Volume mounts for (const mount of mounts) { if (mount.readonly) { args.push(...readonlyMountArgs(mount.hostPath, mount.containerPath)); @@ -257,421 +265,13 @@ async function buildContainerArgs( } } + // Override entrypoint: compile agent-runner source, run v2 entry point (no stdin) + args.push('--entrypoint', 'bash'); args.push(CONTAINER_IMAGE); + args.push( + '-c', + 'cd /app && npx tsc --outDir /tmp/dist 2>&1 >&2 && ln -sf /app/node_modules /tmp/dist/node_modules && node /tmp/dist/index.js', + ); return args; } - -export async function runContainerAgent( - group: RegisteredGroup, - input: ContainerInput, - onProcess: (proc: ChildProcess, containerName: string) => void, - onOutput?: (output: ContainerOutput) => Promise, -): Promise { - const startTime = Date.now(); - - const groupDir = resolveGroupFolderPath(group.folder); - fs.mkdirSync(groupDir, { recursive: true }); - - const mounts = buildVolumeMounts(group, input.isMain); - const safeName = group.folder.replace(/[^a-zA-Z0-9-]/g, '-'); - const containerName = `nanoclaw-${safeName}-${Date.now()}`; - // Main group uses the default OneCLI agent; others use their own agent. - const agentIdentifier = input.isMain ? undefined : group.folder.toLowerCase().replace(/_/g, '-'); - const containerArgs = await buildContainerArgs(mounts, containerName, agentIdentifier); - - logger.debug( - { - group: group.name, - containerName, - mounts: mounts.map((m) => `${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`), - containerArgs: containerArgs.join(' '), - }, - 'Container mount configuration', - ); - - logger.info( - { - group: group.name, - containerName, - mountCount: mounts.length, - isMain: input.isMain, - }, - 'Spawning container agent', - ); - - const logsDir = path.join(groupDir, 'logs'); - fs.mkdirSync(logsDir, { recursive: true }); - - return new Promise((resolve) => { - const container = spawn(CONTAINER_RUNTIME_BIN, containerArgs, { - stdio: ['pipe', 'pipe', 'pipe'], - }); - - onProcess(container, containerName); - - let stdout = ''; - let stderr = ''; - let stdoutTruncated = false; - let stderrTruncated = false; - - container.stdin.write(JSON.stringify(input)); - container.stdin.end(); - - // Streaming output: parse OUTPUT_START/END marker pairs as they arrive - let parseBuffer = ''; - let newSessionId: string | undefined; - let outputChain = Promise.resolve(); - - container.stdout.on('data', (data) => { - const chunk = data.toString(); - - // Always accumulate for logging - if (!stdoutTruncated) { - const remaining = CONTAINER_MAX_OUTPUT_SIZE - stdout.length; - if (chunk.length > remaining) { - stdout += chunk.slice(0, remaining); - stdoutTruncated = true; - logger.warn({ group: group.name, size: stdout.length }, 'Container stdout truncated due to size limit'); - } else { - stdout += chunk; - } - } - - // Stream-parse for output markers - if (onOutput) { - parseBuffer += chunk; - let startIdx: number; - while ((startIdx = parseBuffer.indexOf(OUTPUT_START_MARKER)) !== -1) { - const endIdx = parseBuffer.indexOf(OUTPUT_END_MARKER, startIdx); - if (endIdx === -1) break; // Incomplete pair, wait for more data - - const jsonStr = parseBuffer.slice(startIdx + OUTPUT_START_MARKER.length, endIdx).trim(); - parseBuffer = parseBuffer.slice(endIdx + OUTPUT_END_MARKER.length); - - try { - const parsed: ContainerOutput = JSON.parse(jsonStr); - if (parsed.newSessionId) { - newSessionId = parsed.newSessionId; - } - hadStreamingOutput = true; - // Activity detected — reset the hard timeout - resetTimeout(); - // Call onOutput for all markers (including null results) - // so idle timers start even for "silent" query completions. - outputChain = outputChain.then(() => onOutput(parsed)); - } catch (err) { - logger.warn({ group: group.name, error: err }, 'Failed to parse streamed output chunk'); - } - } - } - }); - - container.stderr.on('data', (data) => { - const chunk = data.toString(); - const lines = chunk.trim().split('\n'); - for (const line of lines) { - if (line) logger.debug({ container: group.folder }, line); - } - // Don't reset timeout on stderr — SDK writes debug logs continuously. - // Timeout only resets on actual output (OUTPUT_MARKER in stdout). - if (stderrTruncated) return; - const remaining = CONTAINER_MAX_OUTPUT_SIZE - stderr.length; - if (chunk.length > remaining) { - stderr += chunk.slice(0, remaining); - stderrTruncated = true; - logger.warn({ group: group.name, size: stderr.length }, 'Container stderr truncated due to size limit'); - } else { - stderr += chunk; - } - }); - - let timedOut = false; - let hadStreamingOutput = false; - const configTimeout = group.containerConfig?.timeout || CONTAINER_TIMEOUT; - // Grace period: hard timeout must be at least IDLE_TIMEOUT + 30s so the - // graceful _close sentinel has time to trigger before the hard kill fires. - const timeoutMs = Math.max(configTimeout, IDLE_TIMEOUT + 30_000); - - const killOnTimeout = () => { - timedOut = true; - logger.error({ group: group.name, containerName }, 'Container timeout, stopping gracefully'); - try { - stopContainer(containerName); - } catch (err) { - logger.warn({ group: group.name, containerName, err }, 'Graceful stop failed, force killing'); - container.kill('SIGKILL'); - } - }; - - let timeout = setTimeout(killOnTimeout, timeoutMs); - - // Reset the timeout whenever there's activity (streaming output) - const resetTimeout = () => { - clearTimeout(timeout); - timeout = setTimeout(killOnTimeout, timeoutMs); - }; - - container.on('close', (code) => { - clearTimeout(timeout); - const duration = Date.now() - startTime; - - if (timedOut) { - const ts = new Date().toISOString().replace(/[:.]/g, '-'); - const timeoutLog = path.join(logsDir, `container-${ts}.log`); - fs.writeFileSync( - timeoutLog, - [ - `=== Container Run Log (TIMEOUT) ===`, - `Timestamp: ${new Date().toISOString()}`, - `Group: ${group.name}`, - `Container: ${containerName}`, - `Duration: ${duration}ms`, - `Exit Code: ${code}`, - `Had Streaming Output: ${hadStreamingOutput}`, - ].join('\n'), - ); - - // Timeout after output = idle cleanup, not failure. - // The agent already sent its response; this is just the - // container being reaped after the idle period expired. - if (hadStreamingOutput) { - logger.info( - { group: group.name, containerName, duration, code }, - 'Container timed out after output (idle cleanup)', - ); - outputChain.then(() => { - resolve({ - status: 'success', - result: null, - newSessionId, - }); - }); - return; - } - - logger.error({ group: group.name, containerName, duration, code }, 'Container timed out with no output'); - - resolve({ - status: 'error', - result: null, - error: `Container timed out after ${configTimeout}ms`, - }); - return; - } - - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const logFile = path.join(logsDir, `container-${timestamp}.log`); - const isVerbose = process.env.LOG_LEVEL === 'debug' || process.env.LOG_LEVEL === 'trace'; - - const logLines = [ - `=== Container Run Log ===`, - `Timestamp: ${new Date().toISOString()}`, - `Group: ${group.name}`, - `IsMain: ${input.isMain}`, - `Duration: ${duration}ms`, - `Exit Code: ${code}`, - `Stdout Truncated: ${stdoutTruncated}`, - `Stderr Truncated: ${stderrTruncated}`, - ``, - ]; - - const isError = code !== 0; - - if (isVerbose || isError) { - // On error, log input metadata only — not the full prompt. - // Full input is only included at verbose level to avoid - // persisting user conversation content on every non-zero exit. - if (isVerbose) { - logLines.push(`=== Input ===`, JSON.stringify(input, null, 2), ``); - } else { - logLines.push( - `=== Input Summary ===`, - `Prompt length: ${input.prompt.length} chars`, - `Session ID: ${input.sessionId || 'new'}`, - ``, - ); - } - logLines.push( - `=== Container Args ===`, - containerArgs.join(' '), - ``, - `=== Mounts ===`, - mounts.map((m) => `${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`).join('\n'), - ``, - `=== Stderr${stderrTruncated ? ' (TRUNCATED)' : ''} ===`, - stderr, - ``, - `=== Stdout${stdoutTruncated ? ' (TRUNCATED)' : ''} ===`, - stdout, - ); - } else { - logLines.push( - `=== Input Summary ===`, - `Prompt length: ${input.prompt.length} chars`, - `Session ID: ${input.sessionId || 'new'}`, - ``, - `=== Mounts ===`, - mounts.map((m) => `${m.containerPath}${m.readonly ? ' (ro)' : ''}`).join('\n'), - ``, - ); - } - - fs.writeFileSync(logFile, logLines.join('\n')); - logger.debug({ logFile, verbose: isVerbose }, 'Container log written'); - - if (code !== 0) { - logger.error( - { - group: group.name, - code, - duration, - stderr, - stdout, - logFile, - }, - 'Container exited with error', - ); - - resolve({ - status: 'error', - result: null, - error: `Container exited with code ${code}: ${stderr.slice(-200)}`, - }); - return; - } - - // Streaming mode: wait for output chain to settle, return completion marker - if (onOutput) { - outputChain.then(() => { - logger.info({ group: group.name, duration, newSessionId }, 'Container completed (streaming mode)'); - resolve({ - status: 'success', - result: null, - newSessionId, - }); - }); - return; - } - - // Legacy mode: parse the last output marker pair from accumulated stdout - try { - // Extract JSON between sentinel markers for robust parsing - const startIdx = stdout.indexOf(OUTPUT_START_MARKER); - const endIdx = stdout.indexOf(OUTPUT_END_MARKER); - - let jsonLine: string; - if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) { - jsonLine = stdout.slice(startIdx + OUTPUT_START_MARKER.length, endIdx).trim(); - } else { - // Fallback: last non-empty line (backwards compatibility) - const lines = stdout.trim().split('\n'); - jsonLine = lines[lines.length - 1]; - } - - const output: ContainerOutput = JSON.parse(jsonLine); - - logger.info( - { - group: group.name, - duration, - status: output.status, - hasResult: !!output.result, - }, - 'Container completed', - ); - - resolve(output); - } catch (err) { - logger.error( - { - group: group.name, - stdout, - stderr, - error: err, - }, - 'Failed to parse container output', - ); - - resolve({ - status: 'error', - result: null, - error: `Failed to parse container output: ${err instanceof Error ? err.message : String(err)}`, - }); - } - }); - - container.on('error', (err) => { - clearTimeout(timeout); - logger.error({ group: group.name, containerName, error: err }, 'Container spawn error'); - resolve({ - status: 'error', - result: null, - error: `Container spawn error: ${err.message}`, - }); - }); - }); -} - -export function writeTasksSnapshot( - groupFolder: string, - isMain: boolean, - tasks: Array<{ - id: string; - groupFolder: string; - prompt: string; - script?: string | null; - schedule_type: string; - schedule_value: string; - status: string; - next_run: string | null; - }>, -): void { - // Write filtered tasks to the group's IPC directory - const groupIpcDir = resolveGroupIpcPath(groupFolder); - fs.mkdirSync(groupIpcDir, { recursive: true }); - - // Main sees all tasks, others only see their own - const filteredTasks = isMain ? tasks : tasks.filter((t) => t.groupFolder === groupFolder); - - const tasksFile = path.join(groupIpcDir, 'current_tasks.json'); - fs.writeFileSync(tasksFile, JSON.stringify(filteredTasks, null, 2)); -} - -export interface AvailableGroup { - jid: string; - name: string; - lastActivity: string; - isRegistered: boolean; -} - -/** - * Write available groups snapshot for the container to read. - * Only main group can see all available groups (for activation). - * Non-main groups only see their own registration status. - */ -export function writeGroupsSnapshot( - groupFolder: string, - isMain: boolean, - groups: AvailableGroup[], - _registeredJids: Set, -): void { - const groupIpcDir = resolveGroupIpcPath(groupFolder); - fs.mkdirSync(groupIpcDir, { recursive: true }); - - // Main sees all groups; others see nothing (they can't activate groups) - const visibleGroups = isMain ? groups : []; - - const groupsFile = path.join(groupIpcDir, 'available_groups.json'); - fs.writeFileSync( - groupsFile, - JSON.stringify( - { - groups: visibleGroups, - lastSync: new Date().toISOString(), - }, - null, - 2, - ), - ); -} diff --git a/src/container-runtime.test.ts b/src/container-runtime.test.ts index 94e14e9..80eb46e 100644 --- a/src/container-runtime.test.ts +++ b/src/container-runtime.test.ts @@ -1,12 +1,13 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -// Mock logger -vi.mock('./logger.js', () => ({ - logger: { +// Mock log +vi.mock('./log.js', () => ({ + log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), + fatal: vi.fn(), }, })); @@ -23,7 +24,7 @@ import { ensureContainerRuntimeRunning, cleanupOrphans, } from './container-runtime.js'; -import { logger } from './logger.js'; +import { log } from './log.js'; beforeEach(() => { vi.clearAllMocks(); @@ -67,7 +68,7 @@ describe('ensureContainerRuntimeRunning', () => { stdio: 'pipe', timeout: 10000, }); - expect(logger.debug).toHaveBeenCalledWith('Container runtime already running'); + expect(log.debug).toHaveBeenCalledWith('Container runtime already running'); }); it('throws when docker info fails', () => { @@ -76,7 +77,7 @@ describe('ensureContainerRuntimeRunning', () => { }); expect(() => ensureContainerRuntimeRunning()).toThrow('Container runtime is required but failed to start'); - expect(logger.error).toHaveBeenCalled(); + expect(log.error).toHaveBeenCalled(); }); }); @@ -99,9 +100,9 @@ describe('cleanupOrphans', () => { expect(mockExecSync).toHaveBeenNthCalledWith(3, `${CONTAINER_RUNTIME_BIN} stop -t 1 nanoclaw-group2-222`, { stdio: 'pipe', }); - expect(logger.info).toHaveBeenCalledWith( - { count: 2, names: ['nanoclaw-group1-111', 'nanoclaw-group2-222'] }, + expect(log.info).toHaveBeenCalledWith( 'Stopped orphaned containers', + { count: 2, names: ['nanoclaw-group1-111', 'nanoclaw-group2-222'] }, ); }); @@ -111,7 +112,7 @@ describe('cleanupOrphans', () => { cleanupOrphans(); expect(mockExecSync).toHaveBeenCalledTimes(1); - expect(logger.info).not.toHaveBeenCalled(); + expect(log.info).not.toHaveBeenCalled(); }); it('warns and continues when ps fails', () => { @@ -121,9 +122,9 @@ describe('cleanupOrphans', () => { cleanupOrphans(); // should not throw - expect(logger.warn).toHaveBeenCalledWith( - expect.objectContaining({ err: expect.any(Error) }), + expect(log.warn).toHaveBeenCalledWith( 'Failed to clean up orphaned containers', + expect.objectContaining({ err: expect.any(Error) }), ); }); @@ -139,9 +140,9 @@ describe('cleanupOrphans', () => { cleanupOrphans(); // should not throw expect(mockExecSync).toHaveBeenCalledTimes(3); - expect(logger.info).toHaveBeenCalledWith( - { count: 2, names: ['nanoclaw-a-1', 'nanoclaw-b-2'] }, + expect(log.info).toHaveBeenCalledWith( 'Stopped orphaned containers', + { count: 2, names: ['nanoclaw-a-1', 'nanoclaw-b-2'] }, ); }); }); diff --git a/src/container-runtime.ts b/src/container-runtime.ts index 678a708..5e68426 100644 --- a/src/container-runtime.ts +++ b/src/container-runtime.ts @@ -5,7 +5,7 @@ import { execSync } from 'child_process'; import os from 'os'; -import { logger } from './logger.js'; +import { log } from './log.js'; /** The container runtime binary name. */ export const CONTAINER_RUNTIME_BIN = 'docker'; @@ -39,9 +39,9 @@ export function ensureContainerRuntimeRunning(): void { stdio: 'pipe', timeout: 10000, }); - logger.debug('Container runtime already running'); + log.debug('Container runtime already running'); } catch (err) { - logger.error({ err }, 'Failed to reach container runtime'); + log.error('Failed to reach container runtime', { err }); console.error('\n╔════════════════════════════════════════════════════════════════╗'); console.error('║ FATAL: Container runtime failed to start ║'); console.error('║ ║'); @@ -72,9 +72,9 @@ export function cleanupOrphans(): void { } } if (orphans.length > 0) { - logger.info({ count: orphans.length, names: orphans }, 'Stopped orphaned containers'); + log.info('Stopped orphaned containers', { count: orphans.length, names: orphans }); } } catch (err) { - logger.warn({ err }, 'Failed to clean up orphaned containers'); + log.warn('Failed to clean up orphaned containers', { err }); } } diff --git a/src/db/agent-groups.ts b/src/db/agent-groups.ts index a306616..6b04e82 100644 --- a/src/db/agent-groups.ts +++ b/src/db/agent-groups.ts @@ -1,4 +1,4 @@ -import type { AgentGroup } from '../types-v2.js'; +import type { AgentGroup } from '../types.js'; import { getDb } from './connection.js'; export function createAgentGroup(group: AgentGroup): void { diff --git a/src/db/messaging-groups.ts b/src/db/messaging-groups.ts index ef3b46c..b7994fc 100644 --- a/src/db/messaging-groups.ts +++ b/src/db/messaging-groups.ts @@ -1,4 +1,4 @@ -import type { MessagingGroup, MessagingGroupAgent } from '../types-v2.js'; +import type { MessagingGroup, MessagingGroupAgent } from '../types.js'; import { getDb } from './connection.js'; // ── Messaging Groups ── diff --git a/src/db/sessions.ts b/src/db/sessions.ts index 57f00b9..c1c9ba5 100644 --- a/src/db/sessions.ts +++ b/src/db/sessions.ts @@ -1,4 +1,4 @@ -import type { PendingQuestion, Session } from '../types-v2.js'; +import type { PendingQuestion, Session } from '../types.js'; import { getDb } from './connection.js'; // ── Sessions ── diff --git a/src/delivery.ts b/src/delivery.ts index 8d1c268..4a020f8 100644 --- a/src/delivery.ts +++ b/src/delivery.ts @@ -10,9 +10,9 @@ import { getRunningSessions, getActiveSessions, createPendingQuestion } from './ import { getAgentGroup } from './db/agent-groups.js'; import { log } from './log.js'; import { openSessionDb, sessionDir } from './session-manager.js'; -import { resetContainerIdleTimer } from './container-runner-v2.js'; +import { resetContainerIdleTimer } from './container-runner.js'; import type { OutboundFile } from './channels/adapter.js'; -import type { Session } from './types-v2.js'; +import type { Session } from './types.js'; const ACTIVE_POLL_MS = 1000; const SWEEP_POLL_MS = 60_000; diff --git a/src/env.ts b/src/env.ts index 064e6f8..e04b4f4 100644 --- a/src/env.ts +++ b/src/env.ts @@ -1,6 +1,6 @@ import fs from 'fs'; import path from 'path'; -import { logger } from './logger.js'; +import { log } from './log.js'; /** * Parse the .env file and return values for the requested keys. @@ -14,7 +14,7 @@ export function readEnvFile(keys: string[]): Record { try { content = fs.readFileSync(envFile, 'utf-8'); } catch (err) { - logger.debug({ err }, '.env file not found, using defaults'); + log.debug('.env file not found, using defaults', { err }); return {}; } diff --git a/src/host-core.test.ts b/src/host-core.test.ts index 960e3a6..03ddd98 100644 --- a/src/host-core.test.ts +++ b/src/host-core.test.ts @@ -25,10 +25,10 @@ import { sessionsBaseDir, } from './session-manager.js'; import { getSession, findSession } from './db/sessions.js'; -import type { InboundEvent } from './router-v2.js'; +import type { InboundEvent } from './router.js'; // Mock container runner to prevent actual Docker spawning -vi.mock('./container-runner-v2.js', () => ({ +vi.mock('./container-runner.js', () => ({ wakeContainer: vi.fn().mockResolvedValue(undefined), resetContainerIdleTimer: vi.fn(), isContainerRunning: vi.fn().mockReturnValue(false), @@ -202,8 +202,8 @@ describe('router', () => { }); it('should route a message end-to-end', async () => { - const { routeInbound } = await import('./router-v2.js'); - const { wakeContainer } = await import('./container-runner-v2.js'); + const { routeInbound } = await import('./router.js'); + const { wakeContainer } = await import('./container-runner.js'); const event: InboundEvent = { channelType: 'discord', @@ -237,7 +237,7 @@ describe('router', () => { }); it('should auto-create messaging group for unknown platform', async () => { - const { routeInbound } = await import('./router-v2.js'); + const { routeInbound } = await import('./router.js'); // This platform ID isn't registered — but since there's no agent configured for it, // it should create the messaging group but not route (no agents configured) @@ -262,7 +262,7 @@ describe('router', () => { }); it('should route multiple messages to the same session', async () => { - const { routeInbound } = await import('./router-v2.js'); + const { routeInbound } = await import('./router.js'); await routeInbound({ channelType: 'discord', diff --git a/src/host-sweep.ts b/src/host-sweep.ts index d93d821..26a926f 100644 --- a/src/host-sweep.ts +++ b/src/host-sweep.ts @@ -13,8 +13,8 @@ import { getActiveSessions, updateSession } from './db/sessions.js'; import { getAgentGroup } from './db/agent-groups.js'; import { log } from './log.js'; import { openSessionDb, sessionDbPath } from './session-manager.js'; -import { wakeContainer, isContainerRunning } from './container-runner-v2.js'; -import type { Session } from './types-v2.js'; +import { wakeContainer, isContainerRunning } from './container-runner.js'; +import type { Session } from './types.js'; const SWEEP_INTERVAL_MS = 60_000; const STALE_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes diff --git a/src/index-v2.ts b/src/index-v2.ts deleted file mode 100644 index a72540b..0000000 --- a/src/index-v2.ts +++ /dev/null @@ -1,180 +0,0 @@ -/** - * NanoClaw v2 — main entry point. - * - * Thin orchestrator: init DB, run migrations, start channel adapters, - * start delivery polls, start sweep, handle shutdown. - */ -import path from 'path'; - -import { DATA_DIR } from './config.js'; -import { initDb } from './db/connection.js'; -import { runMigrations } from './db/migrations/index.js'; -import { getMessagingGroupsByChannel, getMessagingGroupAgents } from './db/messaging-groups.js'; -import { ensureContainerRuntimeRunning, cleanupOrphans } from './container-runtime.js'; -import { startActiveDeliveryPoll, startSweepDeliveryPoll, setDeliveryAdapter, stopDeliveryPolls } from './delivery.js'; -import { startHostSweep, stopHostSweep } from './host-sweep.js'; -import { routeInbound } from './router-v2.js'; -import { getPendingQuestion, deletePendingQuestion, getSession } from './db/sessions.js'; -import { writeSessionMessage } from './session-manager.js'; -import { wakeContainer } from './container-runner-v2.js'; -import { log } from './log.js'; - -// Channel imports — each triggers self-registration -import './channels/discord-v2.js'; - -import type { ChannelAdapter, ChannelSetup, ConversationConfig } from './channels/adapter.js'; -import { initChannelAdapters, teardownChannelAdapters, getChannelAdapter } from './channels/channel-registry.js'; - -async function main(): Promise { - log.info('NanoClaw v2 starting'); - - // 1. Init central DB - const dbPath = path.join(DATA_DIR, 'v2.db'); - const db = initDb(dbPath); - runMigrations(db); - log.info('Central DB ready', { path: dbPath }); - - // 2. Container runtime - ensureContainerRuntimeRunning(); - cleanupOrphans(); - - // 3. Channel adapters - await initChannelAdapters((adapter: ChannelAdapter): ChannelSetup => { - const conversations = buildConversationConfigs(adapter.channelType); - return { - conversations, - onInbound(platformId, threadId, message) { - routeInbound({ - channelType: adapter.channelType, - platformId, - threadId, - message: { - id: message.id, - kind: message.kind, - content: JSON.stringify(message.content), - timestamp: message.timestamp, - }, - }).catch((err) => { - log.error('Failed to route inbound message', { channelType: adapter.channelType, err }); - }); - }, - onMetadata(platformId, name, isGroup) { - log.info('Channel metadata discovered', { - channelType: adapter.channelType, - platformId, - name, - isGroup, - }); - }, - onAction(questionId, selectedOption, userId) { - handleQuestionResponse(questionId, selectedOption, userId).catch((err) => { - log.error('Failed to handle question response', { questionId, err }); - }); - }, - }; - }); - - // 4. Delivery adapter bridge — dispatches to channel adapters - setDeliveryAdapter({ - async deliver(channelType, platformId, threadId, kind, content, files) { - const adapter = getChannelAdapter(channelType); - if (!adapter) { - log.warn('No adapter for channel type', { channelType }); - return; - } - await adapter.deliver(platformId, threadId, { kind, content: JSON.parse(content), files }); - }, - async setTyping(channelType, platformId, threadId) { - const adapter = getChannelAdapter(channelType); - await adapter?.setTyping?.(platformId, threadId); - }, - }); - - // 5. Start delivery polls - startActiveDeliveryPoll(); - startSweepDeliveryPoll(); - log.info('Delivery polls started'); - - // 6. Start host sweep - startHostSweep(); - log.info('Host sweep started'); - - log.info('NanoClaw v2 running'); -} - -/** Build ConversationConfig[] for a channel type from the central DB. */ -function buildConversationConfigs(channelType: string): ConversationConfig[] { - const groups = getMessagingGroupsByChannel(channelType); - const configs: ConversationConfig[] = []; - - for (const mg of groups) { - const agents = getMessagingGroupAgents(mg.id); - for (const agent of agents) { - const triggerRules = agent.trigger_rules ? JSON.parse(agent.trigger_rules) : null; - configs.push({ - platformId: mg.platform_id, - agentGroupId: agent.agent_group_id, - triggerPattern: triggerRules?.pattern, - requiresTrigger: triggerRules?.requiresTrigger ?? false, - sessionMode: agent.session_mode, - }); - } - } - - return configs; -} - -/** Handle a user's response to an ask_user_question card. */ -async function handleQuestionResponse(questionId: string, selectedOption: string, userId: string): Promise { - const pq = getPendingQuestion(questionId); - if (!pq) { - log.warn('Pending question not found (may have expired)', { questionId }); - return; - } - - const session = getSession(pq.session_id); - if (!session) { - log.warn('Session not found for pending question', { questionId, sessionId: pq.session_id }); - deletePendingQuestion(questionId); - return; - } - - // Write the response to the session DB as a system message - writeSessionMessage(session.agent_group_id, session.id, { - id: `qr-${questionId}-${Date.now()}`, - kind: 'system', - timestamp: new Date().toISOString(), - platformId: pq.platform_id, - channelType: pq.channel_type, - threadId: pq.thread_id, - content: JSON.stringify({ - type: 'question_response', - questionId, - selectedOption, - userId, - }), - }); - - deletePendingQuestion(questionId); - log.info('Question response routed', { questionId, selectedOption, sessionId: session.id }); - - // Wake the container so the MCP tool's poll picks up the response - await wakeContainer(session); -} - -/** Graceful shutdown. */ -async function shutdown(signal: string): Promise { - log.info('Shutdown signal received', { signal }); - stopDeliveryPolls(); - stopHostSweep(); - await teardownChannelAdapters(); - process.exit(0); -} - -process.on('SIGTERM', () => shutdown('SIGTERM')); -process.on('SIGINT', () => shutdown('SIGINT')); - -main().catch((err) => { - log.fatal('Startup failed', { err }); - process.exit(1); -}); diff --git a/src/index.ts b/src/index.ts index ded6b94..03bc093 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,647 +1,180 @@ -import fs from 'fs'; +/** + * NanoClaw v2 — main entry point. + * + * Thin orchestrator: init DB, run migrations, start channel adapters, + * start delivery polls, start sweep, handle shutdown. + */ import path from 'path'; -import { OneCLI } from '@onecli-sh/sdk'; +import { DATA_DIR } from './config.js'; +import { initDb } from './db/connection.js'; +import { runMigrations } from './db/migrations/index.js'; +import { getMessagingGroupsByChannel, getMessagingGroupAgents } from './db/messaging-groups.js'; +import { ensureContainerRuntimeRunning, cleanupOrphans } from './container-runtime.js'; +import { startActiveDeliveryPoll, startSweepDeliveryPoll, setDeliveryAdapter, stopDeliveryPolls } from './delivery.js'; +import { startHostSweep, stopHostSweep } from './host-sweep.js'; +import { routeInbound } from './router.js'; +import { getPendingQuestion, deletePendingQuestion, getSession } from './db/sessions.js'; +import { writeSessionMessage } from './session-manager.js'; +import { wakeContainer } from './container-runner.js'; +import { log } from './log.js'; -import { - ASSISTANT_NAME, - DEFAULT_TRIGGER, - getTriggerPattern, - GROUPS_DIR, - IDLE_TIMEOUT, - MAX_MESSAGES_PER_PROMPT, - ONECLI_URL, - POLL_INTERVAL, - TIMEZONE, -} from './config.js'; -import './channels/index.js'; -import { getChannelFactory, getRegisteredChannelNames } from './channels/registry.js'; -import { ContainerOutput, runContainerAgent, writeGroupsSnapshot, writeTasksSnapshot } from './container-runner.js'; -import { cleanupOrphans, ensureContainerRuntimeRunning } from './container-runtime.js'; -import { - getAllChats, - getAllRegisteredGroups, - getAllSessions, - deleteSession, - getAllTasks, - getLastBotMessageTimestamp, - getMessagesSince, - getNewMessages, - getRouterState, - initDatabase, - setRegisteredGroup, - setRouterState, - setSession, - storeChatMetadata, - storeMessage, -} from './db.js'; -import { GroupQueue } from './group-queue.js'; -import { resolveGroupFolderPath } from './group-folder.js'; -import { startIpcWatcher } from './ipc.js'; -import { findChannel, formatMessages, formatOutbound } from './router.js'; -import { restoreRemoteControl, startRemoteControl, stopRemoteControl } from './remote-control.js'; -import { isSenderAllowed, isTriggerAllowed, loadSenderAllowlist, shouldDropMessage } from './sender-allowlist.js'; -import { startSessionCleanup } from './session-cleanup.js'; -import { startSchedulerLoop } from './task-scheduler.js'; -import { Channel, NewMessage, RegisteredGroup } from './types.js'; -import { logger } from './logger.js'; +// Channel imports — each triggers self-registration +import './channels/discord.js'; -// Re-export for backwards compatibility during refactor -export { escapeXml, formatMessages } from './router.js'; - -let lastTimestamp = ''; -let sessions: Record = {}; -let registeredGroups: Record = {}; -let lastAgentTimestamp: Record = {}; -let messageLoopRunning = false; - -const channels: Channel[] = []; -const queue = new GroupQueue(); - -const onecli = new OneCLI({ url: ONECLI_URL }); - -function ensureOneCLIAgent(jid: string, group: RegisteredGroup): void { - if (group.isMain) return; - const identifier = group.folder.toLowerCase().replace(/_/g, '-'); - onecli.ensureAgent({ name: group.name, identifier }).then( - (res) => { - logger.info({ jid, identifier, created: res.created }, 'OneCLI agent ensured'); - }, - (err) => { - logger.debug({ jid, identifier, err: String(err) }, 'OneCLI agent ensure skipped'); - }, - ); -} - -function loadState(): void { - lastTimestamp = getRouterState('last_timestamp') || ''; - const agentTs = getRouterState('last_agent_timestamp'); - try { - lastAgentTimestamp = agentTs ? JSON.parse(agentTs) : {}; - } catch { - logger.warn('Corrupted last_agent_timestamp in DB, resetting'); - lastAgentTimestamp = {}; - } - sessions = getAllSessions(); - registeredGroups = getAllRegisteredGroups(); - logger.info({ groupCount: Object.keys(registeredGroups).length }, 'State loaded'); -} - -/** - * Return the message cursor for a group, recovering from the last bot reply - * if lastAgentTimestamp is missing (new group, corrupted state, restart). - */ -function getOrRecoverCursor(chatJid: string): string { - const existing = lastAgentTimestamp[chatJid]; - if (existing) return existing; - - const botTs = getLastBotMessageTimestamp(chatJid, ASSISTANT_NAME); - if (botTs) { - logger.info({ chatJid, recoveredFrom: botTs }, 'Recovered message cursor from last bot reply'); - lastAgentTimestamp[chatJid] = botTs; - saveState(); - return botTs; - } - return ''; -} - -function saveState(): void { - setRouterState('last_timestamp', lastTimestamp); - setRouterState('last_agent_timestamp', JSON.stringify(lastAgentTimestamp)); -} - -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; - setRegisteredGroup(jid, group); - - // Create group folder - fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true }); - - // Copy CLAUDE.md template into the new group folder so agents have - // identity and instructions from the first run. (Fixes #1391) - const groupMdFile = path.join(groupDir, 'CLAUDE.md'); - if (!fs.existsSync(groupMdFile)) { - const templateFile = path.join(GROUPS_DIR, group.isMain ? 'main' : 'global', 'CLAUDE.md'); - if (fs.existsSync(templateFile)) { - let content = fs.readFileSync(templateFile, 'utf-8'); - if (ASSISTANT_NAME !== 'Andy') { - content = content.replace(/^# Andy$/m, `# ${ASSISTANT_NAME}`); - content = content.replace(/You are Andy/g, `You are ${ASSISTANT_NAME}`); - } - fs.writeFileSync(groupMdFile, content); - logger.info({ folder: group.folder }, 'Created CLAUDE.md from template'); - } - } - - // Ensure a corresponding OneCLI agent exists (best-effort, non-blocking) - ensureOneCLIAgent(jid, group); - - logger.info({ jid, name: group.name, folder: group.folder }, 'Group registered'); -} - -/** - * Get available groups list for the agent. - * Returns groups ordered by most recent activity. - */ -export function getAvailableGroups(): import('./container-runner.js').AvailableGroup[] { - const chats = getAllChats(); - const registeredJids = new Set(Object.keys(registeredGroups)); - - return chats - .filter((c) => c.jid !== '__group_sync__' && c.is_group) - .map((c) => ({ - jid: c.jid, - name: c.name, - lastActivity: c.last_message_time, - isRegistered: registeredJids.has(c.jid), - })); -} - -/** @internal - exported for testing */ -export function _setRegisteredGroups(groups: Record): void { - registeredGroups = groups; -} - -/** - * Process all pending messages for a group. - * Called by the GroupQueue when it's this group's turn. - */ -async function processGroupMessages(chatJid: string): Promise { - const group = registeredGroups[chatJid]; - if (!group) return true; - - const channel = findChannel(channels, chatJid); - if (!channel) { - logger.warn({ chatJid }, 'No channel owns JID, skipping messages'); - return true; - } - - const isMainGroup = group.isMain === true; - - const missedMessages = getMessagesSince( - chatJid, - getOrRecoverCursor(chatJid), - ASSISTANT_NAME, - MAX_MESSAGES_PER_PROMPT, - ); - - if (missedMessages.length === 0) return true; - - // For non-main groups, check if trigger is required and present - if (!isMainGroup && group.requiresTrigger !== false) { - const triggerPattern = getTriggerPattern(group.trigger); - const allowlistCfg = loadSenderAllowlist(); - const hasTrigger = missedMessages.some( - (m) => - triggerPattern.test(m.content.trim()) && (m.is_from_me || isTriggerAllowed(chatJid, m.sender, allowlistCfg)), - ); - if (!hasTrigger) return true; - } - - const prompt = formatMessages(missedMessages, TIMEZONE); - - // Advance cursor so the piping path in startMessageLoop won't re-fetch - // these messages. Save the old cursor so we can roll back on error. - const previousCursor = lastAgentTimestamp[chatJid] || ''; - lastAgentTimestamp[chatJid] = missedMessages[missedMessages.length - 1].timestamp; - saveState(); - - logger.info({ group: group.name, messageCount: missedMessages.length }, 'Processing messages'); - - // Track idle timer for closing stdin when agent is idle - let idleTimer: ReturnType | null = null; - - const resetIdleTimer = () => { - if (idleTimer) clearTimeout(idleTimer); - idleTimer = setTimeout(() => { - logger.debug({ group: group.name }, 'Idle timeout, closing container stdin'); - queue.closeStdin(chatJid); - }, IDLE_TIMEOUT); - }; - - await channel.setTyping?.(chatJid, true); - let hadError = false; - let outputSentToUser = false; - - const output = await runAgent(group, prompt, chatJid, async (result) => { - // Streaming output callback — called for each agent result - if (result.result) { - const raw = typeof result.result === 'string' ? result.result : JSON.stringify(result.result); - // Strip ... blocks — agent uses these for internal reasoning - const text = raw.replace(/[\s\S]*?<\/internal>/g, '').trim(); - logger.info({ group: group.name }, `Agent output: ${raw.length} chars`); - if (text) { - await channel.sendMessage(chatJid, text); - outputSentToUser = true; - } - // Only reset idle timer on actual results, not session-update markers (result: null) - resetIdleTimer(); - } - - if (result.status === 'success') { - queue.notifyIdle(chatJid); - } - - if (result.status === 'error') { - hadError = true; - } - }); - - await channel.setTyping?.(chatJid, false); - if (idleTimer) clearTimeout(idleTimer); - - if (output === 'error' || hadError) { - // If we already sent output to the user, don't roll back the cursor — - // the user got their response and re-processing would send duplicates. - if (outputSentToUser) { - logger.warn( - { group: group.name }, - 'Agent error after output was sent, skipping cursor rollback to prevent duplicates', - ); - return true; - } - // Roll back cursor so retries can re-process these messages - lastAgentTimestamp[chatJid] = previousCursor; - saveState(); - logger.warn({ group: group.name }, 'Agent error, rolled back message cursor for retry'); - return false; - } - - return true; -} - -async function runAgent( - group: RegisteredGroup, - prompt: string, - chatJid: string, - onOutput?: (output: ContainerOutput) => Promise, -): Promise<'success' | 'error'> { - const isMain = group.isMain === true; - const sessionId = sessions[group.folder]; - - // Update tasks snapshot for container to read (filtered by group) - const tasks = getAllTasks(); - writeTasksSnapshot( - group.folder, - isMain, - tasks.map((t) => ({ - id: t.id, - groupFolder: t.group_folder, - prompt: t.prompt, - script: t.script || undefined, - schedule_type: t.schedule_type, - schedule_value: t.schedule_value, - status: t.status, - next_run: t.next_run, - })), - ); - - // Update available groups snapshot (main group only can see all groups) - const availableGroups = getAvailableGroups(); - writeGroupsSnapshot(group.folder, isMain, availableGroups, new Set(Object.keys(registeredGroups))); - - // Wrap onOutput to track session ID from streamed results - const wrappedOnOutput = onOutput - ? async (output: ContainerOutput) => { - if (output.newSessionId) { - sessions[group.folder] = output.newSessionId; - setSession(group.folder, output.newSessionId); - } - await onOutput(output); - } - : undefined; - - try { - const output = await runContainerAgent( - group, - { - prompt, - sessionId, - groupFolder: group.folder, - chatJid, - isMain, - assistantName: ASSISTANT_NAME, - }, - (proc, containerName) => queue.registerProcess(chatJid, proc, containerName, group.folder), - wrappedOnOutput, - ); - - if (output.newSessionId) { - sessions[group.folder] = output.newSessionId; - setSession(group.folder, output.newSessionId); - } - - if (output.status === 'error') { - // Detect stale/corrupt session — clear it so the next retry starts fresh. - // The session .jsonl can go missing after a crash mid-write, manual - // deletion, or disk-full. The existing backoff in group-queue.ts - // handles the retry; we just need to remove the broken session ID. - const isStaleSession = - sessionId && output.error && /no conversation found|ENOENT.*\.jsonl|session.*not found/i.test(output.error); - - if (isStaleSession) { - logger.warn( - { group: group.name, staleSessionId: sessionId, error: output.error }, - 'Stale session detected — clearing for next retry', - ); - delete sessions[group.folder]; - deleteSession(group.folder); - } - - logger.error({ group: group.name, error: output.error }, 'Container agent error'); - return 'error'; - } - - return 'success'; - } catch (err) { - logger.error({ group: group.name, err }, 'Agent error'); - return 'error'; - } -} - -async function startMessageLoop(): Promise { - if (messageLoopRunning) { - logger.debug('Message loop already running, skipping duplicate start'); - return; - } - messageLoopRunning = true; - - logger.info(`NanoClaw running (default trigger: ${DEFAULT_TRIGGER})`); - - while (true) { - try { - const jids = Object.keys(registeredGroups); - const { messages, newTimestamp } = getNewMessages(jids, lastTimestamp, ASSISTANT_NAME); - - if (messages.length > 0) { - logger.info({ count: messages.length }, 'New messages'); - - // Advance the "seen" cursor for all messages immediately - lastTimestamp = newTimestamp; - saveState(); - - // Deduplicate by group - const messagesByGroup = new Map(); - for (const msg of messages) { - const existing = messagesByGroup.get(msg.chat_jid); - if (existing) { - existing.push(msg); - } else { - messagesByGroup.set(msg.chat_jid, [msg]); - } - } - - for (const [chatJid, groupMessages] of messagesByGroup) { - const group = registeredGroups[chatJid]; - if (!group) continue; - - const channel = findChannel(channels, chatJid); - if (!channel) { - logger.warn({ chatJid }, 'No channel owns JID, skipping messages'); - continue; - } - - const isMainGroup = group.isMain === true; - const needsTrigger = !isMainGroup && group.requiresTrigger !== false; - - // For non-main groups, only act on trigger messages. - // Non-trigger messages accumulate in DB and get pulled as - // context when a trigger eventually arrives. - if (needsTrigger) { - const triggerPattern = getTriggerPattern(group.trigger); - const allowlistCfg = loadSenderAllowlist(); - const hasTrigger = groupMessages.some( - (m) => - triggerPattern.test(m.content.trim()) && - (m.is_from_me || isTriggerAllowed(chatJid, m.sender, allowlistCfg)), - ); - if (!hasTrigger) continue; - } - - // Pull all messages since lastAgentTimestamp so non-trigger - // context that accumulated between triggers is included. - const allPending = getMessagesSince( - chatJid, - getOrRecoverCursor(chatJid), - ASSISTANT_NAME, - MAX_MESSAGES_PER_PROMPT, - ); - const messagesToSend = allPending.length > 0 ? allPending : groupMessages; - const formatted = formatMessages(messagesToSend, TIMEZONE); - - if (queue.sendMessage(chatJid, formatted)) { - logger.debug({ chatJid, count: messagesToSend.length }, 'Piped messages to active container'); - lastAgentTimestamp[chatJid] = messagesToSend[messagesToSend.length - 1].timestamp; - saveState(); - // Show typing indicator while the container processes the piped message - channel - .setTyping?.(chatJid, true) - ?.catch((err) => logger.warn({ chatJid, err }, 'Failed to set typing indicator')); - } else { - // No active container — enqueue for a new one - queue.enqueueMessageCheck(chatJid); - } - } - } - } catch (err) { - logger.error({ err }, 'Error in message loop'); - } - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL)); - } -} - -/** - * Startup recovery: check for unprocessed messages in registered groups. - * Handles crash between advancing lastTimestamp and processing messages. - */ -function recoverPendingMessages(): void { - for (const [chatJid, group] of Object.entries(registeredGroups)) { - const pending = getMessagesSince(chatJid, getOrRecoverCursor(chatJid), ASSISTANT_NAME, MAX_MESSAGES_PER_PROMPT); - if (pending.length > 0) { - logger.info({ group: group.name, pendingCount: pending.length }, 'Recovery: found unprocessed messages'); - queue.enqueueMessageCheck(chatJid); - } - } -} - -function ensureContainerSystemRunning(): void { - ensureContainerRuntimeRunning(); - cleanupOrphans(); -} +import type { ChannelAdapter, ChannelSetup, ConversationConfig } from './channels/adapter.js'; +import { initChannelAdapters, teardownChannelAdapters, getChannelAdapter } from './channels/channel-registry.js'; async function main(): Promise { - ensureContainerSystemRunning(); - initDatabase(); - logger.info('Database initialized'); - loadState(); + log.info('NanoClaw v2 starting'); - // Ensure OneCLI agents exist for all registered groups. - // Recovers from missed creates (e.g. OneCLI was down at registration time). - for (const [jid, group] of Object.entries(registeredGroups)) { - ensureOneCLIAgent(jid, group); - } + // 1. Init central DB + const dbPath = path.join(DATA_DIR, 'v2.db'); + const db = initDb(dbPath); + runMigrations(db); + log.info('Central DB ready', { path: dbPath }); - restoreRemoteControl(); + // 2. Container runtime + ensureContainerRuntimeRunning(); + cleanupOrphans(); - // Graceful shutdown handlers - const shutdown = async (signal: string) => { - logger.info({ signal }, 'Shutdown signal received'); - await queue.shutdown(10000); - for (const ch of channels) await ch.disconnect(); - process.exit(0); - }; - process.on('SIGTERM', () => shutdown('SIGTERM')); - process.on('SIGINT', () => shutdown('SIGINT')); + // 3. Channel adapters + await initChannelAdapters((adapter: ChannelAdapter): ChannelSetup => { + const conversations = buildConversationConfigs(adapter.channelType); + return { + conversations, + onInbound(platformId, threadId, message) { + routeInbound({ + channelType: adapter.channelType, + platformId, + threadId, + message: { + id: message.id, + kind: message.kind, + content: JSON.stringify(message.content), + timestamp: message.timestamp, + }, + }).catch((err) => { + log.error('Failed to route inbound message', { channelType: adapter.channelType, err }); + }); + }, + onMetadata(platformId, name, isGroup) { + log.info('Channel metadata discovered', { + channelType: adapter.channelType, + platformId, + name, + isGroup, + }); + }, + onAction(questionId, selectedOption, userId) { + handleQuestionResponse(questionId, selectedOption, userId).catch((err) => { + log.error('Failed to handle question response', { questionId, err }); + }); + }, + }; + }); - // Handle /remote-control and /remote-control-end commands - async function handleRemoteControl(command: string, chatJid: string, msg: NewMessage): Promise { - const group = registeredGroups[chatJid]; - if (!group?.isMain) { - logger.warn({ chatJid, sender: msg.sender }, 'Remote control rejected: not main group'); - return; - } - - const channel = findChannel(channels, chatJid); - if (!channel) return; - - if (command === '/remote-control') { - const result = await startRemoteControl(msg.sender, chatJid, process.cwd()); - if (result.ok) { - await channel.sendMessage(chatJid, result.url); - } else { - await channel.sendMessage(chatJid, `Remote Control failed: ${result.error}`); - } - } else { - const result = stopRemoteControl(); - if (result.ok) { - await channel.sendMessage(chatJid, 'Remote Control session ended.'); - } else { - await channel.sendMessage(chatJid, result.error); - } - } - } - - // Channel callbacks (shared by all channels) - const channelOpts = { - onMessage: (chatJid: string, msg: NewMessage) => { - // Remote control commands — intercept before storage - const trimmed = msg.content.trim(); - if (trimmed === '/remote-control' || trimmed === '/remote-control-end') { - handleRemoteControl(trimmed, chatJid, msg).catch((err) => - logger.error({ err, chatJid }, 'Remote control command error'), - ); + // 4. Delivery adapter bridge — dispatches to channel adapters + setDeliveryAdapter({ + async deliver(channelType, platformId, threadId, kind, content, files) { + const adapter = getChannelAdapter(channelType); + if (!adapter) { + log.warn('No adapter for channel type', { channelType }); return; } - - // Sender allowlist drop mode: discard messages from denied senders before storing - if (!msg.is_from_me && !msg.is_bot_message && registeredGroups[chatJid]) { - const cfg = loadSenderAllowlist(); - if (shouldDropMessage(chatJid, cfg) && !isSenderAllowed(chatJid, msg.sender, cfg)) { - if (cfg.logDenied) { - logger.debug({ chatJid, sender: msg.sender }, 'sender-allowlist: dropping message (drop mode)'); - } - return; - } - } - storeMessage(msg); + await adapter.deliver(platformId, threadId, { kind, content: JSON.parse(content), files }); }, - onChatMetadata: (chatJid: string, timestamp: string, name?: string, channel?: string, isGroup?: boolean) => - storeChatMetadata(chatJid, timestamp, name, channel, isGroup), - registeredGroups: () => registeredGroups, - }; + async setTyping(channelType, platformId, threadId) { + const adapter = getChannelAdapter(channelType); + await adapter?.setTyping?.(platformId, threadId); + }, + }); - // Create and connect all registered channels. - // Each channel self-registers via the barrel import above. - // Factories return null when credentials are missing, so unconfigured channels are skipped. - for (const channelName of getRegisteredChannelNames()) { - const factory = getChannelFactory(channelName)!; - const channel = factory(channelOpts); - if (!channel) { - logger.warn( - { channel: channelName }, - 'Channel installed but credentials missing — skipping. Check .env or re-run the channel skill.', - ); - continue; + // 5. Start delivery polls + startActiveDeliveryPoll(); + startSweepDeliveryPoll(); + log.info('Delivery polls started'); + + // 6. Start host sweep + startHostSweep(); + log.info('Host sweep started'); + + log.info('NanoClaw v2 running'); +} + +/** Build ConversationConfig[] for a channel type from the central DB. */ +function buildConversationConfigs(channelType: string): ConversationConfig[] { + const groups = getMessagingGroupsByChannel(channelType); + const configs: ConversationConfig[] = []; + + for (const mg of groups) { + const agents = getMessagingGroupAgents(mg.id); + for (const agent of agents) { + const triggerRules = agent.trigger_rules ? JSON.parse(agent.trigger_rules) : null; + configs.push({ + platformId: mg.platform_id, + agentGroupId: agent.agent_group_id, + triggerPattern: triggerRules?.pattern, + requiresTrigger: triggerRules?.requiresTrigger ?? false, + sessionMode: agent.session_mode, + }); } - channels.push(channel); - await channel.connect(); - } - if (channels.length === 0) { - logger.fatal('No channels connected'); - process.exit(1); } - // Start subsystems (independently of connection handler) - startSchedulerLoop({ - registeredGroups: () => registeredGroups, - getSessions: () => sessions, - queue, - onProcess: (groupJid, proc, containerName, groupFolder) => - queue.registerProcess(groupJid, proc, containerName, groupFolder), - sendMessage: async (jid, rawText) => { - const channel = findChannel(channels, jid); - if (!channel) { - logger.warn({ jid }, 'No channel owns JID, cannot send message'); - return; - } - const text = formatOutbound(rawText); - if (text) await channel.sendMessage(jid, text); - }, - }); - startIpcWatcher({ - sendMessage: (jid, text) => { - const channel = findChannel(channels, jid); - if (!channel) throw new Error(`No channel for JID: ${jid}`); - return channel.sendMessage(jid, text); - }, - registeredGroups: () => registeredGroups, - registerGroup, - syncGroups: async (force: boolean) => { - await Promise.all(channels.filter((ch) => ch.syncGroups).map((ch) => ch.syncGroups!(force))); - }, - getAvailableGroups, - writeGroupsSnapshot: (gf, im, ag, rj) => writeGroupsSnapshot(gf, im, ag, rj), - onTasksChanged: () => { - const tasks = getAllTasks(); - const taskRows = tasks.map((t) => ({ - id: t.id, - groupFolder: t.group_folder, - prompt: t.prompt, - script: t.script || undefined, - schedule_type: t.schedule_type, - schedule_value: t.schedule_value, - status: t.status, - next_run: t.next_run, - })); - for (const group of Object.values(registeredGroups)) { - writeTasksSnapshot(group.folder, group.isMain === true, taskRows); - } - }, - }); - startSessionCleanup(); - queue.setProcessMessagesFn(processGroupMessages); - recoverPendingMessages(); - startMessageLoop().catch((err) => { - logger.fatal({ err }, 'Message loop crashed unexpectedly'); - process.exit(1); - }); + return configs; } -// Guard: only run when executed directly, not when imported by tests -const isDirectRun = - process.argv[1] && new URL(import.meta.url).pathname === new URL(`file://${process.argv[1]}`).pathname; +/** Handle a user's response to an ask_user_question card. */ +async function handleQuestionResponse(questionId: string, selectedOption: string, userId: string): Promise { + const pq = getPendingQuestion(questionId); + if (!pq) { + log.warn('Pending question not found (may have expired)', { questionId }); + return; + } -if (isDirectRun) { - main().catch((err) => { - logger.error({ err }, 'Failed to start NanoClaw'); - process.exit(1); + const session = getSession(pq.session_id); + if (!session) { + log.warn('Session not found for pending question', { questionId, sessionId: pq.session_id }); + deletePendingQuestion(questionId); + return; + } + + // Write the response to the session DB as a system message + writeSessionMessage(session.agent_group_id, session.id, { + id: `qr-${questionId}-${Date.now()}`, + kind: 'system', + timestamp: new Date().toISOString(), + platformId: pq.platform_id, + channelType: pq.channel_type, + threadId: pq.thread_id, + content: JSON.stringify({ + type: 'question_response', + questionId, + selectedOption, + userId, + }), }); + + deletePendingQuestion(questionId); + log.info('Question response routed', { questionId, selectedOption, sessionId: session.id }); + + // Wake the container so the MCP tool's poll picks up the response + await wakeContainer(session); } + +/** Graceful shutdown. */ +async function shutdown(signal: string): Promise { + log.info('Shutdown signal received', { signal }); + stopDeliveryPolls(); + stopHostSweep(); + await teardownChannelAdapters(); + process.exit(0); +} + +process.on('SIGTERM', () => shutdown('SIGTERM')); +process.on('SIGINT', () => shutdown('SIGINT')); + +main().catch((err) => { + log.fatal('Startup failed', { err }); + process.exit(1); +}); diff --git a/src/mount-security.ts b/src/mount-security.ts index c44620c..cea550a 100644 --- a/src/mount-security.ts +++ b/src/mount-security.ts @@ -10,8 +10,25 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import { MOUNT_ALLOWLIST_PATH } from './config.js'; -import { logger } from './logger.js'; -import { AdditionalMount, AllowedRoot, MountAllowlist } from './types.js'; +import { log } from './log.js'; + +export interface AdditionalMount { + hostPath: string; + containerPath?: string; + readonly?: boolean; +} + +export interface MountAllowlist { + allowedRoots: AllowedRoot[]; + blockedPatterns: string[]; + nonMainReadOnly: boolean; +} + +export interface AllowedRoot { + path: string; + allowReadWrite: boolean; + description?: string; +} // Cache the allowlist in memory - only reloads on process restart let cachedAllowlist: MountAllowlist | null = null; @@ -59,11 +76,7 @@ export function loadMountAllowlist(): MountAllowlist | null { if (!fs.existsSync(MOUNT_ALLOWLIST_PATH)) { // Do NOT cache this as an error — file may be created later without restart. // Only parse/structural errors are permanently cached. - logger.warn( - { path: MOUNT_ALLOWLIST_PATH }, - 'Mount allowlist not found - additional mounts will be BLOCKED. ' + - 'Create the file to enable additional mounts.', - ); + log.warn('Mount allowlist not found - additional mounts will be BLOCKED. Create the file to enable additional mounts.', { path: MOUNT_ALLOWLIST_PATH }); return null; } @@ -88,25 +101,12 @@ export function loadMountAllowlist(): MountAllowlist | null { allowlist.blockedPatterns = mergedBlockedPatterns; cachedAllowlist = allowlist; - logger.info( - { - path: MOUNT_ALLOWLIST_PATH, - allowedRoots: allowlist.allowedRoots.length, - blockedPatterns: allowlist.blockedPatterns.length, - }, - 'Mount allowlist loaded successfully', - ); + log.info('Mount allowlist loaded successfully', { path: MOUNT_ALLOWLIST_PATH, allowedRoots: allowlist.allowedRoots.length, blockedPatterns: allowlist.blockedPatterns.length }); return cachedAllowlist; } catch (err) { allowlistLoadError = err instanceof Error ? err.message : String(err); - logger.error( - { - path: MOUNT_ALLOWLIST_PATH, - error: allowlistLoadError, - }, - 'Failed to load mount allowlist - additional mounts will be BLOCKED', - ); + log.error('Failed to load mount allowlist - additional mounts will be BLOCKED', { path: MOUNT_ALLOWLIST_PATH, error: allowlistLoadError }); return null; } } @@ -283,22 +283,11 @@ export function validateMount(mount: AdditionalMount, isMain: boolean): MountVal if (!isMain && allowlist.nonMainReadOnly) { // Non-main groups forced to read-only effectiveReadonly = true; - logger.info( - { - mount: mount.hostPath, - }, - 'Mount forced to read-only for non-main group', - ); + log.info('Mount forced to read-only for non-main group', { mount: mount.hostPath }); } else if (!allowedRoot.allowReadWrite) { // Root doesn't allow read-write effectiveReadonly = true; - logger.info( - { - mount: mount.hostPath, - root: allowedRoot.path, - }, - 'Mount forced to read-only - root does not allow read-write', - ); + log.info('Mount forced to read-only - root does not allow read-write', { mount: mount.hostPath, root: allowedRoot.path }); } else { // Read-write allowed effectiveReadonly = false; @@ -344,26 +333,9 @@ export function validateAdditionalMounts( readonly: result.effectiveReadonly!, }); - logger.debug( - { - group: groupName, - hostPath: result.realHostPath, - containerPath: result.resolvedContainerPath, - readonly: result.effectiveReadonly, - reason: result.reason, - }, - 'Mount validated successfully', - ); + log.debug('Mount validated successfully', { group: groupName, hostPath: result.realHostPath, containerPath: result.resolvedContainerPath, readonly: result.effectiveReadonly, reason: result.reason }); } else { - logger.warn( - { - group: groupName, - requestedPath: mount.hostPath, - containerPath: mount.containerPath, - reason: result.reason, - }, - 'Additional mount REJECTED', - ); + log.warn('Additional mount REJECTED', { group: groupName, requestedPath: mount.hostPath, containerPath: mount.containerPath, reason: result.reason }); } } diff --git a/src/router-v2.ts b/src/router-v2.ts deleted file mode 100644 index 3859576..0000000 --- a/src/router-v2.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Inbound message routing for v2. - * - * Channel adapter event → resolve messaging group → resolve agent group - * → resolve/create session → write messages_in → wake container - */ -import { getMessagingGroupByPlatform, createMessagingGroup, getMessagingGroupAgents } from './db/messaging-groups.js'; -import { log } from './log.js'; -import { resolveSession, writeSessionMessage } from './session-manager.js'; -import { wakeContainer } from './container-runner-v2.js'; -import { getSession } from './db/sessions.js'; -import type { MessagingGroupAgent } from './types-v2.js'; - -function generateId(): string { - return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; -} - -export interface InboundEvent { - channelType: string; - platformId: string; - threadId: string | null; - message: { - id: string; - kind: 'chat' | 'chat-sdk'; - content: string; // JSON blob - timestamp: string; - }; -} - -/** - * Route an inbound message from a channel adapter to the correct session. - * Creates messaging group + session if they don't exist yet. - */ -export async function routeInbound(event: InboundEvent): Promise { - // 1. Resolve messaging group - let mg = getMessagingGroupByPlatform(event.channelType, event.platformId); - - if (!mg) { - // Auto-create messaging group (adapter already decided to forward this) - const mgId = `mg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; - mg = { - id: mgId, - channel_type: event.channelType, - platform_id: event.platformId, - name: null, - is_group: 0, - admin_user_id: null, - created_at: new Date().toISOString(), - }; - createMessagingGroup(mg); - log.info('Auto-created messaging group', { - id: mgId, - channelType: event.channelType, - platformId: event.platformId, - }); - } - - // 2. Resolve agent group via messaging_group_agents - const agents = getMessagingGroupAgents(mg.id); - if (agents.length === 0) { - log.warn('No agent groups configured for messaging group', { - messagingGroupId: mg.id, - platformId: event.platformId, - }); - return; - } - - // Pick the best matching agent (highest priority, trigger matching in future) - const match = pickAgent(agents, event); - if (!match) { - log.debug('No agent matched for message', { messagingGroupId: mg.id }); - return; - } - - // 3. Resolve or create session - const { session, created } = resolveSession(match.agent_group_id, mg.id, event.threadId, match.session_mode); - - // 4. Write message to session DB - writeSessionMessage(session.agent_group_id, session.id, { - id: event.message.id || generateId(), - kind: event.message.kind, - timestamp: event.message.timestamp, - platformId: event.platformId, - channelType: event.channelType, - threadId: event.threadId, - content: event.message.content, - }); - - log.info('Message routed', { - sessionId: session.id, - agentGroup: match.agent_group_id, - kind: event.message.kind, - created, - }); - - // 5. Wake container - const freshSession = getSession(session.id); - if (freshSession) { - await wakeContainer(freshSession); - } -} - -/** - * Pick the matching agent for an inbound event. - * Currently: highest priority agent. Future: trigger rule matching. - */ -function pickAgent(agents: MessagingGroupAgent[], _event: InboundEvent): MessagingGroupAgent | null { - // Agents are already ordered by priority DESC from the DB query - // TODO: apply trigger_rules matching (pattern, mentionOnly, etc.) - return agents[0] ?? null; -} diff --git a/src/router.ts b/src/router.ts index 4c7dd38..2bcce73 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,43 +1,111 @@ -import { Channel, NewMessage } from './types.js'; -import { formatLocalTime } from './timezone.js'; +/** + * Inbound message routing for v2. + * + * Channel adapter event → resolve messaging group → resolve agent group + * → resolve/create session → write messages_in → wake container + */ +import { getMessagingGroupByPlatform, createMessagingGroup, getMessagingGroupAgents } from './db/messaging-groups.js'; +import { log } from './log.js'; +import { resolveSession, writeSessionMessage } from './session-manager.js'; +import { wakeContainer } from './container-runner.js'; +import { getSession } from './db/sessions.js'; +import type { MessagingGroupAgent } from './types.js'; -export function escapeXml(s: string): string { - if (!s) return ''; - return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +function generateId(): string { + return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } -export function formatMessages(messages: NewMessage[], timezone: string): string { - const lines = messages.map((m) => { - const displayTime = formatLocalTime(m.timestamp, timezone); - const replyAttr = m.reply_to_message_id ? ` reply_to="${escapeXml(m.reply_to_message_id)}"` : ''; - const replySnippet = - m.reply_to_message_content && m.reply_to_sender_name - ? `\n ${escapeXml(m.reply_to_message_content)}` - : ''; - return `${replySnippet}${escapeXml(m.content)}`; +export interface InboundEvent { + channelType: string; + platformId: string; + threadId: string | null; + message: { + id: string; + kind: 'chat' | 'chat-sdk'; + content: string; // JSON blob + timestamp: string; + }; +} + +/** + * Route an inbound message from a channel adapter to the correct session. + * Creates messaging group + session if they don't exist yet. + */ +export async function routeInbound(event: InboundEvent): Promise { + // 1. Resolve messaging group + let mg = getMessagingGroupByPlatform(event.channelType, event.platformId); + + if (!mg) { + // Auto-create messaging group (adapter already decided to forward this) + const mgId = `mg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + mg = { + id: mgId, + channel_type: event.channelType, + platform_id: event.platformId, + name: null, + is_group: 0, + admin_user_id: null, + created_at: new Date().toISOString(), + }; + createMessagingGroup(mg); + log.info('Auto-created messaging group', { + id: mgId, + channelType: event.channelType, + platformId: event.platformId, + }); + } + + // 2. Resolve agent group via messaging_group_agents + const agents = getMessagingGroupAgents(mg.id); + if (agents.length === 0) { + log.warn('No agent groups configured for messaging group', { + messagingGroupId: mg.id, + platformId: event.platformId, + }); + return; + } + + // Pick the best matching agent (highest priority, trigger matching in future) + const match = pickAgent(agents, event); + if (!match) { + log.debug('No agent matched for message', { messagingGroupId: mg.id }); + return; + } + + // 3. Resolve or create session + const { session, created } = resolveSession(match.agent_group_id, mg.id, event.threadId, match.session_mode); + + // 4. Write message to session DB + writeSessionMessage(session.agent_group_id, session.id, { + id: event.message.id || generateId(), + kind: event.message.kind, + timestamp: event.message.timestamp, + platformId: event.platformId, + channelType: event.channelType, + threadId: event.threadId, + content: event.message.content, }); - const header = `\n`; + log.info('Message routed', { + sessionId: session.id, + agentGroup: match.agent_group_id, + kind: event.message.kind, + created, + }); - return `${header}\n${lines.join('\n')}\n`; + // 5. Wake container + const freshSession = getSession(session.id); + if (freshSession) { + await wakeContainer(freshSession); + } } -export function stripInternalTags(text: string): string { - return text.replace(/[\s\S]*?<\/internal>/g, '').trim(); -} - -export function formatOutbound(rawText: string): string { - const text = stripInternalTags(rawText); - if (!text) return ''; - return text; -} - -export function routeOutbound(channels: Channel[], jid: string, text: string): Promise { - const channel = channels.find((c) => c.ownsJid(jid) && c.isConnected()); - if (!channel) throw new Error(`No channel for JID: ${jid}`); - return channel.sendMessage(jid, text); -} - -export function findChannel(channels: Channel[], jid: string): Channel | undefined { - return channels.find((c) => c.ownsJid(jid)); +/** + * Pick the matching agent for an inbound event. + * Currently: highest priority agent. Future: trigger rule matching. + */ +function pickAgent(agents: MessagingGroupAgent[], _event: InboundEvent): MessagingGroupAgent | null { + // Agents are already ordered by priority DESC from the DB query + // TODO: apply trigger_rules matching (pattern, mentionOnly, etc.) + return agents[0] ?? null; } diff --git a/src/session-manager.ts b/src/session-manager.ts index 4498198..64e1922 100644 --- a/src/session-manager.ts +++ b/src/session-manager.ts @@ -10,7 +10,7 @@ import { DATA_DIR } from './config.js'; import { createSession, findSession, getSession, updateSession } from './db/sessions.js'; import { log } from './log.js'; import { SESSION_SCHEMA } from './db/schema.js'; -import type { Session } from './types-v2.js'; +import type { Session } from './types.js'; /** Root directory for all session data. */ export function sessionsBaseDir(): string { diff --git a/src/state-sqlite.ts b/src/state-sqlite.ts index 64731a2..ec15bd6 100644 --- a/src/state-sqlite.ts +++ b/src/state-sqlite.ts @@ -31,9 +31,9 @@ export class SqliteStateAdapter implements StateAdapter { async get(key: string): Promise { this.cleanup(); - const row = this.db - .prepare('SELECT value, expires_at FROM chat_sdk_kv WHERE key = ?') - .get(key) as { value: string; expires_at: number | null } | undefined; + const row = this.db.prepare('SELECT value, expires_at FROM chat_sdk_kv WHERE key = ?').get(key) as + | { value: string; expires_at: number | null } + | undefined; if (!row) return null; if (row.expires_at && row.expires_at < Date.now()) { this.db.prepare('DELETE FROM chat_sdk_kv WHERE key = ?').run(key); @@ -44,16 +44,22 @@ export class SqliteStateAdapter implements StateAdapter { async set(key: string, value: T, ttlMs?: number): Promise { const expiresAt = ttlMs ? Date.now() + ttlMs : null; - this.db.prepare('INSERT OR REPLACE INTO chat_sdk_kv (key, value, expires_at) VALUES (?, ?, ?)').run(key, JSON.stringify(value), expiresAt); + this.db + .prepare('INSERT OR REPLACE INTO chat_sdk_kv (key, value, expires_at) VALUES (?, ?, ?)') + .run(key, JSON.stringify(value), expiresAt); } async setIfNotExists(key: string, value: unknown, ttlMs?: number): Promise { - const existing = this.db.prepare('SELECT expires_at FROM chat_sdk_kv WHERE key = ?').get(key) as { expires_at: number | null } | undefined; + const existing = this.db.prepare('SELECT expires_at FROM chat_sdk_kv WHERE key = ?').get(key) as + | { expires_at: number | null } + | undefined; if (existing?.expires_at && existing.expires_at < Date.now()) { this.db.prepare('DELETE FROM chat_sdk_kv WHERE key = ?').run(key); } const expiresAt = ttlMs ? Date.now() + ttlMs : null; - const result = this.db.prepare('INSERT OR IGNORE INTO chat_sdk_kv (key, value, expires_at) VALUES (?, ?, ?)').run(key, JSON.stringify(value), expiresAt); + const result = this.db + .prepare('INSERT OR IGNORE INTO chat_sdk_kv (key, value, expires_at) VALUES (?, ?, ?)') + .run(key, JSON.stringify(value), expiresAt); return result.changes > 0; } @@ -83,7 +89,9 @@ export class SqliteStateAdapter implements StateAdapter { const token = crypto.randomUUID(); const expiresAt = now + ttlMs; this.db.prepare('DELETE FROM chat_sdk_locks WHERE thread_id = ? AND expires_at < ?').run(threadId, now); - const result = this.db.prepare('INSERT OR IGNORE INTO chat_sdk_locks (thread_id, token, expires_at) VALUES (?, ?, ?)').run(threadId, token, expiresAt); + const result = this.db + .prepare('INSERT OR IGNORE INTO chat_sdk_locks (thread_id, token, expires_at) VALUES (?, ?, ?)') + .run(threadId, token, expiresAt); if (result.changes === 0) return null; return { threadId, token, expiresAt }; } @@ -94,7 +102,9 @@ export class SqliteStateAdapter implements StateAdapter { async extendLock(lock: Lock, ttlMs: number): Promise { const newExpiry = Date.now() + ttlMs; - const result = this.db.prepare('UPDATE chat_sdk_locks SET expires_at = ? WHERE thread_id = ? AND token = ?').run(newExpiry, lock.threadId, lock.token); + const result = this.db + .prepare('UPDATE chat_sdk_locks SET expires_at = ? WHERE thread_id = ? AND token = ?') + .run(newExpiry, lock.threadId, lock.token); if (result.changes > 0) { lock.expiresAt = newExpiry; return true; @@ -110,9 +120,13 @@ export class SqliteStateAdapter implements StateAdapter { async appendToList(key: string, value: unknown, options?: { maxLength?: number; ttlMs?: number }): Promise { const expiresAt = options?.ttlMs ? Date.now() + options.ttlMs : null; - const maxRow = this.db.prepare('SELECT MAX(idx) as maxIdx FROM chat_sdk_lists WHERE key = ?').get(key) as { maxIdx: number | null } | undefined; + const maxRow = this.db.prepare('SELECT MAX(idx) as maxIdx FROM chat_sdk_lists WHERE key = ?').get(key) as + | { maxIdx: number | null } + | undefined; const nextIdx = (maxRow?.maxIdx ?? -1) + 1; - this.db.prepare('INSERT INTO chat_sdk_lists (key, idx, value, expires_at) VALUES (?, ?, ?, ?)').run(key, nextIdx, JSON.stringify(value), expiresAt); + this.db + .prepare('INSERT INTO chat_sdk_lists (key, idx, value, expires_at) VALUES (?, ?, ?, ?)') + .run(key, nextIdx, JSON.stringify(value), expiresAt); if (options?.maxLength) { const cutoff = nextIdx - options.maxLength; if (cutoff >= 0) { @@ -123,7 +137,11 @@ export class SqliteStateAdapter implements StateAdapter { async getList(key: string): Promise { const now = Date.now(); - const rows = this.db.prepare('SELECT value FROM chat_sdk_lists WHERE key = ? AND (expires_at IS NULL OR expires_at > ?) ORDER BY idx ASC').all(key, now) as { value: string }[]; + const rows = this.db + .prepare( + 'SELECT value FROM chat_sdk_lists WHERE key = ? AND (expires_at IS NULL OR expires_at > ?) ORDER BY idx ASC', + ) + .all(key, now) as { value: string }[]; return rows.map((r) => JSON.parse(r.value) as T); } @@ -137,7 +155,9 @@ export class SqliteStateAdapter implements StateAdapter { async dequeue(threadId: string): Promise { const key = `queue:${threadId}`; - const row = this.db.prepare('SELECT idx, value FROM chat_sdk_lists WHERE key = ? ORDER BY idx ASC LIMIT 1').get(key) as { idx: number; value: string } | undefined; + const row = this.db + .prepare('SELECT idx, value FROM chat_sdk_lists WHERE key = ? ORDER BY idx ASC LIMIT 1') + .get(key) as { idx: number; value: string } | undefined; if (!row) return null; this.db.prepare('DELETE FROM chat_sdk_lists WHERE key = ? AND idx = ?').run(key, row.idx); return JSON.parse(row.value) as QueueEntry; @@ -145,7 +165,9 @@ export class SqliteStateAdapter implements StateAdapter { async queueDepth(threadId: string): Promise { const key = `queue:${threadId}`; - const row = this.db.prepare('SELECT COUNT(*) as count FROM chat_sdk_lists WHERE key = ?').get(key) as { count: number }; + const row = this.db.prepare('SELECT COUNT(*) as count FROM chat_sdk_lists WHERE key = ?').get(key) as { + count: number; + }; return row.count; } diff --git a/src/types-v2.ts b/src/types-v2.ts deleted file mode 100644 index 7b202bb..0000000 --- a/src/types-v2.ts +++ /dev/null @@ -1,90 +0,0 @@ -// ── Central DB entities ── - -export interface AgentGroup { - id: string; - name: string; - folder: string; - is_admin: number; // 0 | 1 - agent_provider: string | null; - container_config: string | null; // JSON: { additionalMounts, timeout } - created_at: string; -} - -export interface MessagingGroup { - id: string; - channel_type: string; - platform_id: string; - name: string | null; - is_group: number; // 0 | 1 - admin_user_id: string | null; - created_at: string; -} - -export interface MessagingGroupAgent { - id: string; - messaging_group_id: string; - agent_group_id: string; - trigger_rules: string | null; // JSON: { pattern, mentionOnly, excludeSenders, includeSenders } - response_scope: 'all' | 'triggered' | 'allowlisted'; - session_mode: 'shared' | 'per-thread'; - priority: number; - created_at: string; -} - -export interface Session { - id: string; - agent_group_id: string; - messaging_group_id: string | null; - thread_id: string | null; - agent_provider: string | null; - status: 'active' | 'closed'; - container_status: 'running' | 'idle' | 'stopped'; - last_active: string | null; - created_at: string; -} - -// ── Session DB entities ── - -export type MessageInKind = 'chat' | 'chat-sdk' | 'task' | 'webhook' | 'system'; -export type MessageInStatus = 'pending' | 'processing' | 'completed' | 'failed'; - -export interface MessageIn { - id: string; - kind: MessageInKind; - timestamp: string; - status: MessageInStatus; - status_changed: string | null; - process_after: string | null; - recurrence: string | null; - tries: number; - platform_id: string | null; - channel_type: string | null; - thread_id: string | null; - content: string; // JSON blob -} - -export interface MessageOut { - id: string; - in_reply_to: string | null; - timestamp: string; - delivered: number; // 0 | 1 - deliver_after: string | null; - recurrence: string | null; - kind: string; - platform_id: string | null; - channel_type: string | null; - thread_id: string | null; - content: string; // JSON blob -} - -// ── Pending questions (central DB) ── - -export interface PendingQuestion { - question_id: string; - session_id: string; - message_out_id: string; - platform_id: string | null; - channel_type: string | null; - thread_id: string | null; - created_at: string; -} diff --git a/src/types.ts b/src/types.ts index 717aff6..7b202bb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,112 +1,90 @@ -export interface AdditionalMount { - hostPath: string; // Absolute path on host (supports ~ for home) - containerPath?: string; // Optional — defaults to basename of hostPath. Mounted at /workspace/extra/{value} - readonly?: boolean; // Default: true for safety -} +// ── Central DB entities ── -/** - * Mount Allowlist - Security configuration for additional mounts - * This file should be stored at ~/.config/nanoclaw/mount-allowlist.json - * and is NOT mounted into any container, making it tamper-proof from agents. - */ -export interface MountAllowlist { - // Directories that can be mounted into containers - allowedRoots: AllowedRoot[]; - // Glob patterns for paths that should never be mounted (e.g., ".ssh", ".gnupg") - blockedPatterns: string[]; - // If true, non-main groups can only mount read-only regardless of config - nonMainReadOnly: boolean; -} - -export interface AllowedRoot { - // Absolute path or ~ for home (e.g., "~/projects", "/var/repos") - path: string; - // Whether read-write mounts are allowed under this root - allowReadWrite: boolean; - // Optional description for documentation - description?: string; -} - -export interface ContainerConfig { - additionalMounts?: AdditionalMount[]; - timeout?: number; // Default: 300000 (5 minutes) -} - -export interface RegisteredGroup { +export interface AgentGroup { + id: string; name: string; folder: string; - trigger: string; - added_at: string; - containerConfig?: ContainerConfig; - requiresTrigger?: boolean; // Default: true for groups, false for solo chats - isMain?: boolean; // True for the main control group (no trigger, elevated privileges) -} - -export interface NewMessage { - id: string; - chat_jid: string; - sender: string; - sender_name: string; - content: string; - timestamp: string; - is_from_me?: boolean; - is_bot_message?: boolean; - thread_id?: string; - reply_to_message_id?: string; - reply_to_message_content?: string; - reply_to_sender_name?: string; -} - -export interface ScheduledTask { - id: string; - group_folder: string; - chat_jid: string; - prompt: string; - script?: string | null; - schedule_type: 'cron' | 'interval' | 'once'; - schedule_value: string; - context_mode: 'group' | 'isolated'; - next_run: string | null; - last_run: string | null; - last_result: string | null; - status: 'active' | 'paused' | 'completed'; + is_admin: number; // 0 | 1 + agent_provider: string | null; + container_config: string | null; // JSON: { additionalMounts, timeout } created_at: string; } -export interface TaskRunLog { - task_id: string; - run_at: string; - duration_ms: number; - status: 'success' | 'error'; - result: string | null; - error: string | null; +export interface MessagingGroup { + id: string; + channel_type: string; + platform_id: string; + name: string | null; + is_group: number; // 0 | 1 + admin_user_id: string | null; + created_at: string; } -// --- Channel abstraction --- - -export interface Channel { - name: string; - connect(): Promise; - sendMessage(jid: string, text: string): Promise; - isConnected(): boolean; - ownsJid(jid: string): boolean; - disconnect(): Promise; - // Optional: typing indicator. Channels that support it implement it. - setTyping?(jid: string, isTyping: boolean): Promise; - // Optional: sync group/chat names from the platform. - syncGroups?(force: boolean): Promise; +export interface MessagingGroupAgent { + id: string; + messaging_group_id: string; + agent_group_id: string; + trigger_rules: string | null; // JSON: { pattern, mentionOnly, excludeSenders, includeSenders } + response_scope: 'all' | 'triggered' | 'allowlisted'; + session_mode: 'shared' | 'per-thread'; + priority: number; + created_at: string; } -// Callback type that channels use to deliver inbound messages -export type OnInboundMessage = (chatJid: string, message: NewMessage) => void; +export interface Session { + id: string; + agent_group_id: string; + messaging_group_id: string | null; + thread_id: string | null; + agent_provider: string | null; + status: 'active' | 'closed'; + container_status: 'running' | 'idle' | 'stopped'; + last_active: string | null; + created_at: string; +} -// Callback for chat metadata discovery. -// name is optional — channels that deliver names inline (Telegram) pass it here; -// channels that sync names separately (via syncGroups) omit it. -export type OnChatMetadata = ( - chatJid: string, - timestamp: string, - name?: string, - channel?: string, - isGroup?: boolean, -) => void; +// ── Session DB entities ── + +export type MessageInKind = 'chat' | 'chat-sdk' | 'task' | 'webhook' | 'system'; +export type MessageInStatus = 'pending' | 'processing' | 'completed' | 'failed'; + +export interface MessageIn { + id: string; + kind: MessageInKind; + timestamp: string; + status: MessageInStatus; + status_changed: string | null; + process_after: string | null; + recurrence: string | null; + tries: number; + platform_id: string | null; + channel_type: string | null; + thread_id: string | null; + content: string; // JSON blob +} + +export interface MessageOut { + id: string; + in_reply_to: string | null; + timestamp: string; + delivered: number; // 0 | 1 + deliver_after: string | null; + recurrence: string | null; + kind: string; + platform_id: string | null; + channel_type: string | null; + thread_id: string | null; + content: string; // JSON blob +} + +// ── Pending questions (central DB) ── + +export interface PendingQuestion { + question_id: string; + session_id: string; + message_out_id: string; + platform_id: string | null; + channel_type: string | null; + thread_id: string | null; + created_at: string; +} diff --git a/src/channels/registry.test.ts b/src/v1/channels/registry.test.ts similarity index 100% rename from src/channels/registry.test.ts rename to src/v1/channels/registry.test.ts diff --git a/src/channels/registry.ts b/src/v1/channels/registry.ts similarity index 100% rename from src/channels/registry.ts rename to src/v1/channels/registry.ts diff --git a/src/v1/config.ts b/src/v1/config.ts new file mode 100644 index 0000000..ef1ba9e --- /dev/null +++ b/src/v1/config.ts @@ -0,0 +1,62 @@ +import os from 'os'; +import path from 'path'; + +import { readEnvFile } from './env.js'; +import { isValidTimezone } from './timezone.js'; + +// Read config values from .env (falls back to process.env). +const envConfig = readEnvFile(['ASSISTANT_NAME', 'ASSISTANT_HAS_OWN_NUMBER', 'ONECLI_URL', 'TZ']); + +export const ASSISTANT_NAME = process.env.ASSISTANT_NAME || envConfig.ASSISTANT_NAME || 'Andy'; +export const ASSISTANT_HAS_OWN_NUMBER = + (process.env.ASSISTANT_HAS_OWN_NUMBER || envConfig.ASSISTANT_HAS_OWN_NUMBER) === 'true'; +export const POLL_INTERVAL = 2000; +export const SCHEDULER_POLL_INTERVAL = 60000; + +// Absolute paths needed for container mounts +const PROJECT_ROOT = process.cwd(); +const HOME_DIR = process.env.HOME || os.homedir(); + +// Mount security: allowlist stored OUTSIDE project root, never mounted into containers +export const MOUNT_ALLOWLIST_PATH = path.join(HOME_DIR, '.config', 'nanoclaw', 'mount-allowlist.json'); +export const SENDER_ALLOWLIST_PATH = path.join(HOME_DIR, '.config', 'nanoclaw', 'sender-allowlist.json'); +export const STORE_DIR = path.resolve(PROJECT_ROOT, 'store'); +export const GROUPS_DIR = path.resolve(PROJECT_ROOT, 'groups'); +export const DATA_DIR = path.resolve(PROJECT_ROOT, 'data'); + +export const CONTAINER_IMAGE = process.env.CONTAINER_IMAGE || 'nanoclaw-agent:latest'; +export const CONTAINER_TIMEOUT = parseInt(process.env.CONTAINER_TIMEOUT || '1800000', 10); +export const CONTAINER_MAX_OUTPUT_SIZE = parseInt(process.env.CONTAINER_MAX_OUTPUT_SIZE || '10485760', 10); // 10MB default +export const ONECLI_URL = process.env.ONECLI_URL || envConfig.ONECLI_URL; +export const MAX_MESSAGES_PER_PROMPT = Math.max(1, parseInt(process.env.MAX_MESSAGES_PER_PROMPT || '10', 10) || 10); +export const IPC_POLL_INTERVAL = 1000; +export const IDLE_TIMEOUT = parseInt(process.env.IDLE_TIMEOUT || '1800000', 10); // 30min default — how long to keep container alive after last result +export const MAX_CONCURRENT_CONTAINERS = Math.max(1, parseInt(process.env.MAX_CONCURRENT_CONTAINERS || '5', 10) || 5); + +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +export function buildTriggerPattern(trigger: string): RegExp { + return new RegExp(`^${escapeRegex(trigger.trim())}\\b`, 'i'); +} + +export const DEFAULT_TRIGGER = `@${ASSISTANT_NAME}`; + +export function getTriggerPattern(trigger?: string): RegExp { + const normalizedTrigger = trigger?.trim(); + return buildTriggerPattern(normalizedTrigger || DEFAULT_TRIGGER); +} + +export const TRIGGER_PATTERN = buildTriggerPattern(DEFAULT_TRIGGER); + +// Timezone for scheduled tasks, message formatting, etc. +// Validates each candidate is a real IANA identifier before accepting. +function resolveConfigTimezone(): string { + const candidates = [process.env.TZ, envConfig.TZ, Intl.DateTimeFormat().resolvedOptions().timeZone]; + for (const tz of candidates) { + if (tz && isValidTimezone(tz)) return tz; + } + return 'UTC'; +} +export const TIMEZONE = resolveConfigTimezone(); diff --git a/src/container-runner.test.ts b/src/v1/container-runner.test.ts similarity index 100% rename from src/container-runner.test.ts rename to src/v1/container-runner.test.ts diff --git a/src/v1/container-runner.ts b/src/v1/container-runner.ts new file mode 100644 index 0000000..b04cc28 --- /dev/null +++ b/src/v1/container-runner.ts @@ -0,0 +1,677 @@ +/** + * Container Runner for NanoClaw + * Spawns agent execution in containers and handles IPC + */ +import { ChildProcess, spawn } from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +import { + CONTAINER_IMAGE, + CONTAINER_MAX_OUTPUT_SIZE, + CONTAINER_TIMEOUT, + DATA_DIR, + GROUPS_DIR, + IDLE_TIMEOUT, + ONECLI_URL, + TIMEZONE, +} from './config.js'; +import { resolveGroupFolderPath, resolveGroupIpcPath } from './group-folder.js'; +import { logger } from './logger.js'; +import { CONTAINER_RUNTIME_BIN, hostGatewayArgs, readonlyMountArgs, stopContainer } from './container-runtime.js'; +import { OneCLI } from '@onecli-sh/sdk'; +import { validateAdditionalMounts } from './mount-security.js'; +import { RegisteredGroup } from './types.js'; + +const onecli = new OneCLI({ url: ONECLI_URL }); + +// Sentinel markers for robust output parsing (must match agent-runner) +const OUTPUT_START_MARKER = '---NANOCLAW_OUTPUT_START---'; +const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---'; + +export interface ContainerInput { + prompt: string; + sessionId?: string; + groupFolder: string; + chatJid: string; + isMain: boolean; + isScheduledTask?: boolean; + assistantName?: string; + script?: string; +} + +export interface ContainerOutput { + status: 'success' | 'error'; + result: string | null; + newSessionId?: string; + error?: string; +} + +interface VolumeMount { + hostPath: string; + containerPath: string; + readonly: boolean; +} + +function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount[] { + const mounts: VolumeMount[] = []; + const projectRoot = process.cwd(); + const groupDir = resolveGroupFolderPath(group.folder); + + if (isMain) { + // Main gets the project root read-only. Writable paths the agent needs + // (store, group folder, IPC, .claude/) are mounted separately below. + // Read-only prevents the agent from modifying host application code + // (src/, dist/, package.json, etc.) which would bypass the sandbox + // entirely on next restart. + mounts.push({ + hostPath: projectRoot, + containerPath: '/workspace/project', + readonly: true, + }); + + // Shadow .env so the agent cannot read secrets from the mounted project root. + // Credentials are injected by the OneCLI gateway, never exposed to containers. + const envFile = path.join(projectRoot, '.env'); + if (fs.existsSync(envFile)) { + mounts.push({ + hostPath: '/dev/null', + containerPath: '/workspace/project/.env', + readonly: true, + }); + } + + // Main gets writable access to the store (SQLite DB) so it can + // query and write to the database directly. + const storeDir = path.join(projectRoot, 'store'); + mounts.push({ + hostPath: storeDir, + containerPath: '/workspace/project/store', + readonly: false, + }); + + // Main also gets its group folder as the working directory + mounts.push({ + hostPath: groupDir, + containerPath: '/workspace/group', + readonly: false, + }); + + // Global memory directory — writable for main so it can update shared context + const globalDir = path.join(GROUPS_DIR, 'global'); + if (fs.existsSync(globalDir)) { + mounts.push({ + hostPath: globalDir, + containerPath: '/workspace/global', + readonly: false, + }); + } + } else { + // Other groups only get their own folder + mounts.push({ + hostPath: groupDir, + containerPath: '/workspace/group', + readonly: false, + }); + + // Global memory directory (read-only for non-main) + // Only directory mounts are supported, not file mounts + const globalDir = path.join(GROUPS_DIR, 'global'); + if (fs.existsSync(globalDir)) { + mounts.push({ + hostPath: globalDir, + containerPath: '/workspace/global', + readonly: true, + }); + } + } + + // Per-group Claude sessions directory (isolated from other groups) + // Each group gets their own .claude/ to prevent cross-group session access + const groupSessionsDir = path.join(DATA_DIR, 'sessions', group.folder, '.claude'); + fs.mkdirSync(groupSessionsDir, { recursive: true }); + const settingsFile = path.join(groupSessionsDir, 'settings.json'); + if (!fs.existsSync(settingsFile)) { + fs.writeFileSync( + settingsFile, + JSON.stringify( + { + env: { + // Enable agent swarms (subagent orchestration) + // https://code.claude.com/docs/en/agent-teams#orchestrate-teams-of-claude-code-sessions + CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1', + // Load CLAUDE.md from additional mounted directories + // https://code.claude.com/docs/en/memory#load-memory-from-additional-directories + CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: '1', + // Enable Claude's memory feature (persists user preferences between sessions) + // https://code.claude.com/docs/en/memory#manage-auto-memory + CLAUDE_CODE_DISABLE_AUTO_MEMORY: '0', + }, + }, + null, + 2, + ) + '\n', + ); + } + + // Sync skills from container/skills/ into each group's .claude/skills/ + const skillsSrc = path.join(process.cwd(), 'container', 'skills'); + const skillsDst = path.join(groupSessionsDir, 'skills'); + if (fs.existsSync(skillsSrc)) { + for (const skillDir of fs.readdirSync(skillsSrc)) { + const srcDir = path.join(skillsSrc, skillDir); + if (!fs.statSync(srcDir).isDirectory()) continue; + const dstDir = path.join(skillsDst, skillDir); + fs.cpSync(srcDir, dstDir, { recursive: true }); + } + } + mounts.push({ + hostPath: groupSessionsDir, + containerPath: '/home/node/.claude', + readonly: false, + }); + + // Per-group IPC namespace: each group gets its own IPC directory + // This prevents cross-group privilege escalation via IPC + const groupIpcDir = resolveGroupIpcPath(group.folder); + fs.mkdirSync(path.join(groupIpcDir, 'messages'), { recursive: true }); + fs.mkdirSync(path.join(groupIpcDir, 'tasks'), { recursive: true }); + fs.mkdirSync(path.join(groupIpcDir, 'input'), { recursive: true }); + mounts.push({ + hostPath: groupIpcDir, + containerPath: '/workspace/ipc', + readonly: false, + }); + + // Copy agent-runner source into a per-group writable location so agents + // can customize it (add tools, change behavior) without affecting other + // groups. Recompiled on container startup via entrypoint.sh. + const agentRunnerSrc = path.join(projectRoot, 'container', 'agent-runner', 'src'); + const groupAgentRunnerDir = path.join(DATA_DIR, 'sessions', group.folder, 'agent-runner-src'); + if (fs.existsSync(agentRunnerSrc)) { + const srcIndex = path.join(agentRunnerSrc, 'index.ts'); + const cachedIndex = path.join(groupAgentRunnerDir, 'index.ts'); + const needsCopy = + !fs.existsSync(groupAgentRunnerDir) || + !fs.existsSync(cachedIndex) || + (fs.existsSync(srcIndex) && fs.statSync(srcIndex).mtimeMs > fs.statSync(cachedIndex).mtimeMs); + if (needsCopy) { + fs.cpSync(agentRunnerSrc, groupAgentRunnerDir, { recursive: true }); + } + } + mounts.push({ + hostPath: groupAgentRunnerDir, + containerPath: '/app/src', + readonly: false, + }); + + // Additional mounts validated against external allowlist (tamper-proof from containers) + if (group.containerConfig?.additionalMounts) { + const validatedMounts = validateAdditionalMounts(group.containerConfig.additionalMounts, group.name, isMain); + mounts.push(...validatedMounts); + } + + return mounts; +} + +async function buildContainerArgs( + mounts: VolumeMount[], + containerName: string, + agentIdentifier?: string, +): Promise { + const args: string[] = ['run', '-i', '--rm', '--name', containerName]; + + // Pass host timezone so container's local time matches the user's + args.push('-e', `TZ=${TIMEZONE}`); + + // OneCLI gateway handles credential injection — containers never see real secrets. + // The gateway intercepts HTTPS traffic and injects API keys or OAuth tokens. + const onecliApplied = await onecli.applyContainerConfig(args, { + addHostMapping: false, // Nanoclaw already handles host gateway + agent: agentIdentifier, + }); + if (onecliApplied) { + logger.info({ containerName }, 'OneCLI gateway config applied'); + } else { + logger.warn({ containerName }, 'OneCLI gateway not reachable — container will have no credentials'); + } + + // Runtime-specific args for host gateway resolution + args.push(...hostGatewayArgs()); + + // Run as host user so bind-mounted files are accessible. + // Skip when running as root (uid 0), as the container's node user (uid 1000), + // or when getuid is unavailable (native Windows without WSL). + const hostUid = process.getuid?.(); + const hostGid = process.getgid?.(); + if (hostUid != null && hostUid !== 0 && hostUid !== 1000) { + args.push('--user', `${hostUid}:${hostGid}`); + args.push('-e', 'HOME=/home/node'); + } + + for (const mount of mounts) { + if (mount.readonly) { + args.push(...readonlyMountArgs(mount.hostPath, mount.containerPath)); + } else { + args.push('-v', `${mount.hostPath}:${mount.containerPath}`); + } + } + + args.push(CONTAINER_IMAGE); + + return args; +} + +export async function runContainerAgent( + group: RegisteredGroup, + input: ContainerInput, + onProcess: (proc: ChildProcess, containerName: string) => void, + onOutput?: (output: ContainerOutput) => Promise, +): Promise { + const startTime = Date.now(); + + const groupDir = resolveGroupFolderPath(group.folder); + fs.mkdirSync(groupDir, { recursive: true }); + + const mounts = buildVolumeMounts(group, input.isMain); + const safeName = group.folder.replace(/[^a-zA-Z0-9-]/g, '-'); + const containerName = `nanoclaw-${safeName}-${Date.now()}`; + // Main group uses the default OneCLI agent; others use their own agent. + const agentIdentifier = input.isMain ? undefined : group.folder.toLowerCase().replace(/_/g, '-'); + const containerArgs = await buildContainerArgs(mounts, containerName, agentIdentifier); + + logger.debug( + { + group: group.name, + containerName, + mounts: mounts.map((m) => `${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`), + containerArgs: containerArgs.join(' '), + }, + 'Container mount configuration', + ); + + logger.info( + { + group: group.name, + containerName, + mountCount: mounts.length, + isMain: input.isMain, + }, + 'Spawning container agent', + ); + + const logsDir = path.join(groupDir, 'logs'); + fs.mkdirSync(logsDir, { recursive: true }); + + return new Promise((resolve) => { + const container = spawn(CONTAINER_RUNTIME_BIN, containerArgs, { + stdio: ['pipe', 'pipe', 'pipe'], + }); + + onProcess(container, containerName); + + let stdout = ''; + let stderr = ''; + let stdoutTruncated = false; + let stderrTruncated = false; + + container.stdin.write(JSON.stringify(input)); + container.stdin.end(); + + // Streaming output: parse OUTPUT_START/END marker pairs as they arrive + let parseBuffer = ''; + let newSessionId: string | undefined; + let outputChain = Promise.resolve(); + + container.stdout.on('data', (data) => { + const chunk = data.toString(); + + // Always accumulate for logging + if (!stdoutTruncated) { + const remaining = CONTAINER_MAX_OUTPUT_SIZE - stdout.length; + if (chunk.length > remaining) { + stdout += chunk.slice(0, remaining); + stdoutTruncated = true; + logger.warn({ group: group.name, size: stdout.length }, 'Container stdout truncated due to size limit'); + } else { + stdout += chunk; + } + } + + // Stream-parse for output markers + if (onOutput) { + parseBuffer += chunk; + let startIdx: number; + while ((startIdx = parseBuffer.indexOf(OUTPUT_START_MARKER)) !== -1) { + const endIdx = parseBuffer.indexOf(OUTPUT_END_MARKER, startIdx); + if (endIdx === -1) break; // Incomplete pair, wait for more data + + const jsonStr = parseBuffer.slice(startIdx + OUTPUT_START_MARKER.length, endIdx).trim(); + parseBuffer = parseBuffer.slice(endIdx + OUTPUT_END_MARKER.length); + + try { + const parsed: ContainerOutput = JSON.parse(jsonStr); + if (parsed.newSessionId) { + newSessionId = parsed.newSessionId; + } + hadStreamingOutput = true; + // Activity detected — reset the hard timeout + resetTimeout(); + // Call onOutput for all markers (including null results) + // so idle timers start even for "silent" query completions. + outputChain = outputChain.then(() => onOutput(parsed)); + } catch (err) { + logger.warn({ group: group.name, error: err }, 'Failed to parse streamed output chunk'); + } + } + } + }); + + container.stderr.on('data', (data) => { + const chunk = data.toString(); + const lines = chunk.trim().split('\n'); + for (const line of lines) { + if (line) logger.debug({ container: group.folder }, line); + } + // Don't reset timeout on stderr — SDK writes debug logs continuously. + // Timeout only resets on actual output (OUTPUT_MARKER in stdout). + if (stderrTruncated) return; + const remaining = CONTAINER_MAX_OUTPUT_SIZE - stderr.length; + if (chunk.length > remaining) { + stderr += chunk.slice(0, remaining); + stderrTruncated = true; + logger.warn({ group: group.name, size: stderr.length }, 'Container stderr truncated due to size limit'); + } else { + stderr += chunk; + } + }); + + let timedOut = false; + let hadStreamingOutput = false; + const configTimeout = group.containerConfig?.timeout || CONTAINER_TIMEOUT; + // Grace period: hard timeout must be at least IDLE_TIMEOUT + 30s so the + // graceful _close sentinel has time to trigger before the hard kill fires. + const timeoutMs = Math.max(configTimeout, IDLE_TIMEOUT + 30_000); + + const killOnTimeout = () => { + timedOut = true; + logger.error({ group: group.name, containerName }, 'Container timeout, stopping gracefully'); + try { + stopContainer(containerName); + } catch (err) { + logger.warn({ group: group.name, containerName, err }, 'Graceful stop failed, force killing'); + container.kill('SIGKILL'); + } + }; + + let timeout = setTimeout(killOnTimeout, timeoutMs); + + // Reset the timeout whenever there's activity (streaming output) + const resetTimeout = () => { + clearTimeout(timeout); + timeout = setTimeout(killOnTimeout, timeoutMs); + }; + + container.on('close', (code) => { + clearTimeout(timeout); + const duration = Date.now() - startTime; + + if (timedOut) { + const ts = new Date().toISOString().replace(/[:.]/g, '-'); + const timeoutLog = path.join(logsDir, `container-${ts}.log`); + fs.writeFileSync( + timeoutLog, + [ + `=== Container Run Log (TIMEOUT) ===`, + `Timestamp: ${new Date().toISOString()}`, + `Group: ${group.name}`, + `Container: ${containerName}`, + `Duration: ${duration}ms`, + `Exit Code: ${code}`, + `Had Streaming Output: ${hadStreamingOutput}`, + ].join('\n'), + ); + + // Timeout after output = idle cleanup, not failure. + // The agent already sent its response; this is just the + // container being reaped after the idle period expired. + if (hadStreamingOutput) { + logger.info( + { group: group.name, containerName, duration, code }, + 'Container timed out after output (idle cleanup)', + ); + outputChain.then(() => { + resolve({ + status: 'success', + result: null, + newSessionId, + }); + }); + return; + } + + logger.error({ group: group.name, containerName, duration, code }, 'Container timed out with no output'); + + resolve({ + status: 'error', + result: null, + error: `Container timed out after ${configTimeout}ms`, + }); + return; + } + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const logFile = path.join(logsDir, `container-${timestamp}.log`); + const isVerbose = process.env.LOG_LEVEL === 'debug' || process.env.LOG_LEVEL === 'trace'; + + const logLines = [ + `=== Container Run Log ===`, + `Timestamp: ${new Date().toISOString()}`, + `Group: ${group.name}`, + `IsMain: ${input.isMain}`, + `Duration: ${duration}ms`, + `Exit Code: ${code}`, + `Stdout Truncated: ${stdoutTruncated}`, + `Stderr Truncated: ${stderrTruncated}`, + ``, + ]; + + const isError = code !== 0; + + if (isVerbose || isError) { + // On error, log input metadata only — not the full prompt. + // Full input is only included at verbose level to avoid + // persisting user conversation content on every non-zero exit. + if (isVerbose) { + logLines.push(`=== Input ===`, JSON.stringify(input, null, 2), ``); + } else { + logLines.push( + `=== Input Summary ===`, + `Prompt length: ${input.prompt.length} chars`, + `Session ID: ${input.sessionId || 'new'}`, + ``, + ); + } + logLines.push( + `=== Container Args ===`, + containerArgs.join(' '), + ``, + `=== Mounts ===`, + mounts.map((m) => `${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`).join('\n'), + ``, + `=== Stderr${stderrTruncated ? ' (TRUNCATED)' : ''} ===`, + stderr, + ``, + `=== Stdout${stdoutTruncated ? ' (TRUNCATED)' : ''} ===`, + stdout, + ); + } else { + logLines.push( + `=== Input Summary ===`, + `Prompt length: ${input.prompt.length} chars`, + `Session ID: ${input.sessionId || 'new'}`, + ``, + `=== Mounts ===`, + mounts.map((m) => `${m.containerPath}${m.readonly ? ' (ro)' : ''}`).join('\n'), + ``, + ); + } + + fs.writeFileSync(logFile, logLines.join('\n')); + logger.debug({ logFile, verbose: isVerbose }, 'Container log written'); + + if (code !== 0) { + logger.error( + { + group: group.name, + code, + duration, + stderr, + stdout, + logFile, + }, + 'Container exited with error', + ); + + resolve({ + status: 'error', + result: null, + error: `Container exited with code ${code}: ${stderr.slice(-200)}`, + }); + return; + } + + // Streaming mode: wait for output chain to settle, return completion marker + if (onOutput) { + outputChain.then(() => { + logger.info({ group: group.name, duration, newSessionId }, 'Container completed (streaming mode)'); + resolve({ + status: 'success', + result: null, + newSessionId, + }); + }); + return; + } + + // Legacy mode: parse the last output marker pair from accumulated stdout + try { + // Extract JSON between sentinel markers for robust parsing + const startIdx = stdout.indexOf(OUTPUT_START_MARKER); + const endIdx = stdout.indexOf(OUTPUT_END_MARKER); + + let jsonLine: string; + if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) { + jsonLine = stdout.slice(startIdx + OUTPUT_START_MARKER.length, endIdx).trim(); + } else { + // Fallback: last non-empty line (backwards compatibility) + const lines = stdout.trim().split('\n'); + jsonLine = lines[lines.length - 1]; + } + + const output: ContainerOutput = JSON.parse(jsonLine); + + logger.info( + { + group: group.name, + duration, + status: output.status, + hasResult: !!output.result, + }, + 'Container completed', + ); + + resolve(output); + } catch (err) { + logger.error( + { + group: group.name, + stdout, + stderr, + error: err, + }, + 'Failed to parse container output', + ); + + resolve({ + status: 'error', + result: null, + error: `Failed to parse container output: ${err instanceof Error ? err.message : String(err)}`, + }); + } + }); + + container.on('error', (err) => { + clearTimeout(timeout); + logger.error({ group: group.name, containerName, error: err }, 'Container spawn error'); + resolve({ + status: 'error', + result: null, + error: `Container spawn error: ${err.message}`, + }); + }); + }); +} + +export function writeTasksSnapshot( + groupFolder: string, + isMain: boolean, + tasks: Array<{ + id: string; + groupFolder: string; + prompt: string; + script?: string | null; + schedule_type: string; + schedule_value: string; + status: string; + next_run: string | null; + }>, +): void { + // Write filtered tasks to the group's IPC directory + const groupIpcDir = resolveGroupIpcPath(groupFolder); + fs.mkdirSync(groupIpcDir, { recursive: true }); + + // Main sees all tasks, others only see their own + const filteredTasks = isMain ? tasks : tasks.filter((t) => t.groupFolder === groupFolder); + + const tasksFile = path.join(groupIpcDir, 'current_tasks.json'); + fs.writeFileSync(tasksFile, JSON.stringify(filteredTasks, null, 2)); +} + +export interface AvailableGroup { + jid: string; + name: string; + lastActivity: string; + isRegistered: boolean; +} + +/** + * Write available groups snapshot for the container to read. + * Only main group can see all available groups (for activation). + * Non-main groups only see their own registration status. + */ +export function writeGroupsSnapshot( + groupFolder: string, + isMain: boolean, + groups: AvailableGroup[], + _registeredJids: Set, +): void { + const groupIpcDir = resolveGroupIpcPath(groupFolder); + fs.mkdirSync(groupIpcDir, { recursive: true }); + + // Main sees all groups; others see nothing (they can't activate groups) + const visibleGroups = isMain ? groups : []; + + const groupsFile = path.join(groupIpcDir, 'available_groups.json'); + fs.writeFileSync( + groupsFile, + JSON.stringify( + { + groups: visibleGroups, + lastSync: new Date().toISOString(), + }, + null, + 2, + ), + ); +} diff --git a/src/v1/container-runtime.test.ts b/src/v1/container-runtime.test.ts new file mode 100644 index 0000000..94e14e9 --- /dev/null +++ b/src/v1/container-runtime.test.ts @@ -0,0 +1,147 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Mock logger +vi.mock('./logger.js', () => ({ + logger: { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }, +})); + +// Mock child_process — store the mock fn so tests can configure it +const mockExecSync = vi.fn(); +vi.mock('child_process', () => ({ + execSync: (...args: unknown[]) => mockExecSync(...args), +})); + +import { + CONTAINER_RUNTIME_BIN, + readonlyMountArgs, + stopContainer, + ensureContainerRuntimeRunning, + cleanupOrphans, +} from './container-runtime.js'; +import { logger } from './logger.js'; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +// --- Pure functions --- + +describe('readonlyMountArgs', () => { + it('returns -v flag with :ro suffix', () => { + const args = readonlyMountArgs('/host/path', '/container/path'); + expect(args).toEqual(['-v', '/host/path:/container/path:ro']); + }); +}); + +describe('stopContainer', () => { + it('calls docker stop for valid container names', () => { + stopContainer('nanoclaw-test-123'); + expect(mockExecSync).toHaveBeenCalledWith(`${CONTAINER_RUNTIME_BIN} stop -t 1 nanoclaw-test-123`, { + stdio: 'pipe', + }); + }); + + it('rejects names with shell metacharacters', () => { + expect(() => stopContainer('foo; rm -rf /')).toThrow('Invalid container name'); + expect(() => stopContainer('foo$(whoami)')).toThrow('Invalid container name'); + expect(() => stopContainer('foo`id`')).toThrow('Invalid container name'); + expect(mockExecSync).not.toHaveBeenCalled(); + }); +}); + +// --- ensureContainerRuntimeRunning --- + +describe('ensureContainerRuntimeRunning', () => { + it('does nothing when runtime is already running', () => { + mockExecSync.mockReturnValueOnce(''); + + ensureContainerRuntimeRunning(); + + expect(mockExecSync).toHaveBeenCalledTimes(1); + expect(mockExecSync).toHaveBeenCalledWith(`${CONTAINER_RUNTIME_BIN} info`, { + stdio: 'pipe', + timeout: 10000, + }); + expect(logger.debug).toHaveBeenCalledWith('Container runtime already running'); + }); + + it('throws when docker info fails', () => { + mockExecSync.mockImplementationOnce(() => { + throw new Error('Cannot connect to the Docker daemon'); + }); + + expect(() => ensureContainerRuntimeRunning()).toThrow('Container runtime is required but failed to start'); + expect(logger.error).toHaveBeenCalled(); + }); +}); + +// --- cleanupOrphans --- + +describe('cleanupOrphans', () => { + it('stops orphaned nanoclaw containers', () => { + // docker ps returns container names, one per line + mockExecSync.mockReturnValueOnce('nanoclaw-group1-111\nnanoclaw-group2-222\n'); + // stop calls succeed + mockExecSync.mockReturnValue(''); + + cleanupOrphans(); + + // ps + 2 stop calls + expect(mockExecSync).toHaveBeenCalledTimes(3); + expect(mockExecSync).toHaveBeenNthCalledWith(2, `${CONTAINER_RUNTIME_BIN} stop -t 1 nanoclaw-group1-111`, { + stdio: 'pipe', + }); + expect(mockExecSync).toHaveBeenNthCalledWith(3, `${CONTAINER_RUNTIME_BIN} stop -t 1 nanoclaw-group2-222`, { + stdio: 'pipe', + }); + expect(logger.info).toHaveBeenCalledWith( + { count: 2, names: ['nanoclaw-group1-111', 'nanoclaw-group2-222'] }, + 'Stopped orphaned containers', + ); + }); + + it('does nothing when no orphans exist', () => { + mockExecSync.mockReturnValueOnce(''); + + cleanupOrphans(); + + expect(mockExecSync).toHaveBeenCalledTimes(1); + expect(logger.info).not.toHaveBeenCalled(); + }); + + it('warns and continues when ps fails', () => { + mockExecSync.mockImplementationOnce(() => { + throw new Error('docker not available'); + }); + + cleanupOrphans(); // should not throw + + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ err: expect.any(Error) }), + 'Failed to clean up orphaned containers', + ); + }); + + it('continues stopping remaining containers when one stop fails', () => { + mockExecSync.mockReturnValueOnce('nanoclaw-a-1\nnanoclaw-b-2\n'); + // First stop fails + mockExecSync.mockImplementationOnce(() => { + throw new Error('already stopped'); + }); + // Second stop succeeds + mockExecSync.mockReturnValueOnce(''); + + cleanupOrphans(); // should not throw + + expect(mockExecSync).toHaveBeenCalledTimes(3); + expect(logger.info).toHaveBeenCalledWith( + { count: 2, names: ['nanoclaw-a-1', 'nanoclaw-b-2'] }, + 'Stopped orphaned containers', + ); + }); +}); diff --git a/src/v1/container-runtime.ts b/src/v1/container-runtime.ts new file mode 100644 index 0000000..678a708 --- /dev/null +++ b/src/v1/container-runtime.ts @@ -0,0 +1,80 @@ +/** + * Container runtime abstraction for NanoClaw. + * All runtime-specific logic lives here so swapping runtimes means changing one file. + */ +import { execSync } from 'child_process'; +import os from 'os'; + +import { logger } from './logger.js'; + +/** The container runtime binary name. */ +export const CONTAINER_RUNTIME_BIN = 'docker'; + +/** CLI args needed for the container to resolve the host gateway. */ +export function hostGatewayArgs(): string[] { + // On Linux, host.docker.internal isn't built-in — add it explicitly + if (os.platform() === 'linux') { + return ['--add-host=host.docker.internal:host-gateway']; + } + return []; +} + +/** Returns CLI args for a readonly bind mount. */ +export function readonlyMountArgs(hostPath: string, containerPath: string): string[] { + return ['-v', `${hostPath}:${containerPath}:ro`]; +} + +/** Stop a container by name. Uses execFileSync to avoid shell injection. */ +export function stopContainer(name: string): void { + if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(name)) { + throw new Error(`Invalid container name: ${name}`); + } + execSync(`${CONTAINER_RUNTIME_BIN} stop -t 1 ${name}`, { stdio: 'pipe' }); +} + +/** Ensure the container runtime is running, starting it if needed. */ +export function ensureContainerRuntimeRunning(): void { + try { + execSync(`${CONTAINER_RUNTIME_BIN} info`, { + stdio: 'pipe', + timeout: 10000, + }); + logger.debug('Container runtime already running'); + } catch (err) { + logger.error({ err }, 'Failed to reach container runtime'); + console.error('\n╔════════════════════════════════════════════════════════════════╗'); + console.error('║ FATAL: Container runtime failed to start ║'); + console.error('║ ║'); + console.error('║ Agents cannot run without a container runtime. To fix: ║'); + console.error('║ 1. Ensure Docker is installed and running ║'); + console.error('║ 2. Run: docker info ║'); + console.error('║ 3. Restart NanoClaw ║'); + console.error('╚════════════════════════════════════════════════════════════════╝\n'); + throw new Error('Container runtime is required but failed to start', { + cause: err, + }); + } +} + +/** Kill orphaned NanoClaw containers from previous runs. */ +export function cleanupOrphans(): void { + try { + const output = execSync(`${CONTAINER_RUNTIME_BIN} ps --filter name=nanoclaw- --format '{{.Names}}'`, { + stdio: ['pipe', 'pipe', 'pipe'], + encoding: 'utf-8', + }); + const orphans = output.trim().split('\n').filter(Boolean); + for (const name of orphans) { + try { + stopContainer(name); + } 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'); + } +} diff --git a/src/db-migration.test.ts b/src/v1/db-migration.test.ts similarity index 100% rename from src/db-migration.test.ts rename to src/v1/db-migration.test.ts diff --git a/src/db.test.ts b/src/v1/db.test.ts similarity index 100% rename from src/db.test.ts rename to src/v1/db.test.ts diff --git a/src/db.ts b/src/v1/db.ts similarity index 100% rename from src/db.ts rename to src/v1/db.ts diff --git a/src/v1/env.ts b/src/v1/env.ts new file mode 100644 index 0000000..064e6f8 --- /dev/null +++ b/src/v1/env.ts @@ -0,0 +1,42 @@ +import fs from 'fs'; +import path from 'path'; +import { logger } from './logger.js'; + +/** + * Parse the .env file and return values for the requested keys. + * Does NOT load anything into process.env — callers decide what to + * do with the values. This keeps secrets out of the process environment + * so they don't leak to child processes. + */ +export function readEnvFile(keys: string[]): Record { + const envFile = path.join(process.cwd(), '.env'); + let content: string; + try { + content = fs.readFileSync(envFile, 'utf-8'); + } catch (err) { + logger.debug({ err }, '.env file not found, using defaults'); + return {}; + } + + const result: Record = {}; + const wanted = new Set(keys); + + for (const line of content.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eqIdx = trimmed.indexOf('='); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + if (!wanted.has(key)) continue; + let value = trimmed.slice(eqIdx + 1).trim(); + if ( + value.length >= 2 && + ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) + ) { + value = value.slice(1, -1); + } + if (value) result[key] = value; + } + + return result; +} diff --git a/src/formatting.test.ts b/src/v1/formatting.test.ts similarity index 100% rename from src/formatting.test.ts rename to src/v1/formatting.test.ts diff --git a/src/v1/group-folder.test.ts b/src/v1/group-folder.test.ts new file mode 100644 index 0000000..cc77210 --- /dev/null +++ b/src/v1/group-folder.test.ts @@ -0,0 +1,35 @@ +import path from 'path'; + +import { describe, expect, it } from 'vitest'; + +import { isValidGroupFolder, resolveGroupFolderPath, resolveGroupIpcPath } from './group-folder.js'; + +describe('group folder validation', () => { + it('accepts normal group folder names', () => { + expect(isValidGroupFolder('main')).toBe(true); + expect(isValidGroupFolder('family-chat')).toBe(true); + expect(isValidGroupFolder('Team_42')).toBe(true); + }); + + it('rejects traversal and reserved names', () => { + expect(isValidGroupFolder('../../etc')).toBe(false); + expect(isValidGroupFolder('/tmp')).toBe(false); + expect(isValidGroupFolder('global')).toBe(false); + expect(isValidGroupFolder('')).toBe(false); + }); + + it('resolves safe paths under groups directory', () => { + const resolved = resolveGroupFolderPath('family-chat'); + expect(resolved.endsWith(`${path.sep}groups${path.sep}family-chat`)).toBe(true); + }); + + it('resolves safe paths under data ipc directory', () => { + const resolved = resolveGroupIpcPath('family-chat'); + expect(resolved.endsWith(`${path.sep}data${path.sep}ipc${path.sep}family-chat`)).toBe(true); + }); + + it('throws for unsafe folder names', () => { + expect(() => resolveGroupFolderPath('../../etc')).toThrow(); + expect(() => resolveGroupIpcPath('/tmp')).toThrow(); + }); +}); diff --git a/src/v1/group-folder.ts b/src/v1/group-folder.ts new file mode 100644 index 0000000..5745954 --- /dev/null +++ b/src/v1/group-folder.ts @@ -0,0 +1,44 @@ +import path from 'path'; + +import { DATA_DIR, GROUPS_DIR } from './config.js'; + +const GROUP_FOLDER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/; +const RESERVED_FOLDERS = new Set(['global']); + +export function isValidGroupFolder(folder: string): boolean { + if (!folder) return false; + if (folder !== folder.trim()) return false; + if (!GROUP_FOLDER_PATTERN.test(folder)) return false; + if (folder.includes('/') || folder.includes('\\')) return false; + if (folder.includes('..')) return false; + if (RESERVED_FOLDERS.has(folder.toLowerCase())) return false; + return true; +} + +export function assertValidGroupFolder(folder: string): void { + if (!isValidGroupFolder(folder)) { + throw new Error(`Invalid group folder "${folder}"`); + } +} + +function ensureWithinBase(baseDir: string, resolvedPath: string): void { + const rel = path.relative(baseDir, resolvedPath); + if (rel.startsWith('..') || path.isAbsolute(rel)) { + throw new Error(`Path escapes base directory: ${resolvedPath}`); + } +} + +export function resolveGroupFolderPath(folder: string): string { + assertValidGroupFolder(folder); + const groupPath = path.resolve(GROUPS_DIR, folder); + ensureWithinBase(GROUPS_DIR, groupPath); + return groupPath; +} + +export function resolveGroupIpcPath(folder: string): string { + assertValidGroupFolder(folder); + const ipcBaseDir = path.resolve(DATA_DIR, 'ipc'); + const ipcPath = path.resolve(ipcBaseDir, folder); + ensureWithinBase(ipcBaseDir, ipcPath); + return ipcPath; +} diff --git a/src/group-queue.test.ts b/src/v1/group-queue.test.ts similarity index 100% rename from src/group-queue.test.ts rename to src/v1/group-queue.test.ts diff --git a/src/group-queue.ts b/src/v1/group-queue.ts similarity index 100% rename from src/group-queue.ts rename to src/v1/group-queue.ts diff --git a/src/v1/index.ts b/src/v1/index.ts new file mode 100644 index 0000000..ded6b94 --- /dev/null +++ b/src/v1/index.ts @@ -0,0 +1,647 @@ +import fs from 'fs'; +import path from 'path'; + +import { OneCLI } from '@onecli-sh/sdk'; + +import { + ASSISTANT_NAME, + DEFAULT_TRIGGER, + getTriggerPattern, + GROUPS_DIR, + IDLE_TIMEOUT, + MAX_MESSAGES_PER_PROMPT, + ONECLI_URL, + POLL_INTERVAL, + TIMEZONE, +} from './config.js'; +import './channels/index.js'; +import { getChannelFactory, getRegisteredChannelNames } from './channels/registry.js'; +import { ContainerOutput, runContainerAgent, writeGroupsSnapshot, writeTasksSnapshot } from './container-runner.js'; +import { cleanupOrphans, ensureContainerRuntimeRunning } from './container-runtime.js'; +import { + getAllChats, + getAllRegisteredGroups, + getAllSessions, + deleteSession, + getAllTasks, + getLastBotMessageTimestamp, + getMessagesSince, + getNewMessages, + getRouterState, + initDatabase, + setRegisteredGroup, + setRouterState, + setSession, + storeChatMetadata, + storeMessage, +} from './db.js'; +import { GroupQueue } from './group-queue.js'; +import { resolveGroupFolderPath } from './group-folder.js'; +import { startIpcWatcher } from './ipc.js'; +import { findChannel, formatMessages, formatOutbound } from './router.js'; +import { restoreRemoteControl, startRemoteControl, stopRemoteControl } from './remote-control.js'; +import { isSenderAllowed, isTriggerAllowed, loadSenderAllowlist, shouldDropMessage } from './sender-allowlist.js'; +import { startSessionCleanup } from './session-cleanup.js'; +import { startSchedulerLoop } from './task-scheduler.js'; +import { Channel, NewMessage, RegisteredGroup } from './types.js'; +import { logger } from './logger.js'; + +// Re-export for backwards compatibility during refactor +export { escapeXml, formatMessages } from './router.js'; + +let lastTimestamp = ''; +let sessions: Record = {}; +let registeredGroups: Record = {}; +let lastAgentTimestamp: Record = {}; +let messageLoopRunning = false; + +const channels: Channel[] = []; +const queue = new GroupQueue(); + +const onecli = new OneCLI({ url: ONECLI_URL }); + +function ensureOneCLIAgent(jid: string, group: RegisteredGroup): void { + if (group.isMain) return; + const identifier = group.folder.toLowerCase().replace(/_/g, '-'); + onecli.ensureAgent({ name: group.name, identifier }).then( + (res) => { + logger.info({ jid, identifier, created: res.created }, 'OneCLI agent ensured'); + }, + (err) => { + logger.debug({ jid, identifier, err: String(err) }, 'OneCLI agent ensure skipped'); + }, + ); +} + +function loadState(): void { + lastTimestamp = getRouterState('last_timestamp') || ''; + const agentTs = getRouterState('last_agent_timestamp'); + try { + lastAgentTimestamp = agentTs ? JSON.parse(agentTs) : {}; + } catch { + logger.warn('Corrupted last_agent_timestamp in DB, resetting'); + lastAgentTimestamp = {}; + } + sessions = getAllSessions(); + registeredGroups = getAllRegisteredGroups(); + logger.info({ groupCount: Object.keys(registeredGroups).length }, 'State loaded'); +} + +/** + * Return the message cursor for a group, recovering from the last bot reply + * if lastAgentTimestamp is missing (new group, corrupted state, restart). + */ +function getOrRecoverCursor(chatJid: string): string { + const existing = lastAgentTimestamp[chatJid]; + if (existing) return existing; + + const botTs = getLastBotMessageTimestamp(chatJid, ASSISTANT_NAME); + if (botTs) { + logger.info({ chatJid, recoveredFrom: botTs }, 'Recovered message cursor from last bot reply'); + lastAgentTimestamp[chatJid] = botTs; + saveState(); + return botTs; + } + return ''; +} + +function saveState(): void { + setRouterState('last_timestamp', lastTimestamp); + setRouterState('last_agent_timestamp', JSON.stringify(lastAgentTimestamp)); +} + +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; + setRegisteredGroup(jid, group); + + // Create group folder + fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true }); + + // Copy CLAUDE.md template into the new group folder so agents have + // identity and instructions from the first run. (Fixes #1391) + const groupMdFile = path.join(groupDir, 'CLAUDE.md'); + if (!fs.existsSync(groupMdFile)) { + const templateFile = path.join(GROUPS_DIR, group.isMain ? 'main' : 'global', 'CLAUDE.md'); + if (fs.existsSync(templateFile)) { + let content = fs.readFileSync(templateFile, 'utf-8'); + if (ASSISTANT_NAME !== 'Andy') { + content = content.replace(/^# Andy$/m, `# ${ASSISTANT_NAME}`); + content = content.replace(/You are Andy/g, `You are ${ASSISTANT_NAME}`); + } + fs.writeFileSync(groupMdFile, content); + logger.info({ folder: group.folder }, 'Created CLAUDE.md from template'); + } + } + + // Ensure a corresponding OneCLI agent exists (best-effort, non-blocking) + ensureOneCLIAgent(jid, group); + + logger.info({ jid, name: group.name, folder: group.folder }, 'Group registered'); +} + +/** + * Get available groups list for the agent. + * Returns groups ordered by most recent activity. + */ +export function getAvailableGroups(): import('./container-runner.js').AvailableGroup[] { + const chats = getAllChats(); + const registeredJids = new Set(Object.keys(registeredGroups)); + + return chats + .filter((c) => c.jid !== '__group_sync__' && c.is_group) + .map((c) => ({ + jid: c.jid, + name: c.name, + lastActivity: c.last_message_time, + isRegistered: registeredJids.has(c.jid), + })); +} + +/** @internal - exported for testing */ +export function _setRegisteredGroups(groups: Record): void { + registeredGroups = groups; +} + +/** + * Process all pending messages for a group. + * Called by the GroupQueue when it's this group's turn. + */ +async function processGroupMessages(chatJid: string): Promise { + const group = registeredGroups[chatJid]; + if (!group) return true; + + const channel = findChannel(channels, chatJid); + if (!channel) { + logger.warn({ chatJid }, 'No channel owns JID, skipping messages'); + return true; + } + + const isMainGroup = group.isMain === true; + + const missedMessages = getMessagesSince( + chatJid, + getOrRecoverCursor(chatJid), + ASSISTANT_NAME, + MAX_MESSAGES_PER_PROMPT, + ); + + if (missedMessages.length === 0) return true; + + // For non-main groups, check if trigger is required and present + if (!isMainGroup && group.requiresTrigger !== false) { + const triggerPattern = getTriggerPattern(group.trigger); + const allowlistCfg = loadSenderAllowlist(); + const hasTrigger = missedMessages.some( + (m) => + triggerPattern.test(m.content.trim()) && (m.is_from_me || isTriggerAllowed(chatJid, m.sender, allowlistCfg)), + ); + if (!hasTrigger) return true; + } + + const prompt = formatMessages(missedMessages, TIMEZONE); + + // Advance cursor so the piping path in startMessageLoop won't re-fetch + // these messages. Save the old cursor so we can roll back on error. + const previousCursor = lastAgentTimestamp[chatJid] || ''; + lastAgentTimestamp[chatJid] = missedMessages[missedMessages.length - 1].timestamp; + saveState(); + + logger.info({ group: group.name, messageCount: missedMessages.length }, 'Processing messages'); + + // Track idle timer for closing stdin when agent is idle + let idleTimer: ReturnType | null = null; + + const resetIdleTimer = () => { + if (idleTimer) clearTimeout(idleTimer); + idleTimer = setTimeout(() => { + logger.debug({ group: group.name }, 'Idle timeout, closing container stdin'); + queue.closeStdin(chatJid); + }, IDLE_TIMEOUT); + }; + + await channel.setTyping?.(chatJid, true); + let hadError = false; + let outputSentToUser = false; + + const output = await runAgent(group, prompt, chatJid, async (result) => { + // Streaming output callback — called for each agent result + if (result.result) { + const raw = typeof result.result === 'string' ? result.result : JSON.stringify(result.result); + // Strip ... blocks — agent uses these for internal reasoning + const text = raw.replace(/[\s\S]*?<\/internal>/g, '').trim(); + logger.info({ group: group.name }, `Agent output: ${raw.length} chars`); + if (text) { + await channel.sendMessage(chatJid, text); + outputSentToUser = true; + } + // Only reset idle timer on actual results, not session-update markers (result: null) + resetIdleTimer(); + } + + if (result.status === 'success') { + queue.notifyIdle(chatJid); + } + + if (result.status === 'error') { + hadError = true; + } + }); + + await channel.setTyping?.(chatJid, false); + if (idleTimer) clearTimeout(idleTimer); + + if (output === 'error' || hadError) { + // If we already sent output to the user, don't roll back the cursor — + // the user got their response and re-processing would send duplicates. + if (outputSentToUser) { + logger.warn( + { group: group.name }, + 'Agent error after output was sent, skipping cursor rollback to prevent duplicates', + ); + return true; + } + // Roll back cursor so retries can re-process these messages + lastAgentTimestamp[chatJid] = previousCursor; + saveState(); + logger.warn({ group: group.name }, 'Agent error, rolled back message cursor for retry'); + return false; + } + + return true; +} + +async function runAgent( + group: RegisteredGroup, + prompt: string, + chatJid: string, + onOutput?: (output: ContainerOutput) => Promise, +): Promise<'success' | 'error'> { + const isMain = group.isMain === true; + const sessionId = sessions[group.folder]; + + // Update tasks snapshot for container to read (filtered by group) + const tasks = getAllTasks(); + writeTasksSnapshot( + group.folder, + isMain, + tasks.map((t) => ({ + id: t.id, + groupFolder: t.group_folder, + prompt: t.prompt, + script: t.script || undefined, + schedule_type: t.schedule_type, + schedule_value: t.schedule_value, + status: t.status, + next_run: t.next_run, + })), + ); + + // Update available groups snapshot (main group only can see all groups) + const availableGroups = getAvailableGroups(); + writeGroupsSnapshot(group.folder, isMain, availableGroups, new Set(Object.keys(registeredGroups))); + + // Wrap onOutput to track session ID from streamed results + const wrappedOnOutput = onOutput + ? async (output: ContainerOutput) => { + if (output.newSessionId) { + sessions[group.folder] = output.newSessionId; + setSession(group.folder, output.newSessionId); + } + await onOutput(output); + } + : undefined; + + try { + const output = await runContainerAgent( + group, + { + prompt, + sessionId, + groupFolder: group.folder, + chatJid, + isMain, + assistantName: ASSISTANT_NAME, + }, + (proc, containerName) => queue.registerProcess(chatJid, proc, containerName, group.folder), + wrappedOnOutput, + ); + + if (output.newSessionId) { + sessions[group.folder] = output.newSessionId; + setSession(group.folder, output.newSessionId); + } + + if (output.status === 'error') { + // Detect stale/corrupt session — clear it so the next retry starts fresh. + // The session .jsonl can go missing after a crash mid-write, manual + // deletion, or disk-full. The existing backoff in group-queue.ts + // handles the retry; we just need to remove the broken session ID. + const isStaleSession = + sessionId && output.error && /no conversation found|ENOENT.*\.jsonl|session.*not found/i.test(output.error); + + if (isStaleSession) { + logger.warn( + { group: group.name, staleSessionId: sessionId, error: output.error }, + 'Stale session detected — clearing for next retry', + ); + delete sessions[group.folder]; + deleteSession(group.folder); + } + + logger.error({ group: group.name, error: output.error }, 'Container agent error'); + return 'error'; + } + + return 'success'; + } catch (err) { + logger.error({ group: group.name, err }, 'Agent error'); + return 'error'; + } +} + +async function startMessageLoop(): Promise { + if (messageLoopRunning) { + logger.debug('Message loop already running, skipping duplicate start'); + return; + } + messageLoopRunning = true; + + logger.info(`NanoClaw running (default trigger: ${DEFAULT_TRIGGER})`); + + while (true) { + try { + const jids = Object.keys(registeredGroups); + const { messages, newTimestamp } = getNewMessages(jids, lastTimestamp, ASSISTANT_NAME); + + if (messages.length > 0) { + logger.info({ count: messages.length }, 'New messages'); + + // Advance the "seen" cursor for all messages immediately + lastTimestamp = newTimestamp; + saveState(); + + // Deduplicate by group + const messagesByGroup = new Map(); + for (const msg of messages) { + const existing = messagesByGroup.get(msg.chat_jid); + if (existing) { + existing.push(msg); + } else { + messagesByGroup.set(msg.chat_jid, [msg]); + } + } + + for (const [chatJid, groupMessages] of messagesByGroup) { + const group = registeredGroups[chatJid]; + if (!group) continue; + + const channel = findChannel(channels, chatJid); + if (!channel) { + logger.warn({ chatJid }, 'No channel owns JID, skipping messages'); + continue; + } + + const isMainGroup = group.isMain === true; + const needsTrigger = !isMainGroup && group.requiresTrigger !== false; + + // For non-main groups, only act on trigger messages. + // Non-trigger messages accumulate in DB and get pulled as + // context when a trigger eventually arrives. + if (needsTrigger) { + const triggerPattern = getTriggerPattern(group.trigger); + const allowlistCfg = loadSenderAllowlist(); + const hasTrigger = groupMessages.some( + (m) => + triggerPattern.test(m.content.trim()) && + (m.is_from_me || isTriggerAllowed(chatJid, m.sender, allowlistCfg)), + ); + if (!hasTrigger) continue; + } + + // Pull all messages since lastAgentTimestamp so non-trigger + // context that accumulated between triggers is included. + const allPending = getMessagesSince( + chatJid, + getOrRecoverCursor(chatJid), + ASSISTANT_NAME, + MAX_MESSAGES_PER_PROMPT, + ); + const messagesToSend = allPending.length > 0 ? allPending : groupMessages; + const formatted = formatMessages(messagesToSend, TIMEZONE); + + if (queue.sendMessage(chatJid, formatted)) { + logger.debug({ chatJid, count: messagesToSend.length }, 'Piped messages to active container'); + lastAgentTimestamp[chatJid] = messagesToSend[messagesToSend.length - 1].timestamp; + saveState(); + // Show typing indicator while the container processes the piped message + channel + .setTyping?.(chatJid, true) + ?.catch((err) => logger.warn({ chatJid, err }, 'Failed to set typing indicator')); + } else { + // No active container — enqueue for a new one + queue.enqueueMessageCheck(chatJid); + } + } + } + } catch (err) { + logger.error({ err }, 'Error in message loop'); + } + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL)); + } +} + +/** + * Startup recovery: check for unprocessed messages in registered groups. + * Handles crash between advancing lastTimestamp and processing messages. + */ +function recoverPendingMessages(): void { + for (const [chatJid, group] of Object.entries(registeredGroups)) { + const pending = getMessagesSince(chatJid, getOrRecoverCursor(chatJid), ASSISTANT_NAME, MAX_MESSAGES_PER_PROMPT); + if (pending.length > 0) { + logger.info({ group: group.name, pendingCount: pending.length }, 'Recovery: found unprocessed messages'); + queue.enqueueMessageCheck(chatJid); + } + } +} + +function ensureContainerSystemRunning(): void { + ensureContainerRuntimeRunning(); + cleanupOrphans(); +} + +async function main(): Promise { + ensureContainerSystemRunning(); + initDatabase(); + logger.info('Database initialized'); + loadState(); + + // Ensure OneCLI agents exist for all registered groups. + // Recovers from missed creates (e.g. OneCLI was down at registration time). + for (const [jid, group] of Object.entries(registeredGroups)) { + ensureOneCLIAgent(jid, group); + } + + restoreRemoteControl(); + + // Graceful shutdown handlers + const shutdown = async (signal: string) => { + logger.info({ signal }, 'Shutdown signal received'); + await queue.shutdown(10000); + for (const ch of channels) await ch.disconnect(); + process.exit(0); + }; + process.on('SIGTERM', () => shutdown('SIGTERM')); + process.on('SIGINT', () => shutdown('SIGINT')); + + // Handle /remote-control and /remote-control-end commands + async function handleRemoteControl(command: string, chatJid: string, msg: NewMessage): Promise { + const group = registeredGroups[chatJid]; + if (!group?.isMain) { + logger.warn({ chatJid, sender: msg.sender }, 'Remote control rejected: not main group'); + return; + } + + const channel = findChannel(channels, chatJid); + if (!channel) return; + + if (command === '/remote-control') { + const result = await startRemoteControl(msg.sender, chatJid, process.cwd()); + if (result.ok) { + await channel.sendMessage(chatJid, result.url); + } else { + await channel.sendMessage(chatJid, `Remote Control failed: ${result.error}`); + } + } else { + const result = stopRemoteControl(); + if (result.ok) { + await channel.sendMessage(chatJid, 'Remote Control session ended.'); + } else { + await channel.sendMessage(chatJid, result.error); + } + } + } + + // Channel callbacks (shared by all channels) + const channelOpts = { + onMessage: (chatJid: string, msg: NewMessage) => { + // Remote control commands — intercept before storage + const trimmed = msg.content.trim(); + if (trimmed === '/remote-control' || trimmed === '/remote-control-end') { + handleRemoteControl(trimmed, chatJid, msg).catch((err) => + logger.error({ err, chatJid }, 'Remote control command error'), + ); + return; + } + + // Sender allowlist drop mode: discard messages from denied senders before storing + if (!msg.is_from_me && !msg.is_bot_message && registeredGroups[chatJid]) { + const cfg = loadSenderAllowlist(); + if (shouldDropMessage(chatJid, cfg) && !isSenderAllowed(chatJid, msg.sender, cfg)) { + if (cfg.logDenied) { + logger.debug({ chatJid, sender: msg.sender }, 'sender-allowlist: dropping message (drop mode)'); + } + return; + } + } + storeMessage(msg); + }, + onChatMetadata: (chatJid: string, timestamp: string, name?: string, channel?: string, isGroup?: boolean) => + storeChatMetadata(chatJid, timestamp, name, channel, isGroup), + registeredGroups: () => registeredGroups, + }; + + // Create and connect all registered channels. + // Each channel self-registers via the barrel import above. + // Factories return null when credentials are missing, so unconfigured channels are skipped. + for (const channelName of getRegisteredChannelNames()) { + const factory = getChannelFactory(channelName)!; + const channel = factory(channelOpts); + if (!channel) { + logger.warn( + { channel: channelName }, + 'Channel installed but credentials missing — skipping. Check .env or re-run the channel skill.', + ); + continue; + } + channels.push(channel); + await channel.connect(); + } + if (channels.length === 0) { + logger.fatal('No channels connected'); + process.exit(1); + } + + // Start subsystems (independently of connection handler) + startSchedulerLoop({ + registeredGroups: () => registeredGroups, + getSessions: () => sessions, + queue, + onProcess: (groupJid, proc, containerName, groupFolder) => + queue.registerProcess(groupJid, proc, containerName, groupFolder), + sendMessage: async (jid, rawText) => { + const channel = findChannel(channels, jid); + if (!channel) { + logger.warn({ jid }, 'No channel owns JID, cannot send message'); + return; + } + const text = formatOutbound(rawText); + if (text) await channel.sendMessage(jid, text); + }, + }); + startIpcWatcher({ + sendMessage: (jid, text) => { + const channel = findChannel(channels, jid); + if (!channel) throw new Error(`No channel for JID: ${jid}`); + return channel.sendMessage(jid, text); + }, + registeredGroups: () => registeredGroups, + registerGroup, + syncGroups: async (force: boolean) => { + await Promise.all(channels.filter((ch) => ch.syncGroups).map((ch) => ch.syncGroups!(force))); + }, + getAvailableGroups, + writeGroupsSnapshot: (gf, im, ag, rj) => writeGroupsSnapshot(gf, im, ag, rj), + onTasksChanged: () => { + const tasks = getAllTasks(); + const taskRows = tasks.map((t) => ({ + id: t.id, + groupFolder: t.group_folder, + prompt: t.prompt, + script: t.script || undefined, + schedule_type: t.schedule_type, + schedule_value: t.schedule_value, + status: t.status, + next_run: t.next_run, + })); + for (const group of Object.values(registeredGroups)) { + writeTasksSnapshot(group.folder, group.isMain === true, taskRows); + } + }, + }); + startSessionCleanup(); + queue.setProcessMessagesFn(processGroupMessages); + recoverPendingMessages(); + startMessageLoop().catch((err) => { + logger.fatal({ err }, 'Message loop crashed unexpectedly'); + process.exit(1); + }); +} + +// Guard: only run when executed directly, not when imported by tests +const isDirectRun = + process.argv[1] && new URL(import.meta.url).pathname === new URL(`file://${process.argv[1]}`).pathname; + +if (isDirectRun) { + main().catch((err) => { + logger.error({ err }, 'Failed to start NanoClaw'); + process.exit(1); + }); +} diff --git a/src/ipc-auth.test.ts b/src/v1/ipc-auth.test.ts similarity index 100% rename from src/ipc-auth.test.ts rename to src/v1/ipc-auth.test.ts diff --git a/src/ipc.ts b/src/v1/ipc.ts similarity index 100% rename from src/ipc.ts rename to src/v1/ipc.ts diff --git a/src/logger.ts b/src/v1/logger.ts similarity index 100% rename from src/logger.ts rename to src/v1/logger.ts diff --git a/src/v1/mount-security.ts b/src/v1/mount-security.ts new file mode 100644 index 0000000..c44620c --- /dev/null +++ b/src/v1/mount-security.ts @@ -0,0 +1,405 @@ +/** + * Mount Security Module for NanoClaw + * + * Validates additional mounts against an allowlist stored OUTSIDE the project root. + * This prevents container agents from modifying security configuration. + * + * Allowlist location: ~/.config/nanoclaw/mount-allowlist.json + */ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { MOUNT_ALLOWLIST_PATH } from './config.js'; +import { logger } from './logger.js'; +import { AdditionalMount, AllowedRoot, MountAllowlist } from './types.js'; + +// Cache the allowlist in memory - only reloads on process restart +let cachedAllowlist: MountAllowlist | null = null; +let allowlistLoadError: string | null = null; + +/** + * Default blocked patterns - paths that should never be mounted + */ +const DEFAULT_BLOCKED_PATTERNS = [ + '.ssh', + '.gnupg', + '.gpg', + '.aws', + '.azure', + '.gcloud', + '.kube', + '.docker', + 'credentials', + '.env', + '.netrc', + '.npmrc', + '.pypirc', + 'id_rsa', + 'id_ed25519', + 'private_key', + '.secret', +]; + +/** + * Load the mount allowlist from the external config location. + * Returns null if the file doesn't exist or is invalid. + * Result is cached in memory for the lifetime of the process. + */ +export function loadMountAllowlist(): MountAllowlist | null { + if (cachedAllowlist !== null) { + return cachedAllowlist; + } + + if (allowlistLoadError !== null) { + // Already tried and failed, don't spam logs + return null; + } + + try { + if (!fs.existsSync(MOUNT_ALLOWLIST_PATH)) { + // Do NOT cache this as an error — file may be created later without restart. + // Only parse/structural errors are permanently cached. + logger.warn( + { path: MOUNT_ALLOWLIST_PATH }, + 'Mount allowlist not found - additional mounts will be BLOCKED. ' + + 'Create the file to enable additional mounts.', + ); + return null; + } + + const content = fs.readFileSync(MOUNT_ALLOWLIST_PATH, 'utf-8'); + const allowlist = JSON.parse(content) as MountAllowlist; + + // Validate structure + if (!Array.isArray(allowlist.allowedRoots)) { + throw new Error('allowedRoots must be an array'); + } + + if (!Array.isArray(allowlist.blockedPatterns)) { + throw new Error('blockedPatterns must be an array'); + } + + if (typeof allowlist.nonMainReadOnly !== 'boolean') { + throw new Error('nonMainReadOnly must be a boolean'); + } + + // Merge with default blocked patterns + const mergedBlockedPatterns = [...new Set([...DEFAULT_BLOCKED_PATTERNS, ...allowlist.blockedPatterns])]; + allowlist.blockedPatterns = mergedBlockedPatterns; + + cachedAllowlist = allowlist; + logger.info( + { + path: MOUNT_ALLOWLIST_PATH, + allowedRoots: allowlist.allowedRoots.length, + blockedPatterns: allowlist.blockedPatterns.length, + }, + 'Mount allowlist loaded successfully', + ); + + return cachedAllowlist; + } catch (err) { + allowlistLoadError = err instanceof Error ? err.message : String(err); + logger.error( + { + path: MOUNT_ALLOWLIST_PATH, + error: allowlistLoadError, + }, + 'Failed to load mount allowlist - additional mounts will be BLOCKED', + ); + return null; + } +} + +/** + * Expand ~ to home directory and resolve to absolute path + */ +function expandPath(p: string): string { + const homeDir = process.env.HOME || os.homedir(); + if (p.startsWith('~/')) { + return path.join(homeDir, p.slice(2)); + } + if (p === '~') { + return homeDir; + } + return path.resolve(p); +} + +/** + * Get the real path, resolving symlinks. + * Returns null if the path doesn't exist. + */ +function getRealPath(p: string): string | null { + try { + return fs.realpathSync(p); + } catch { + return null; + } +} + +/** + * Check if a path matches any blocked pattern + */ +function matchesBlockedPattern(realPath: string, blockedPatterns: string[]): string | null { + const pathParts = realPath.split(path.sep); + + for (const pattern of blockedPatterns) { + // Check if any path component matches the pattern + for (const part of pathParts) { + if (part === pattern || part.includes(pattern)) { + return pattern; + } + } + + // Also check if the full path contains the pattern + if (realPath.includes(pattern)) { + return pattern; + } + } + + return null; +} + +/** + * Check if a real path is under an allowed root + */ +function findAllowedRoot(realPath: string, allowedRoots: AllowedRoot[]): AllowedRoot | null { + for (const root of allowedRoots) { + const expandedRoot = expandPath(root.path); + const realRoot = getRealPath(expandedRoot); + + if (realRoot === null) { + // Allowed root doesn't exist, skip it + continue; + } + + // Check if realPath is under realRoot + const relative = path.relative(realRoot, realPath); + if (!relative.startsWith('..') && !path.isAbsolute(relative)) { + return root; + } + } + + return null; +} + +/** + * Validate the container path to prevent escaping /workspace/extra/ + */ +function isValidContainerPath(containerPath: string): boolean { + // Must not contain .. to prevent path traversal + if (containerPath.includes('..')) { + return false; + } + + // Must not be absolute (it will be prefixed with /workspace/extra/) + if (containerPath.startsWith('/')) { + return false; + } + + // Must not be empty + if (!containerPath || containerPath.trim() === '') { + return false; + } + + // Must not contain colons — prevents Docker -v option injection (e.g., "repo:rw") + if (containerPath.includes(':')) { + return false; + } + + return true; +} + +export interface MountValidationResult { + allowed: boolean; + reason: string; + realHostPath?: string; + resolvedContainerPath?: string; + effectiveReadonly?: boolean; +} + +/** + * Validate a single additional mount against the allowlist. + * Returns validation result with reason. + */ +export function validateMount(mount: AdditionalMount, isMain: boolean): MountValidationResult { + const allowlist = loadMountAllowlist(); + + // If no allowlist, block all additional mounts + if (allowlist === null) { + return { + allowed: false, + reason: `No mount allowlist configured at ${MOUNT_ALLOWLIST_PATH}`, + }; + } + + // Derive containerPath from hostPath basename if not specified + const containerPath = mount.containerPath || path.basename(mount.hostPath); + + // Validate container path (cheap check) + if (!isValidContainerPath(containerPath)) { + return { + allowed: false, + reason: `Invalid container path: "${containerPath}" - must be relative, non-empty, and not contain ".."`, + }; + } + + // Expand and resolve the host path + const expandedPath = expandPath(mount.hostPath); + const realPath = getRealPath(expandedPath); + + if (realPath === null) { + return { + allowed: false, + reason: `Host path does not exist: "${mount.hostPath}" (expanded: "${expandedPath}")`, + }; + } + + // Check against blocked patterns + const blockedMatch = matchesBlockedPattern(realPath, allowlist.blockedPatterns); + if (blockedMatch !== null) { + return { + allowed: false, + reason: `Path matches blocked pattern "${blockedMatch}": "${realPath}"`, + }; + } + + // Check if under an allowed root + const allowedRoot = findAllowedRoot(realPath, allowlist.allowedRoots); + if (allowedRoot === null) { + return { + allowed: false, + reason: `Path "${realPath}" is not under any allowed root. Allowed roots: ${allowlist.allowedRoots + .map((r) => expandPath(r.path)) + .join(', ')}`, + }; + } + + // Determine effective readonly status + const requestedReadWrite = mount.readonly === false; + let effectiveReadonly = true; // Default to readonly + + if (requestedReadWrite) { + if (!isMain && allowlist.nonMainReadOnly) { + // Non-main groups forced to read-only + effectiveReadonly = true; + logger.info( + { + mount: mount.hostPath, + }, + 'Mount forced to read-only for non-main group', + ); + } else if (!allowedRoot.allowReadWrite) { + // Root doesn't allow read-write + effectiveReadonly = true; + logger.info( + { + mount: mount.hostPath, + root: allowedRoot.path, + }, + 'Mount forced to read-only - root does not allow read-write', + ); + } else { + // Read-write allowed + effectiveReadonly = false; + } + } + + return { + allowed: true, + reason: `Allowed under root "${allowedRoot.path}"${allowedRoot.description ? ` (${allowedRoot.description})` : ''}`, + realHostPath: realPath, + resolvedContainerPath: containerPath, + effectiveReadonly, + }; +} + +/** + * Validate all additional mounts for a group. + * Returns array of validated mounts (only those that passed validation). + * Logs warnings for rejected mounts. + */ +export function validateAdditionalMounts( + mounts: AdditionalMount[], + groupName: string, + isMain: boolean, +): Array<{ + hostPath: string; + containerPath: string; + readonly: boolean; +}> { + const validatedMounts: Array<{ + hostPath: string; + containerPath: string; + readonly: boolean; + }> = []; + + for (const mount of mounts) { + const result = validateMount(mount, isMain); + + if (result.allowed) { + validatedMounts.push({ + hostPath: result.realHostPath!, + containerPath: `/workspace/extra/${result.resolvedContainerPath}`, + readonly: result.effectiveReadonly!, + }); + + logger.debug( + { + group: groupName, + hostPath: result.realHostPath, + containerPath: result.resolvedContainerPath, + readonly: result.effectiveReadonly, + reason: result.reason, + }, + 'Mount validated successfully', + ); + } else { + logger.warn( + { + group: groupName, + requestedPath: mount.hostPath, + containerPath: mount.containerPath, + reason: result.reason, + }, + 'Additional mount REJECTED', + ); + } + } + + return validatedMounts; +} + +/** + * Generate a template allowlist file for users to customize + */ +export function generateAllowlistTemplate(): string { + const template: MountAllowlist = { + allowedRoots: [ + { + path: '~/projects', + allowReadWrite: true, + description: 'Development projects', + }, + { + path: '~/repos', + allowReadWrite: true, + description: 'Git repositories', + }, + { + path: '~/Documents/work', + allowReadWrite: false, + description: 'Work documents (read-only)', + }, + ], + blockedPatterns: [ + // Additional patterns beyond defaults + 'password', + 'secret', + 'token', + ], + nonMainReadOnly: true, + }; + + return JSON.stringify(template, null, 2); +} diff --git a/src/remote-control.test.ts b/src/v1/remote-control.test.ts similarity index 100% rename from src/remote-control.test.ts rename to src/v1/remote-control.test.ts diff --git a/src/remote-control.ts b/src/v1/remote-control.ts similarity index 100% rename from src/remote-control.ts rename to src/v1/remote-control.ts diff --git a/src/v1/router.ts b/src/v1/router.ts new file mode 100644 index 0000000..4c7dd38 --- /dev/null +++ b/src/v1/router.ts @@ -0,0 +1,43 @@ +import { Channel, NewMessage } from './types.js'; +import { formatLocalTime } from './timezone.js'; + +export function escapeXml(s: string): string { + if (!s) return ''; + return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +export function formatMessages(messages: NewMessage[], timezone: string): string { + const lines = messages.map((m) => { + const displayTime = formatLocalTime(m.timestamp, timezone); + const replyAttr = m.reply_to_message_id ? ` reply_to="${escapeXml(m.reply_to_message_id)}"` : ''; + const replySnippet = + m.reply_to_message_content && m.reply_to_sender_name + ? `\n ${escapeXml(m.reply_to_message_content)}` + : ''; + return `${replySnippet}${escapeXml(m.content)}`; + }); + + const header = `\n`; + + return `${header}\n${lines.join('\n')}\n`; +} + +export function stripInternalTags(text: string): string { + return text.replace(/[\s\S]*?<\/internal>/g, '').trim(); +} + +export function formatOutbound(rawText: string): string { + const text = stripInternalTags(rawText); + if (!text) return ''; + return text; +} + +export function routeOutbound(channels: Channel[], jid: string, text: string): Promise { + const channel = channels.find((c) => c.ownsJid(jid) && c.isConnected()); + if (!channel) throw new Error(`No channel for JID: ${jid}`); + return channel.sendMessage(jid, text); +} + +export function findChannel(channels: Channel[], jid: string): Channel | undefined { + return channels.find((c) => c.ownsJid(jid)); +} diff --git a/src/routing.test.ts b/src/v1/routing.test.ts similarity index 100% rename from src/routing.test.ts rename to src/v1/routing.test.ts diff --git a/src/sender-allowlist.test.ts b/src/v1/sender-allowlist.test.ts similarity index 100% rename from src/sender-allowlist.test.ts rename to src/v1/sender-allowlist.test.ts diff --git a/src/sender-allowlist.ts b/src/v1/sender-allowlist.ts similarity index 100% rename from src/sender-allowlist.ts rename to src/v1/sender-allowlist.ts diff --git a/src/session-cleanup.ts b/src/v1/session-cleanup.ts similarity index 100% rename from src/session-cleanup.ts rename to src/v1/session-cleanup.ts diff --git a/src/task-scheduler.test.ts b/src/v1/task-scheduler.test.ts similarity index 100% rename from src/task-scheduler.test.ts rename to src/v1/task-scheduler.test.ts diff --git a/src/task-scheduler.ts b/src/v1/task-scheduler.ts similarity index 100% rename from src/task-scheduler.ts rename to src/v1/task-scheduler.ts diff --git a/src/v1/timezone.test.ts b/src/v1/timezone.test.ts new file mode 100644 index 0000000..d9e9454 --- /dev/null +++ b/src/v1/timezone.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest'; + +import { formatLocalTime, isValidTimezone, resolveTimezone } from './timezone.js'; + +// --- formatLocalTime --- + +describe('formatLocalTime', () => { + it('converts UTC to local time display', () => { + // 2026-02-04T18:30:00Z in America/New_York (EST, UTC-5) = 1:30 PM + const result = formatLocalTime('2026-02-04T18:30:00.000Z', 'America/New_York'); + expect(result).toContain('1:30'); + expect(result).toContain('PM'); + expect(result).toContain('Feb'); + expect(result).toContain('2026'); + }); + + it('handles different timezones', () => { + // Same UTC time should produce different local times + const utc = '2026-06-15T12:00:00.000Z'; + const ny = formatLocalTime(utc, 'America/New_York'); + const tokyo = formatLocalTime(utc, 'Asia/Tokyo'); + // NY is UTC-4 in summer (EDT), Tokyo is UTC+9 + expect(ny).toContain('8:00'); + expect(tokyo).toContain('9:00'); + }); + + it('does not throw on invalid timezone, falls back to UTC', () => { + expect(() => formatLocalTime('2026-01-01T00:00:00.000Z', 'IST-2')).not.toThrow(); + const result = formatLocalTime('2026-01-01T12:00:00.000Z', 'IST-2'); + // Should format as UTC (noon UTC = 12:00 PM) + expect(result).toContain('12:00'); + expect(result).toContain('PM'); + }); +}); + +describe('isValidTimezone', () => { + it('accepts valid IANA identifiers', () => { + expect(isValidTimezone('America/New_York')).toBe(true); + expect(isValidTimezone('UTC')).toBe(true); + expect(isValidTimezone('Asia/Tokyo')).toBe(true); + expect(isValidTimezone('Asia/Jerusalem')).toBe(true); + }); + + it('rejects invalid timezone strings', () => { + expect(isValidTimezone('IST-2')).toBe(false); + expect(isValidTimezone('XYZ+3')).toBe(false); + }); + + it('rejects empty and garbage strings', () => { + expect(isValidTimezone('')).toBe(false); + expect(isValidTimezone('NotATimezone')).toBe(false); + }); +}); + +describe('resolveTimezone', () => { + it('returns the timezone if valid', () => { + expect(resolveTimezone('America/New_York')).toBe('America/New_York'); + }); + + it('falls back to UTC for invalid timezone', () => { + expect(resolveTimezone('IST-2')).toBe('UTC'); + expect(resolveTimezone('')).toBe('UTC'); + }); +}); diff --git a/src/v1/timezone.ts b/src/v1/timezone.ts new file mode 100644 index 0000000..d8cc6cc --- /dev/null +++ b/src/v1/timezone.ts @@ -0,0 +1,37 @@ +/** + * Check whether a timezone string is a valid IANA identifier + * that Intl.DateTimeFormat can use. + */ +export function isValidTimezone(tz: string): boolean { + try { + Intl.DateTimeFormat(undefined, { timeZone: tz }); + return true; + } catch { + return false; + } +} + +/** + * Return the given timezone if valid IANA, otherwise fall back to UTC. + */ +export function resolveTimezone(tz: string): string { + return isValidTimezone(tz) ? tz : 'UTC'; +} + +/** + * Convert a UTC ISO timestamp to a localized display string. + * Uses the Intl API (no external dependencies). + * Falls back to UTC if the timezone is invalid. + */ +export function formatLocalTime(utcIso: string, timezone: string): string { + const date = new Date(utcIso); + return date.toLocaleString('en-US', { + timeZone: resolveTimezone(timezone), + year: 'numeric', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true, + }); +} diff --git a/src/v1/types.ts b/src/v1/types.ts new file mode 100644 index 0000000..717aff6 --- /dev/null +++ b/src/v1/types.ts @@ -0,0 +1,112 @@ +export interface AdditionalMount { + hostPath: string; // Absolute path on host (supports ~ for home) + containerPath?: string; // Optional — defaults to basename of hostPath. Mounted at /workspace/extra/{value} + readonly?: boolean; // Default: true for safety +} + +/** + * Mount Allowlist - Security configuration for additional mounts + * This file should be stored at ~/.config/nanoclaw/mount-allowlist.json + * and is NOT mounted into any container, making it tamper-proof from agents. + */ +export interface MountAllowlist { + // Directories that can be mounted into containers + allowedRoots: AllowedRoot[]; + // Glob patterns for paths that should never be mounted (e.g., ".ssh", ".gnupg") + blockedPatterns: string[]; + // If true, non-main groups can only mount read-only regardless of config + nonMainReadOnly: boolean; +} + +export interface AllowedRoot { + // Absolute path or ~ for home (e.g., "~/projects", "/var/repos") + path: string; + // Whether read-write mounts are allowed under this root + allowReadWrite: boolean; + // Optional description for documentation + description?: string; +} + +export interface ContainerConfig { + additionalMounts?: AdditionalMount[]; + timeout?: number; // Default: 300000 (5 minutes) +} + +export interface RegisteredGroup { + name: string; + folder: string; + trigger: string; + added_at: string; + containerConfig?: ContainerConfig; + requiresTrigger?: boolean; // Default: true for groups, false for solo chats + isMain?: boolean; // True for the main control group (no trigger, elevated privileges) +} + +export interface NewMessage { + id: string; + chat_jid: string; + sender: string; + sender_name: string; + content: string; + timestamp: string; + is_from_me?: boolean; + is_bot_message?: boolean; + thread_id?: string; + reply_to_message_id?: string; + reply_to_message_content?: string; + reply_to_sender_name?: string; +} + +export interface ScheduledTask { + id: string; + group_folder: string; + chat_jid: string; + prompt: string; + script?: string | null; + schedule_type: 'cron' | 'interval' | 'once'; + schedule_value: string; + context_mode: 'group' | 'isolated'; + next_run: string | null; + last_run: string | null; + last_result: string | null; + status: 'active' | 'paused' | 'completed'; + created_at: string; +} + +export interface TaskRunLog { + task_id: string; + run_at: string; + duration_ms: number; + status: 'success' | 'error'; + result: string | null; + error: string | null; +} + +// --- Channel abstraction --- + +export interface Channel { + name: string; + connect(): Promise; + sendMessage(jid: string, text: string): Promise; + isConnected(): boolean; + ownsJid(jid: string): boolean; + disconnect(): Promise; + // Optional: typing indicator. Channels that support it implement it. + setTyping?(jid: string, isTyping: boolean): Promise; + // Optional: sync group/chat names from the platform. + syncGroups?(force: boolean): Promise; +} + +// Callback type that channels use to deliver inbound messages +export type OnInboundMessage = (chatJid: string, message: NewMessage) => void; + +// Callback for chat metadata discovery. +// name is optional — channels that deliver names inline (Telegram) pass it here; +// channels that sync names separately (via syncGroups) omit it. +export type OnChatMetadata = ( + chatJid: string, + timestamp: string, + name?: string, + channel?: string, + isGroup?: boolean, +) => void;