From 6a7af3ec5eb64e216acaf91a35a91c76b87cf693 Mon Sep 17 00:00:00 2001 From: CSTRSK Date: Sat, 8 Aug 2026 19:29:48 +0000 Subject: [PATCH] fix: remove duplicate packages/bot, fix remaining build breaks (#943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete packages/bot/* (unpublished, unreferenced duplicate of packages/bots/*, zero external consumers) so pnpm -r build no longer compiles the dead tree — fixes the repeated TS2345 failures described in #943/#942 - bots/core: annotate finish() with AIResult instead of a widened inline type (TS2345: type string not assignable to "result") - bots/signal: fix outdated test object (groupId undefined→null, drop non-existent raw field, add isGroup) --- packages/bot/core/README.md | 39 --- packages/bot/core/package.json | 13 - packages/bot/core/src/index.test.ts | 24 -- packages/bot/core/src/index.ts | 249 -------------------- packages/bot/core/tsconfig.json | 8 - packages/bot/discord/README.md | 39 --- packages/bot/discord/example.ts | 73 ------ packages/bot/discord/package.json | 15 -- packages/bot/discord/src/index.test.ts | 39 --- packages/bot/discord/src/index.ts | 169 ------------- packages/bot/discord/src/session-manager.ts | 173 -------------- packages/bot/discord/tsconfig.json | 8 - packages/bot/signal/README.md | 39 --- packages/bot/signal/package.json | 14 -- packages/bot/signal/src/index.test.ts | 44 ---- packages/bot/signal/src/index.ts | 123 ---------- packages/bot/signal/tsconfig.json | 8 - packages/bot/telegram/README.md | 39 --- packages/bot/telegram/package.json | 15 -- packages/bot/telegram/src/index.test.ts | 39 --- packages/bot/telegram/src/index.ts | 118 ---------- packages/bot/telegram/tsconfig.json | 8 - packages/bot/whatsapp/README.md | 39 --- packages/bot/whatsapp/package.json | 15 -- packages/bot/whatsapp/src/index.test.ts | 28 --- packages/bot/whatsapp/src/index.ts | 145 ------------ packages/bot/whatsapp/tsconfig.json | 8 - packages/bots/core/src/index.ts | 11 +- packages/bots/signal/src/index.test.ts | 4 +- pnpm-lock.yaml | 51 ---- 30 files changed, 3 insertions(+), 1594 deletions(-) delete mode 100644 packages/bot/core/README.md delete mode 100644 packages/bot/core/package.json delete mode 100644 packages/bot/core/src/index.test.ts delete mode 100644 packages/bot/core/src/index.ts delete mode 100644 packages/bot/core/tsconfig.json delete mode 100644 packages/bot/discord/README.md delete mode 100644 packages/bot/discord/example.ts delete mode 100644 packages/bot/discord/package.json delete mode 100644 packages/bot/discord/src/index.test.ts delete mode 100644 packages/bot/discord/src/index.ts delete mode 100644 packages/bot/discord/src/session-manager.ts delete mode 100644 packages/bot/discord/tsconfig.json delete mode 100644 packages/bot/signal/README.md delete mode 100644 packages/bot/signal/package.json delete mode 100644 packages/bot/signal/src/index.test.ts delete mode 100644 packages/bot/signal/src/index.ts delete mode 100644 packages/bot/signal/tsconfig.json delete mode 100644 packages/bot/telegram/README.md delete mode 100644 packages/bot/telegram/package.json delete mode 100644 packages/bot/telegram/src/index.test.ts delete mode 100644 packages/bot/telegram/src/index.ts delete mode 100644 packages/bot/telegram/tsconfig.json delete mode 100644 packages/bot/whatsapp/README.md delete mode 100644 packages/bot/whatsapp/package.json delete mode 100644 packages/bot/whatsapp/src/index.test.ts delete mode 100644 packages/bot/whatsapp/src/index.ts delete mode 100644 packages/bot/whatsapp/tsconfig.json diff --git a/packages/bot/core/README.md b/packages/bot/core/README.md deleted file mode 100644 index 2a104a0f..00000000 --- a/packages/bot/core/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# BOT Core - -Provides the BOT Core bot adapter for sh1pt messaging automation. - -## What it does - -- Connects a messaging bot channel to sh1pt. -- Supports bot setup, messaging, or session workflows where implemented. -- Can send messages or posts through the connected provider. - -## Package - -- Name: `@sh1pt/bot-core` -- Path: `packages/bot/core` - -## Scripts - -- `build`: `tsc -p tsconfig.json` -- `typecheck`: `tsc -p tsconfig.json --noEmit` - -## Usage - -```bash -pnpm add @sh1pt/bot-core -``` - -## Development - -```bash -pnpm --filter @sh1pt/bot-core typecheck -``` - -Run tests from the repository root when this module includes a test file: - -```bash -pnpm vitest run packages/bot/core/src/index.test.ts -``` - - diff --git a/packages/bot/core/package.json b/packages/bot/core/package.json deleted file mode 100644 index d1165394..00000000 --- a/packages/bot/core/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "@sh1pt/bot-core", - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "scripts": { - "build": "tsc -p tsconfig.json", - "typecheck": "tsc -p tsconfig.json --noEmit" - }, - "dependencies": { - "zod": "^3.24.0" - } -} \ No newline at end of file diff --git a/packages/bot/core/src/index.test.ts b/packages/bot/core/src/index.test.ts deleted file mode 100644 index 089c1034..00000000 --- a/packages/bot/core/src/index.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { loadAIConfig } from "./index"; - -describe("loadAIConfig", () => { - it("loads positive integer env limits", () => { - expect(loadAIConfig({ - AI_MAX_TURNS: "12", - SESSION_TIMEOUT_MS: "60000", - MAX_CONCURRENT_SESSIONS: "3", - })).toMatchObject({ - aiMaxTurns: 12, - sessionTimeoutMs: 60000, - maxConcurrentSessions: 3, - }); - }); - - it.each([ - ["AI_MAX_TURNS", "10abc"], - ["SESSION_TIMEOUT_MS", "1.5"], - ["MAX_CONCURRENT_SESSIONS", "0"], - ])("rejects malformed %s values", (key, value) => { - expect(() => loadAIConfig({ [key]: value })).toThrow(); - }); -}); diff --git a/packages/bot/core/src/index.ts b/packages/bot/core/src/index.ts deleted file mode 100644 index 00073aba..00000000 --- a/packages/bot/core/src/index.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { spawn } from "node:child_process"; -import { z } from "zod"; - -export const aiConfigSchema = z.object({ - aiProvider: z.enum(["claude-code", "opencode", "qwen"]).default("claude-code"), - aiCliPath: z.string().default("claude"), - aiModel: z.string().optional(), - aiMaxTurns: z.number().int().positive().default(10), - sessionTimeoutMs: z.number().int().positive().default(30 * 60 * 1000), - maxConcurrentSessions: z.number().int().positive().default(5), -}); - -export type AIConfig = z.infer; - -export interface AIResult { - type: "result"; - subtype: "success" | "error"; - is_error: boolean; - result: string; - duration_ms: number; - num_turns: number; - session_id: string; - total_cost_usd: number; -} - -export interface Session { - id: string; - aiSessionId: string | null; - lastActivity: number; - busy: boolean; - queue: Array<{ - prompt: string; - senderName: string; - resolve: (result: AIResult) => void; - reject: (err: Error) => void; - }>; - abortController: AbortController | null; -} - -export class SessionManager { - private sessions = new Map(); - private config: AIConfig; - - constructor(config: AIConfig) { - this.config = config; - } - - async sendMessage( - sessionId: string, - prompt: string, - senderName: string - ): Promise { - const session = this.getOrCreateSession(sessionId); - - if (session.busy) { - return new Promise((resolve, reject) => { - session.queue.push({ prompt, senderName, resolve, reject }); - }); - } - - return this.processMessage(session, prompt, senderName); - } - - private getOrCreateSession(sessionId: string): Session { - let session = this.sessions.get(sessionId); - if (!session) { - session = { - id: sessionId, - aiSessionId: null, - lastActivity: Date.now(), - busy: false, - queue: [], - abortController: null, - }; - this.sessions.set(sessionId, session); - } - - if (Date.now() - session.lastActivity > this.config.sessionTimeoutMs) { - session.aiSessionId = null; - } - - session.lastActivity = Date.now(); - return session; - } - - private async processMessage( - session: Session, - prompt: string, - senderName: string - ): Promise { - if ([...this.sessions.values()].filter((s) => s.busy).length >= this.config.maxConcurrentSessions) { - throw new Error("Too many concurrent sessions"); - } - - session.busy = true; - session.abortController = new AbortController(); - - try { - const args = this.buildArgs(session.aiSessionId); - const contextualPrompt = `[${senderName}]: ${prompt}`; - - const proc = spawn(this.config.aiCliPath, [...args, contextualPrompt], { - stdio: ["pipe", "pipe", "pipe"], - signal: session.abortController.signal, - }); - - let stdout = ""; - let stderr = ""; - - proc.stdout?.on("data", (chunk) => (stdout += chunk.toString())); - proc.stderr?.on("data", (chunk) => (stderr += chunk.toString())); - - return new Promise((resolve) => { - const finish = (result: { - type: string; - subtype: string; - is_error: boolean; - result: string; - duration_ms: number; - num_turns: number; - session_id: string; - total_cost_usd: number; - }) => { - session.busy = false; - session.abortController = null; - resolve(result); - this.processQueue(session); - }; - proc.on("close", (code) => { - if (stdout.trim()) { - try { - const parsed = JSON.parse(stdout.trim()); - if (parsed.session_id) session.aiSessionId = parsed.session_id; - finish({ - type: "result", - subtype: parsed.is_error ? "error" : "success", - is_error: parsed.is_error || false, - result: parsed.result || "", - duration_ms: parsed.duration_ms || 0, - num_turns: parsed.num_turns || 0, - session_id: parsed.session_id || "", - total_cost_usd: parsed.total_cost_usd || 0, - }); - return; - } catch {} - } - finish({ - type: "result", - subtype: code === 0 ? "success" : "error", - is_error: code !== 0, - result: stderr || `Process exited with code ${code}`, - duration_ms: 0, - num_turns: 0, - session_id: "", - total_cost_usd: 0, - }); - }); - proc.on("error", (err) => { - finish({ - type: "result", - subtype: "error", - is_error: true, - result: err.message, - duration_ms: 0, - num_turns: 0, - session_id: "", - total_cost_usd: 0, - }); - }); - }); - } catch (err) { - session.busy = false; - session.abortController = null; - this.processQueue(session); - return { - type: "result", - subtype: "error", - is_error: true, - result: err instanceof Error ? err.message : String(err), - duration_ms: 0, - num_turns: 0, - session_id: "", - total_cost_usd: 0, - }; - } - } - - private processQueue(session: Session): void { if (session.queue.length === 0) return; - const next = session.queue.shift()!; - this.processMessage(session, next.prompt, next.senderName) - .then(next.resolve) - .catch(next.reject); - } - - private buildArgs(sessionId: string | null): string[] { - const args = ["--print", "--output-format", "json"]; - - if (sessionId) args.push("--resume", sessionId); - if (this.config.aiModel) args.push("--model", this.config.aiModel); - if (this.config.aiProvider === "claude-code") args.push("--dangerously-skip-permissions"); - - return args; - } - - reset(sessionId: string): void { - const session = this.sessions.get(sessionId); - if (session) { - if (session.abortController) session.abortController.abort(); - session.aiSessionId = null; - session.queue = []; - } - } - - get activeSessions(): number { - return [...this.sessions.values()].filter((s) => s.busy).length; - } -} - -export function loadAIConfig(env: Record): AIConfig { - return aiConfigSchema.parse({ - aiProvider: (env.AI_PROVIDER as any) || "claude-code", - aiCliPath: env.AI_CLI_PATH || "claude", - aiModel: env.AI_MODEL, - aiMaxTurns: parsePositiveIntegerEnv(env.AI_MAX_TURNS, 10), - sessionTimeoutMs: parsePositiveIntegerEnv(env.SESSION_TIMEOUT_MS, 1800000), - maxConcurrentSessions: parsePositiveIntegerEnv(env.MAX_CONCURRENT_SESSIONS, 5), - }); -} - -function parsePositiveIntegerEnv(value: string | undefined, fallback: number): number { - const text = value?.trim(); - if (!text) return fallback; - if (!/^\d+$/.test(text)) return Number.NaN; - const parsed = Number(text); - return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : Number.NaN; -} - -export function chunkMessage(text: string, maxLength = 2000): string[] { - if (text.length <= maxLength) return [text]; - const chunks: string[] = []; - while (text.length > maxLength) { - const breakAt = text.lastIndexOf("\n", maxLength); - const idx = breakAt > maxLength * 0.5 ? breakAt + 1 : maxLength; - chunks.push(text.slice(0, idx)); - text = text.slice(idx); - } - chunks.push(text); - return chunks; -} diff --git a/packages/bot/core/tsconfig.json b/packages/bot/core/tsconfig.json deleted file mode 100644 index df61a235..00000000 --- a/packages/bot/core/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*.ts"] -} diff --git a/packages/bot/discord/README.md b/packages/bot/discord/README.md deleted file mode 100644 index 8a83ebc1..00000000 --- a/packages/bot/discord/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# BOT Discord - -Provides the BOT Discord bot adapter for sh1pt messaging automation. - -## What it does - -- Connects a messaging bot channel to sh1pt. -- Supports bot setup, messaging, or session workflows where implemented. -- Can send messages or posts through the connected provider. - -## Package - -- Name: `@sh1pt/bot-discord` -- Path: `packages/bot/discord` - -## Scripts - -- `build`: `tsc -p tsconfig.json` -- `typecheck`: `tsc -p tsconfig.json --noEmit` - -## Usage - -```bash -pnpm add @sh1pt/bot-discord -``` - -## Development - -```bash -pnpm --filter @sh1pt/bot-discord typecheck -``` - -Run tests from the repository root when this module includes a test file: - -```bash -pnpm vitest run packages/bot/discord/src/index.test.ts -``` - - diff --git a/packages/bot/discord/example.ts b/packages/bot/discord/example.ts deleted file mode 100644 index a2ed19f0..00000000 --- a/packages/bot/discord/example.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { DiscordBot, loadConfig } from "./src/index.js"; -import { SessionManager } from "./src/session-manager.js"; - -// Load config from env -const config = loadConfig({ - DISCORD_BOT_TOKEN: process.env.DISCORD_BOT_TOKEN, - BOT_TRIGGER: process.env.BOT_TRIGGER, - AI_PROVIDER: process.env.AI_PROVIDER, - AI_CLI_PATH: process.env.AI_CLI_PATH, - AI_MODEL: process.env.AI_MODEL, - SESSION_TIMEOUT_MS: process.env.SESSION_TIMEOUT_MS, - MAX_OUTPUT_LENGTH: process.env.MAX_OUTPUT_LENGTH, - MAX_CONCURRENT_SESSIONS: process.env.MAX_CONCURRENT_SESSIONS, - ALLOWED_USERS: process.env.ALLOWED_USERS, - ADMIN_USERS: process.env.ADMIN_USERS, -}); - -// Create bot and session manager -const bot = new DiscordBot(config); -const sessions = new SessionManager(config); - -// Handle incoming messages -bot.onMessage(async (msg) => { - // Access control - if (config.allowedUsers.length > 0 && !config.allowedUsers.includes(msg.source)) { - return; - } - - // Extract prompt (remove trigger word in guilds) - let prompt = msg.text; - if (msg.isGuild) { - const trigger = config.botTrigger.toLowerCase(); - if (prompt.toLowerCase().startsWith(trigger)) { - prompt = prompt.slice(config.botTrigger.length).trim(); - } - // Also check for @mention - const botId = bot.getBotUserId(); - if (botId) { - const mention = new RegExp(`<@!?${botId}>\\s*`); - prompt = prompt.replace(mention, "").trim(); - } - } - - if (!prompt) return; - - // Show typing indicator - await bot.sendTyping(msg.channelId); - - try { - const result = await sessions.sendMessage(msg.channelId, prompt, msg.sourceName); - - if (result.is_error) { - await bot.reply(msg, `Error: ${result.result}`); - return; - } - - // Send response (chunk if too long) - const chunks = result.result.match(/.{1,1900}/g) || [result.result]; - for (const chunk of chunks) { - await bot.reply(msg, chunk); - } - } catch (err) { - await bot.reply(msg, `Error: ${err}`); - } -}); - -// Start the bot -async function main() { - await bot.start(); - console.log("Bot is running. Press Ctrl+C to stop."); -} - -main().catch(console.error); \ No newline at end of file diff --git a/packages/bot/discord/package.json b/packages/bot/discord/package.json deleted file mode 100644 index e420e80f..00000000 --- a/packages/bot/discord/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@sh1pt/bot-discord", - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "scripts": { - "build": "tsc -p tsconfig.json", - "typecheck": "tsc -p tsconfig.json --noEmit" - }, - "dependencies": { - "@sh1pt/bot-core": "workspace:*", - "discord.js": "^14.26.3", - "zod": "^3.24.0" - } -} \ No newline at end of file diff --git a/packages/bot/discord/src/index.test.ts b/packages/bot/discord/src/index.test.ts deleted file mode 100644 index b3580f7f..00000000 --- a/packages/bot/discord/src/index.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { loadConfig } from "./index.js"; - -const requiredEnv = { - DISCORD_BOT_TOKEN: "token", -}; - -describe("loadConfig", () => { - it("uses numeric defaults when optional env values are absent", () => { - expect(loadConfig(requiredEnv)).toMatchObject({ - sessionTimeoutMs: 1800000, - maxOutputLength: 2000, - maxConcurrentSessions: 5, - }); - }); - - it("accepts complete positive integer env values", () => { - expect( - loadConfig({ - ...requiredEnv, - SESSION_TIMEOUT_MS: "60000", - MAX_OUTPUT_LENGTH: "1200", - MAX_CONCURRENT_SESSIONS: "3", - }), - ).toMatchObject({ - sessionTimeoutMs: 60000, - maxOutputLength: 1200, - maxConcurrentSessions: 3, - }); - }); - - it.each([ - ["SESSION_TIMEOUT_MS", "10abc"], - ["MAX_OUTPUT_LENGTH", "1.5"], - ["MAX_CONCURRENT_SESSIONS", "0"], - ])("rejects malformed positive integer env %s=%s", (name, value) => { - expect(() => loadConfig({ ...requiredEnv, [name]: value })).toThrow(); - }); -}); diff --git a/packages/bot/discord/src/index.ts b/packages/bot/discord/src/index.ts deleted file mode 100644 index 144a9502..00000000 --- a/packages/bot/discord/src/index.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { - Client, - GatewayIntentBits, - Partials, - type Message as DiscordMessage, -} from "discord.js"; -import { z } from "zod"; - -const configSchema = z.object({ - botToken: z.string().min(1), - botTrigger: z.string().default("@claude"), - botName: z.string().default("Claude"), - dmEnabled: z.boolean().default(true), - aiProvider: z.enum(["claude-code", "opencode", "qwen"]).default("claude-code"), - aiCliPath: z.string().default("claude"), - aiModel: z.string().optional(), - sessionTimeoutMs: z.number().int().positive().default(30 * 60 * 1000), - maxOutputLength: z.number().int().positive().default(2000), - maxConcurrentSessions: z.number().int().positive().default(5), - allowedUsers: z.array(z.string()).default([]), - allowedChannels: z.array(z.string()).optional(), - adminUsers: z.array(z.string()).default([]), -}); - -export type Config = z.infer; - -export interface IncomingMessage { - source: string; - sourceName: string; - text: string; - timestamp: number; - channelId: string; - guildId: string | null; - isGuild: boolean; - attachments: Array<{ filename: string; url: string; contentType: string }>; - raw: DiscordMessage; -} - -type MessageHandler = (msg: IncomingMessage) => void | Promise; - -function parsePositiveIntegerEnv(value: string | undefined, fallback: number): number { - if (value === undefined || value.trim() === "") return fallback; - if (!/^\d+$/.test(value)) return Number.NaN; - const parsed = Number(value); - return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : Number.NaN; -} - -export class DiscordBot { - private client: Client; - private handlers: MessageHandler[] = []; - private token: string; - private config: Config; - private botUserId: string | null = null; - private running = false; - - constructor(config: Config) { - this.config = config; - this.token = config.botToken; - this.client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.GuildMessageTyping, - GatewayIntentBits.MessageContent, - GatewayIntentBits.DirectMessages, - GatewayIntentBits.DirectMessageTyping, - ], - partials: [Partials.Channel, Partials.Message], - }); - } - - onMessage(handler: MessageHandler): void { - this.handlers.push(handler); - } - - async start(): Promise { - this.client.on("messageCreate", (msg) => this.handleMessage(msg)); - this.client.on("error", (err) => console.error("Discord error:", err.message)); - - await this.client.login(this.token); - this.running = true; - this.botUserId = this.client.user?.id || null; - console.log(`Discord bot started as ${this.client.user?.tag}`); - } - - async stop(): Promise { - this.client.destroy(); - this.running = false; - } - - isRunning(): boolean { - return this.running; - } - - private handleMessage(msg: DiscordMessage): void { - if (msg.author.bot) return; - if (!msg.content && msg.attachments.size === 0) return; - - const attachments = msg.attachments.map((a: any) => ({ - filename: a.name || a.id, - url: a.url, - contentType: a.contentType || "", - })); - - const incoming: IncomingMessage = { - source: msg.author.id, - sourceName: msg.member?.displayName || msg.author.displayName || msg.author.username, - text: msg.content || "", - timestamp: msg.createdTimestamp, - channelId: msg.channelId, - guildId: msg.guildId, - isGuild: !!msg.guildId, - attachments, - raw: msg, - }; - - for (const handler of this.handlers) { - try { - const result = handler(incoming); - if (result instanceof Promise) result.catch(console.error); - } catch (err) { - console.error("Handler error:", err); - } - } - } - - async send(channelId: string, message: string): Promise { - const channel = await this.client.channels.fetch(channelId); - if (!channel || !("send" in channel)) throw new Error(`Channel ${channelId} not found`); - await channel.send(message); - } - - async sendTyping(channelId: string): Promise { - try { - const channel = await this.client.channels.fetch(channelId); - if (channel && "sendTyping" in channel) await channel.sendTyping(); - } catch {} - } - - async reply(msg: IncomingMessage, text: string): Promise { - try { - await msg.raw.reply({ content: text, allowedMentions: { repliedUser: false } }); - } catch { - await this.send(msg.channelId, text); - } - } - - getBotUserId(): string | null { - return this.botUserId; - } -} - -export function loadConfig(env: Record): Config { - return configSchema.parse({ - botToken: env.DISCORD_BOT_TOKEN || "", - botTrigger: env.BOT_TRIGGER || "@claude", - botName: env.BOT_NAME || "Claude", - dmEnabled: env.DM_ENABLED !== "false", - aiProvider: (env.AI_PROVIDER as any) || "claude-code", - aiCliPath: env.AI_CLI_PATH || "claude", - aiModel: env.AI_MODEL, - sessionTimeoutMs: parsePositiveIntegerEnv(env.SESSION_TIMEOUT_MS, 1800000), - maxOutputLength: parsePositiveIntegerEnv(env.MAX_OUTPUT_LENGTH, 2000), - maxConcurrentSessions: parsePositiveIntegerEnv(env.MAX_CONCURRENT_SESSIONS, 5), - allowedUsers: env.ALLOWED_USERS?.split(",").map((u) => u.trim()).filter(Boolean) || [], - allowedChannels: env.ALLOWED_CHANNELS?.split(",").map((c) => c.trim()).filter(Boolean), - adminUsers: env.ADMIN_USERS?.split(",").map((u) => u.trim()).filter(Boolean) || [], - }); -} diff --git a/packages/bot/discord/src/session-manager.ts b/packages/bot/discord/src/session-manager.ts deleted file mode 100644 index a45ede17..00000000 --- a/packages/bot/discord/src/session-manager.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { spawn } from "node:child_process"; -import type { Config } from "./index.js"; - -export interface ClaudeResult { - type: "result"; - subtype: "success" | "error"; - is_error: boolean; - result: string; - duration_ms: number; - num_turns: number; - session_id: string; - total_cost_usd: number; -} - -export interface Session { - channelId: string; - aiSessionId: string | null; - lastActivity: number; - busy: boolean; - queue: Array<{ - prompt: string; - senderName: string; - resolve: (result: ClaudeResult) => void; - reject: (err: Error) => void; - }>; - abortController: AbortController | null; -} - -export class SessionManager { - private sessions = new Map(); - private config: Config; - - constructor(config: Config) { - this.config = config; - } - - async sendMessage( - channelId: string, - prompt: string, - senderName: string - ): Promise { - const session = this.getOrCreateSession(channelId); - - if (session.busy) { - return new Promise((resolve, reject) => { - session.queue.push({ prompt, senderName, resolve, reject }); - }); - } - - return this.processMessage(session, prompt, senderName); - } - - private getOrCreateSession(channelId: string): Session { - let session = this.sessions.get(channelId); - if (!session) { - session = { - channelId, - aiSessionId: null, - lastActivity: Date.now(), - busy: false, - queue: [], - abortController: null, - }; - this.sessions.set(channelId, session); - } - session.lastActivity = Date.now(); - return session; - } - - private async processMessage( - session: Session, - prompt: string, - senderName: string - ): Promise { - if (this.activeSessions >= this.config.maxConcurrentSessions) { - throw new Error("Too many concurrent sessions"); - } - - session.busy = true; - session.abortController = new AbortController(); - - try { - const args = this.buildArgs(session.aiSessionId); - const proc = spawn(this.config.aiCliPath, [...args, prompt], { - stdio: ["pipe", "pipe", "pipe"], - signal: session.abortController.signal, - }); - - let stdout = ""; - let stderr = ""; - - proc.stdout?.on("data", (chunk) => (stdout += chunk.toString())); - proc.stderr?.on("data", (chunk) => (stderr += chunk.toString())); - - return new Promise((resolve) => { - proc.on("close", (code) => { - if (stdout.trim()) { - try { - const parsed = JSON.parse(stdout.trim()); - if (parsed.session_id) session.aiSessionId = parsed.session_id; - resolve({ - type: "result", - subtype: parsed.is_error ? "error" : "success", - is_error: parsed.is_error || false, - result: parsed.result || "", - duration_ms: parsed.duration_ms || 0, - num_turns: parsed.num_turns || 0, - session_id: parsed.session_id || "", - total_cost_usd: parsed.total_cost_usd || 0, - }); - return; - } catch {} - } - resolve({ - type: "result", - subtype: code === 0 ? "success" : "error", - is_error: code !== 0, - result: stderr || `Process exited with code ${code}`, - duration_ms: 0, - num_turns: 0, - session_id: "", - total_cost_usd: 0, - }); - }); - proc.on("error", (err) => { - resolve({ - type: "result", - subtype: "error", - is_error: true, - result: err.message, - duration_ms: 0, - num_turns: 0, - session_id: "", - total_cost_usd: 0, - }); - }); - }); - } finally { - session.busy = false; - session.abortController = null; - this.processQueue(session); - } - } - - private processQueue(session: Session): void { - if (session.queue.length === 0) return; - const next = session.queue.shift()!; - this.processMessage(session, next.prompt, next.senderName) - .then(next.resolve) - .catch(next.reject); - } - - private buildArgs(sessionId: string | null): string[] { - const args = ["--print", "--output-format", "json"]; - if (sessionId) args.push("--resume", sessionId); - if (this.config.aiModel) args.push("--model", this.config.aiModel); - args.push("--dangerously-skip-permissions"); - return args; - } - - reset(channelId: string): void { - const session = this.sessions.get(channelId); - if (session) { - if (session.abortController) session.abortController.abort(); - session.aiSessionId = null; - session.queue = []; - } - } - - get activeSessions(): number { - return [...this.sessions.values()].filter((s) => s.busy).length; - } -} \ No newline at end of file diff --git a/packages/bot/discord/tsconfig.json b/packages/bot/discord/tsconfig.json deleted file mode 100644 index df61a235..00000000 --- a/packages/bot/discord/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*.ts"] -} diff --git a/packages/bot/signal/README.md b/packages/bot/signal/README.md deleted file mode 100644 index 3edba05c..00000000 --- a/packages/bot/signal/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# BOT Signal - -Provides the BOT Signal bot adapter for sh1pt messaging automation. - -## What it does - -- Connects a messaging bot channel to sh1pt. -- Supports bot setup, messaging, or session workflows where implemented. -- Can send messages or posts through the connected provider. - -## Package - -- Name: `@sh1pt/bot-signal` -- Path: `packages/bot/signal` - -## Scripts - -- `build`: `tsc -p tsconfig.json` -- `typecheck`: `tsc -p tsconfig.json --noEmit` - -## Usage - -```bash -pnpm add @sh1pt/bot-signal -``` - -## Development - -```bash -pnpm --filter @sh1pt/bot-signal typecheck -``` - -Run tests from the repository root when this module includes a test file: - -```bash -pnpm vitest run packages/bot/signal/src/index.test.ts -``` - - diff --git a/packages/bot/signal/package.json b/packages/bot/signal/package.json deleted file mode 100644 index c0f2c6cc..00000000 --- a/packages/bot/signal/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "@sh1pt/bot-signal", - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "scripts": { - "build": "tsc -p tsconfig.json", - "typecheck": "tsc -p tsconfig.json --noEmit" - }, - "dependencies": { - "@sh1pt/bot-core": "workspace:*", - "zod": "^3.24.0" - } -} \ No newline at end of file diff --git a/packages/bot/signal/src/index.test.ts b/packages/bot/signal/src/index.test.ts deleted file mode 100644 index 1fb67796..00000000 --- a/packages/bot/signal/src/index.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { loadConfig } from "./index.js"; - -const requiredEnv = { - SIGNAL_PHONE_NUMBER: "+15551234567", -}; - -describe("loadConfig", () => { - it("uses numeric defaults when optional env values are absent", () => { - expect(loadConfig(requiredEnv)).toMatchObject({ - httpPort: 7580, - sessionTimeoutMs: 1800000, - maxOutputLength: 4000, - maxConcurrentSessions: 5, - }); - }); - - it("accepts complete positive integer env values", () => { - expect( - loadConfig({ - ...requiredEnv, - SIGNAL_HTTP_PORT: "8080", - SESSION_TIMEOUT_MS: "60000", - MAX_OUTPUT_LENGTH: "1200", - MAX_CONCURRENT_SESSIONS: "3", - }), - ).toMatchObject({ - httpPort: 8080, - sessionTimeoutMs: 60000, - maxOutputLength: 1200, - maxConcurrentSessions: 3, - }); - }); - - it.each([ - ["SIGNAL_HTTP_PORT", "7580abc"], - ["SIGNAL_HTTP_PORT", "65536"], - ["SESSION_TIMEOUT_MS", "10abc"], - ["MAX_OUTPUT_LENGTH", "1.5"], - ["MAX_CONCURRENT_SESSIONS", "0"], - ])("rejects malformed numeric env %s=%s", (name, value) => { - expect(() => loadConfig({ ...requiredEnv, [name]: value })).toThrow(); - }); -}); diff --git a/packages/bot/signal/src/index.ts b/packages/bot/signal/src/index.ts deleted file mode 100644 index 744ecfc1..00000000 --- a/packages/bot/signal/src/index.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { z } from "zod"; -import { spawn, type ChildProcess } from "node:child_process"; - -const configSchema = z.object({ - botName: z.string().default("Bot"), - signalCliPath: z.string().default("signal-cli"), - phoneNumber: z.string().min(1), - httpPort: z.number().int().positive().default(7580), - aiProvider: z.enum(["claude-code", "opencode", "qwen"]).default("claude-code"), - aiCliPath: z.string().default("claude"), - aiModel: z.string().optional(), - sessionTimeoutMs: z.number().int().positive().default(30 * 60 * 1000), - maxOutputLength: z.number().int().positive().default(4000), - maxConcurrentSessions: z.number().int().positive().default(5), - allowedUsers: z.array(z.string()).default([]), - adminUsers: z.array(z.string()).default([]), -}); - -export type Config = z.infer; - -export interface IncomingMessage { - source: string; - sourceName: string; - text: string; - timestamp: number; - groupId: string | null; - isGroup: boolean; - attachments: Array<{ filename: string; url: string }>; -} - -type MessageHandler = (msg: IncomingMessage) => void | Promise; - -function parsePositiveIntegerEnv(value: string | undefined, fallback: number): number { - if (value === undefined || value.trim() === "") return fallback; - if (!/^\d+$/.test(value)) return Number.NaN; - const parsed = Number(value); - return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : Number.NaN; -} - -function parsePortEnv(value: string | undefined, fallback: number): number { - const parsed = parsePositiveIntegerEnv(value, fallback); - return parsed <= 65535 ? parsed : Number.NaN; -} - -export class SignalBot { - private handlers: MessageHandler[] = []; - private config: Config; - private running = false; - private proc: ChildProcess | null = null; - private fetchInterval: ReturnType | null = null; - private lastTimestamps = new Map(); - - constructor(config: Config) { - this.config = config; - } - - onMessage(handler: MessageHandler): void { - this.handlers.push(handler); - } - - async start(): Promise { - this.running = true; - console.log("Signal bot started (using REST API polling)"); - } - - async stop(): Promise { - this.running = false; - if (this.proc) { - this.proc.kill(); - this.proc = null; - } - if (this.fetchInterval) { - clearInterval(this.fetchInterval); - this.fetchInterval = null; - } - } - - isRunning(): boolean { - return this.running; - } - - async reply(msg: IncomingMessage, text: string): Promise { - await this.send(msg.source, text); - } - - async send(recipient: string, text: string): Promise { - await this.apiCall("send", { - recipient, - message: text, - }); - } - - private async apiCall(endpoint: string, body: any): Promise { - const url = `http://localhost:${this.config.httpPort}/v1/${endpoint}`; - try { - const res = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - return await res.json(); - } catch (err) { - console.error(`Signal API error: ${err}`); - } - } -} - -export function loadConfig(env: Record): Config { - return configSchema.parse({ - botName: env.BOT_NAME || "Bot", - signalCliPath: env.SIGNAL_CLI_PATH || "signal-cli", - phoneNumber: env.SIGNAL_PHONE_NUMBER || "", - httpPort: parsePortEnv(env.SIGNAL_HTTP_PORT, 7580), - aiProvider: (env.AI_PROVIDER as any) || "claude-code", - aiCliPath: env.AI_CLI_PATH || "claude", - aiModel: env.AI_MODEL, - sessionTimeoutMs: parsePositiveIntegerEnv(env.SESSION_TIMEOUT_MS, 1800000), - maxOutputLength: parsePositiveIntegerEnv(env.MAX_OUTPUT_LENGTH, 4000), - maxConcurrentSessions: parsePositiveIntegerEnv(env.MAX_CONCURRENT_SESSIONS, 5), - allowedUsers: env.ALLOWED_USERS?.split(",").map((u) => u.trim()).filter(Boolean) || [], - adminUsers: env.ADMIN_USERS?.split(",").map((u) => u.trim()).filter(Boolean) || [], - }); -} diff --git a/packages/bot/signal/tsconfig.json b/packages/bot/signal/tsconfig.json deleted file mode 100644 index df61a235..00000000 --- a/packages/bot/signal/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*.ts"] -} diff --git a/packages/bot/telegram/README.md b/packages/bot/telegram/README.md deleted file mode 100644 index e6a17018..00000000 --- a/packages/bot/telegram/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# BOT Telegram - -Provides the BOT Telegram bot adapter for sh1pt messaging automation. - -## What it does - -- Connects a messaging bot channel to sh1pt. -- Supports bot setup, messaging, or session workflows where implemented. -- Can send messages or posts through the connected provider. - -## Package - -- Name: `@sh1pt/bot-telegram` -- Path: `packages/bot/telegram` - -## Scripts - -- `build`: `tsc -p tsconfig.json` -- `typecheck`: `tsc -p tsconfig.json --noEmit` - -## Usage - -```bash -pnpm add @sh1pt/bot-telegram -``` - -## Development - -```bash -pnpm --filter @sh1pt/bot-telegram typecheck -``` - -Run tests from the repository root when this module includes a test file: - -```bash -pnpm vitest run packages/bot/telegram/src/index.test.ts -``` - - diff --git a/packages/bot/telegram/package.json b/packages/bot/telegram/package.json deleted file mode 100644 index ede0ee8b..00000000 --- a/packages/bot/telegram/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@sh1pt/bot-telegram", - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "scripts": { - "build": "tsc -p tsconfig.json", - "typecheck": "tsc -p tsconfig.json --noEmit" - }, - "dependencies": { - "@sh1pt/bot-core": "workspace:*", - "grammy": "^1.21.0", - "zod": "^3.24.0" - } -} \ No newline at end of file diff --git a/packages/bot/telegram/src/index.test.ts b/packages/bot/telegram/src/index.test.ts deleted file mode 100644 index 1d18e6cb..00000000 --- a/packages/bot/telegram/src/index.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { loadConfig } from "./index.js"; - -const requiredEnv = { - TELEGRAM_BOT_TOKEN: "token", -}; - -describe("loadConfig", () => { - it("uses numeric defaults when optional env values are absent", () => { - expect(loadConfig(requiredEnv)).toMatchObject({ - sessionTimeoutMs: 1800000, - maxOutputLength: 4000, - maxConcurrentSessions: 5, - }); - }); - - it("accepts complete positive integer env values", () => { - expect( - loadConfig({ - ...requiredEnv, - SESSION_TIMEOUT_MS: "60000", - MAX_OUTPUT_LENGTH: "1200", - MAX_CONCURRENT_SESSIONS: "3", - }), - ).toMatchObject({ - sessionTimeoutMs: 60000, - maxOutputLength: 1200, - maxConcurrentSessions: 3, - }); - }); - - it.each([ - ["SESSION_TIMEOUT_MS", "10abc"], - ["MAX_OUTPUT_LENGTH", "1.5"], - ["MAX_CONCURRENT_SESSIONS", "0"], - ])("rejects malformed positive integer env %s=%s", (name, value) => { - expect(() => loadConfig({ ...requiredEnv, [name]: value })).toThrow(); - }); -}); diff --git a/packages/bot/telegram/src/index.ts b/packages/bot/telegram/src/index.ts deleted file mode 100644 index 64a9c2ee..00000000 --- a/packages/bot/telegram/src/index.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Bot, type Context } from "grammy"; -import { z } from "zod"; - -const configSchema = z.object({ - botToken: z.string().min(1), - botTrigger: z.string().default("/"), - botName: z.string().default("Bot"), - aiProvider: z.enum(["claude-code", "opencode", "qwen"]).default("claude-code"), - aiCliPath: z.string().default("claude"), - aiModel: z.string().optional(), - sessionTimeoutMs: z.number().int().positive().default(30 * 60 * 1000), - maxOutputLength: z.number().int().positive().default(4000), - maxConcurrentSessions: z.number().int().positive().default(5), - allowedUsers: z.array(z.string()).default([]), - adminUsers: z.array(z.string()).default([]), -}); - -export type Config = z.infer; - -export interface IncomingMessage { - source: string; - sourceName: string; - text: string; - timestamp: number; - chatId: number; - isGroup: boolean; - attachments: Array<{ filename: string; url: string }>; - raw: Context; -} - -type MessageHandler = (msg: IncomingMessage) => void | Promise; - -function parsePositiveIntegerEnv(value: string | undefined, fallback: number): number { - if (value === undefined || value.trim() === "") return fallback; - if (!/^\d+$/.test(value)) return Number.NaN; - const parsed = Number(value); - return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : Number.NaN; -} - -export class TelegramBot { - private bot: Bot; - private handlers: MessageHandler[] = []; - private config: Config; - private running = false; - - constructor(config: Config) { - this.config = config; - this.bot = new Bot(config.botToken); - } - - onMessage(handler: MessageHandler): void { - this.handlers.push(handler); - } - - async start(): Promise { - this.bot.on("message:text", async (ctx) => { - const msg = ctx.message; - if (!msg || !msg.text) return; - - const incoming: IncomingMessage = { - source: String(msg.from?.id), - sourceName: msg.from?.first_name || msg.from?.username || "User", - text: msg.text, - timestamp: msg.date * 1000, - chatId: msg.chat.id, - isGroup: msg.chat.type === "group" || msg.chat.type === "supergroup", - attachments: [], - raw: ctx, - }; - - for (const handler of this.handlers) { - try { - const result = handler(incoming); - if (result instanceof Promise) result.catch(console.error); - } catch (err) { - console.error("Handler error:", err); - } - } - }); - - await this.bot.start(); - this.running = true; - console.log("Telegram bot started"); - } - - async stop(): Promise { - await this.bot.stop(); - this.running = false; - } - - isRunning(): boolean { - return this.running; - } - - async reply(msg: IncomingMessage, text: string): Promise { - await msg.raw.reply(text); - } - - async send(chatId: number, text: string): Promise { - await this.bot.api.sendMessage(chatId, text); - } -} - -export function loadConfig(env: Record): Config { - return configSchema.parse({ - botToken: env.TELEGRAM_BOT_TOKEN || "", - botTrigger: env.BOT_TRIGGER || "/", - botName: env.BOT_NAME || "Bot", - aiProvider: (env.AI_PROVIDER as any) || "claude-code", - aiCliPath: env.AI_CLI_PATH || "claude", - aiModel: env.AI_MODEL, - sessionTimeoutMs: parsePositiveIntegerEnv(env.SESSION_TIMEOUT_MS, 1800000), - maxOutputLength: parsePositiveIntegerEnv(env.MAX_OUTPUT_LENGTH, 4000), - maxConcurrentSessions: parsePositiveIntegerEnv(env.MAX_CONCURRENT_SESSIONS, 5), - allowedUsers: env.ALLOWED_USERS?.split(",").map((u) => u.trim()).filter(Boolean) || [], - adminUsers: env.ADMIN_USERS?.split(",").map((u) => u.trim()).filter(Boolean) || [], - }); -} diff --git a/packages/bot/telegram/tsconfig.json b/packages/bot/telegram/tsconfig.json deleted file mode 100644 index df61a235..00000000 --- a/packages/bot/telegram/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*.ts"] -} diff --git a/packages/bot/whatsapp/README.md b/packages/bot/whatsapp/README.md deleted file mode 100644 index af76166d..00000000 --- a/packages/bot/whatsapp/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# BOT Whatsapp - -Provides the BOT Whatsapp bot adapter for sh1pt messaging automation. - -## What it does - -- Connects a messaging bot channel to sh1pt. -- Supports bot setup, messaging, or session workflows where implemented. -- Can send messages or posts through the connected provider. - -## Package - -- Name: `@sh1pt/bot-whatsapp` -- Path: `packages/bot/whatsapp` - -## Scripts - -- `build`: `tsc -p tsconfig.json` -- `typecheck`: `tsc -p tsconfig.json --noEmit` - -## Usage - -```bash -pnpm add @sh1pt/bot-whatsapp -``` - -## Development - -```bash -pnpm --filter @sh1pt/bot-whatsapp typecheck -``` - -Run tests from the repository root when this module includes a test file: - -```bash -pnpm vitest run packages/bot/whatsapp/src/index.test.ts -``` - - diff --git a/packages/bot/whatsapp/package.json b/packages/bot/whatsapp/package.json deleted file mode 100644 index d90bd3e0..00000000 --- a/packages/bot/whatsapp/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@sh1pt/bot-whatsapp", - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "scripts": { - "build": "tsc -p tsconfig.json", - "typecheck": "tsc -p tsconfig.json --noEmit" - }, - "dependencies": { - "@sh1pt/bot-core": "workspace:*", - "baileys": "^6.10.0", - "zod": "^3.24.0" - } -} \ No newline at end of file diff --git a/packages/bot/whatsapp/src/index.test.ts b/packages/bot/whatsapp/src/index.test.ts deleted file mode 100644 index 9ae31297..00000000 --- a/packages/bot/whatsapp/src/index.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { loadConfig } from "./index.js"; - -describe("loadConfig", () => { - it("parses positive integer environment limits", () => { - const config = loadConfig({ - SESSION_TIMEOUT_MS: "60000", - MAX_OUTPUT_LENGTH: "1200", - MAX_CONCURRENT_SESSIONS: "3", - }); - - expect(config.sessionTimeoutMs).toBe(60000); - expect(config.maxOutputLength).toBe(1200); - expect(config.maxConcurrentSessions).toBe(3); - }); - - it("rejects partially parsed and non-integer environment limits", () => { - expect(() => loadConfig({ SESSION_TIMEOUT_MS: "10abc" })).toThrow( - "SESSION_TIMEOUT_MS must be a positive integer" - ); - expect(() => loadConfig({ MAX_OUTPUT_LENGTH: "1.5" })).toThrow( - "MAX_OUTPUT_LENGTH must be a positive integer" - ); - expect(() => loadConfig({ MAX_CONCURRENT_SESSIONS: "0" })).toThrow( - "MAX_CONCURRENT_SESSIONS must be a positive integer" - ); - }); -}); diff --git a/packages/bot/whatsapp/src/index.ts b/packages/bot/whatsapp/src/index.ts deleted file mode 100644 index 348209e2..00000000 --- a/packages/bot/whatsapp/src/index.ts +++ /dev/null @@ -1,145 +0,0 @@ -import makeBaileysBot, { type WASocket } from "baileys"; -import { z } from "zod"; - -const configSchema = z.object({ - botName: z.string().default("Bot"), - aiProvider: z.enum(["claude-code", "opencode", "qwen"]).default("claude-code"), - aiCliPath: z.string().default("claude"), - aiModel: z.string().optional(), - sessionTimeoutMs: z.number().int().positive().default(30 * 60 * 1000), - maxOutputLength: z.number().int().positive().default(4000), - maxConcurrentSessions: z.number().int().positive().default(5), - allowedUsers: z.array(z.string()).default([]), - adminUsers: z.array(z.string()).default([]), -}); - -export type Config = z.infer; - -export interface IncomingMessage { - source: string; - sourceName: string; - text: string; - timestamp: number; - chatId: string; - isGroup: boolean; - attachments: Array<{ filename: string; url: string }>; - raw: any; -} - -type MessageHandler = (msg: IncomingMessage) => void | Promise; - -export class WhatsAppBot { - private sock: WASocket | null = null; - private handlers: MessageHandler[] = []; - private config: Config; - private running = false; - - constructor(config: Config) { - this.config = config; - } - - onMessage(handler: MessageHandler): void { - this.handlers.push(handler); - } - - async start(): Promise { - const { state } = useMultiFileAuthState("./auth"); - - this.sock = makeBaileysBot({ - auth: state, - printQRInTerminal: true, - browser: ["sh1pt-bot", "Chrome", "120"], - }); - - this.sock.ev.on("messages.upsert", async ({ messages }) => { - for (const msg of messages) { - if (!msg.message || msg.key.fromMe) continue; - - const chatId = msg.key.remoteJid!; - const isGroup = chatId.endsWith("@g.us"); - const text = - msg.message.conversation || - msg.message.extendedTextMessage?.text || - ""; - - const incoming: IncomingMessage = { - source: chatId, - sourceName: msg.pushName || "User", - text, - timestamp: Number(msg.messageTimestamp ?? 0) * 1000, - chatId, - isGroup, - attachments: [], - raw: msg, - }; - - for (const handler of this.handlers) { - try { - const result = handler(incoming); - if (result instanceof Promise) result.catch(console.error); - } catch (err) { - console.error("Handler error:", err); - } - } - } - }); - - this.running = true; - console.log("WhatsApp bot started"); - } - - async stop(): Promise { - this.running = false; - } - - isRunning(): boolean { - return this.running; - } - - async reply(msg: IncomingMessage, text: string): Promise { - await this.sock?.sendMessage(msg.chatId, { text }); - } - - async send(chatId: string, text: string): Promise { - await this.sock?.sendMessage(chatId, { text }); - } -} - -function useMultiFileAuthState( - dir: string -): { state: any; saveState: () => void } { - return { - state: {}, - saveState: () => {}, - }; -} - -export function loadConfig(env: Record): Config { - return configSchema.parse({ - botName: env.BOT_NAME || "Bot", - aiProvider: (env.AI_PROVIDER as any) || "claude-code", - aiCliPath: env.AI_CLI_PATH || "claude", - aiModel: env.AI_MODEL, - sessionTimeoutMs: parseEnvPositiveInteger(env.SESSION_TIMEOUT_MS, 1800000, "SESSION_TIMEOUT_MS"), - maxOutputLength: parseEnvPositiveInteger(env.MAX_OUTPUT_LENGTH, 4000, "MAX_OUTPUT_LENGTH"), - maxConcurrentSessions: parseEnvPositiveInteger(env.MAX_CONCURRENT_SESSIONS, 5, "MAX_CONCURRENT_SESSIONS"), - allowedUsers: env.ALLOWED_USERS?.split(",").map((u) => u.trim()).filter(Boolean) || [], - adminUsers: env.ADMIN_USERS?.split(",").map((u) => u.trim()).filter(Boolean) || [], - }); -} - -function parseEnvPositiveInteger( - value: string | undefined, - defaultValue: number, - name: string -): number { - if (value === undefined || value.trim() === "") return defaultValue; - if (!/^\d+$/.test(value.trim())) { - throw new Error(`${name} must be a positive integer`); - } - const parsed = Number(value); - if (!Number.isSafeInteger(parsed) || parsed < 1) { - throw new Error(`${name} must be a positive integer`); - } - return parsed; -} diff --git a/packages/bot/whatsapp/tsconfig.json b/packages/bot/whatsapp/tsconfig.json deleted file mode 100644 index df61a235..00000000 --- a/packages/bot/whatsapp/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*.ts"] -} diff --git a/packages/bots/core/src/index.ts b/packages/bots/core/src/index.ts index 9284cf07..e15d0383 100644 --- a/packages/bots/core/src/index.ts +++ b/packages/bots/core/src/index.ts @@ -111,16 +111,7 @@ export class SessionManager { proc.stderr?.on("data", (chunk) => (stderr += chunk.toString())); return new Promise((resolve) => { - const finish = (result: { - type: string; - subtype: string; - is_error: boolean; - result: string; - duration_ms: number; - num_turns: number; - session_id: string; - total_cost_usd: number; - }) => { + const finish = (result: AIResult) => { session.busy = false; session.abortController = null; resolve(result); diff --git a/packages/bots/signal/src/index.test.ts b/packages/bots/signal/src/index.test.ts index 4a479d93..7b3ca6e2 100644 --- a/packages/bots/signal/src/index.test.ts +++ b/packages/bots/signal/src/index.test.ts @@ -58,9 +58,9 @@ describe('toBotEvent', () => { sourceName: 'User', text: 'hello', timestamp: Number.POSITIVE_INFINITY, - groupId: undefined, + groupId: null, + isGroup: false, attachments: [], - raw: {}, }).timestamp).toBe('1970-01-01T00:00:00.000Z'); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64808c2f..03594781 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -611,57 +611,6 @@ importers: specifier: workspace:* version: link:../../core - packages/bot/core: - dependencies: - zod: - specifier: ^3.24.0 - version: 3.25.76 - - packages/bot/discord: - dependencies: - '@sh1pt/bot-core': - specifier: workspace:* - version: link:../core - discord.js: - specifier: ^14.26.3 - version: 14.26.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) - zod: - specifier: ^3.24.0 - version: 3.25.76 - - packages/bot/signal: - dependencies: - '@sh1pt/bot-core': - specifier: workspace:* - version: link:../core - zod: - specifier: ^3.24.0 - version: 3.25.76 - - packages/bot/telegram: - dependencies: - '@sh1pt/bot-core': - specifier: workspace:* - version: link:../core - grammy: - specifier: ^1.21.0 - version: 1.42.0 - zod: - specifier: ^3.24.0 - version: 3.25.76 - - packages/bot/whatsapp: - dependencies: - '@sh1pt/bot-core': - specifier: workspace:* - version: link:../core - baileys: - specifier: ^6.10.0 - version: 6.17.16(bufferutil@4.1.0)(eslint@9.39.4)(typescript@5.9.3)(utf-8-validate@5.0.10) - zod: - specifier: ^3.24.0 - version: 3.25.76 - packages/bots/core: dependencies: zod: