@openacp/cli 0.6.4 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/adapter-YSEIZJBA.js +798 -0
  2. package/dist/adapter-YSEIZJBA.js.map +1 -0
  3. package/dist/{chunk-DJIXG62C.js → chunk-3IRAWHMC.js} +3 -3
  4. package/dist/{chunk-L5KZXYJD.js → chunk-3ZO3MHZN.js} +21 -2
  5. package/dist/chunk-3ZO3MHZN.js.map +1 -0
  6. package/dist/chunk-4GQ3I65A.js +23 -0
  7. package/dist/chunk-4GQ3I65A.js.map +1 -0
  8. package/dist/{chunk-TNFXJQZP.js → chunk-7KZI2236.js} +2 -2
  9. package/dist/{chunk-FMWSVLRM.js → chunk-FCLGYYTY.js} +1 -21
  10. package/dist/chunk-FCLGYYTY.js.map +1 -0
  11. package/dist/chunk-FEWSQT3U.js +3949 -0
  12. package/dist/chunk-FEWSQT3U.js.map +1 -0
  13. package/dist/{chunk-E56PPPAE.js → chunk-FW6HM4VU.js} +90 -4029
  14. package/dist/chunk-FW6HM4VU.js.map +1 -0
  15. package/dist/{chunk-TOQPQB5Q.js → chunk-PJVKOZTR.js} +2 -2
  16. package/dist/{chunk-DOCFD5JR.js → chunk-WVMSP4AF.js} +2 -2
  17. package/dist/{chunk-3CHBVO4T.js → chunk-XVL6AGMG.js} +2 -2
  18. package/dist/{chunk-N6E3HE42.js → chunk-ZKTIZME6.js} +2 -2
  19. package/dist/cli.js +21 -21
  20. package/dist/{config-XDUOULXX.js → config-B26J3XXN.js} +2 -2
  21. package/dist/{config-editor-3GGBY7NL.js → config-editor-QTGUK3CD.js} +4 -4
  22. package/dist/{daemon-QY7WXHQ3.js → daemon-5DS5BQXJ.js} +3 -3
  23. package/dist/{discord-4DE22BQC.js → discord-QKT3JMRW.js} +14 -12
  24. package/dist/{discord-4DE22BQC.js.map → discord-QKT3JMRW.js.map} +1 -1
  25. package/dist/doctor-6SUCVUZB.js +9 -0
  26. package/dist/{doctor-D3YZ6VHJ.js → doctor-QQ3YZEYV.js} +4 -4
  27. package/dist/index.d.ts +166 -11
  28. package/dist/index.js +13 -10
  29. package/dist/{main-GVTLD7VI.js → main-TSZR4HPP.js} +25 -17
  30. package/dist/main-TSZR4HPP.js.map +1 -0
  31. package/dist/{setup-D6BU36ZL.js → setup-5ZKSUR26.js} +3 -3
  32. package/package.json +5 -1
  33. package/dist/chunk-E56PPPAE.js.map +0 -1
  34. package/dist/chunk-FMWSVLRM.js.map +0 -1
  35. package/dist/chunk-L5KZXYJD.js.map +0 -1
  36. package/dist/doctor-SNSQ5SS2.js +0 -9
  37. package/dist/main-GVTLD7VI.js.map +0 -1
  38. /package/dist/{chunk-DJIXG62C.js.map → chunk-3IRAWHMC.js.map} +0 -0
  39. /package/dist/{chunk-TNFXJQZP.js.map → chunk-7KZI2236.js.map} +0 -0
  40. /package/dist/{chunk-TOQPQB5Q.js.map → chunk-PJVKOZTR.js.map} +0 -0
  41. /package/dist/{chunk-DOCFD5JR.js.map → chunk-WVMSP4AF.js.map} +0 -0
  42. /package/dist/{chunk-3CHBVO4T.js.map → chunk-XVL6AGMG.js.map} +0 -0
  43. /package/dist/{chunk-N6E3HE42.js.map → chunk-ZKTIZME6.js.map} +0 -0
  44. /package/dist/{config-XDUOULXX.js.map → config-B26J3XXN.js.map} +0 -0
  45. /package/dist/{config-editor-3GGBY7NL.js.map → config-editor-QTGUK3CD.js.map} +0 -0
  46. /package/dist/{daemon-QY7WXHQ3.js.map → daemon-5DS5BQXJ.js.map} +0 -0
  47. /package/dist/{doctor-D3YZ6VHJ.js.map → doctor-6SUCVUZB.js.map} +0 -0
  48. /package/dist/{doctor-SNSQ5SS2.js.map → doctor-QQ3YZEYV.js.map} +0 -0
  49. /package/dist/{setup-D6BU36ZL.js.map → setup-5ZKSUR26.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/slack/adapter.ts","../../src/adapters/slack/send-queue.ts","../../src/adapters/slack/utils.ts","../../src/adapters/slack/formatter.ts","../../src/adapters/slack/slug.ts","../../src/adapters/slack/channel-manager.ts","../../src/adapters/slack/permission-handler.ts","../../src/adapters/slack/event-router.ts","../../src/adapters/slack/text-buffer.ts"],"sourcesContent":["// src/adapters/slack/adapter.ts\nimport fs from \"node:fs\";\nimport { App } from \"@slack/bolt\";\nimport { WebClient } from \"@slack/web-api\";\nimport {\n ChannelAdapter,\n type ChannelConfig,\n type OpenACPCore,\n type OutgoingMessage,\n type PermissionRequest,\n type NotificationMessage,\n} from \"../../core/index.js\";\nimport type { Attachment } from \"../../core/types.js\";\nimport type { FileService } from \"../../core/file-service.js\";\nimport { createChildLogger } from \"../../core/log.js\";\nconst log = createChildLogger({ module: \"slack\" });\n\nimport type { SlackChannelConfig } from \"./types.js\";\nimport type { SlackSessionMeta, SlackFileInfo } from \"./types.js\";\nimport { SlackSendQueue } from \"./send-queue.js\";\nimport { SlackFormatter } from \"./formatter.js\";\nimport { SlackChannelManager } from \"./channel-manager.js\";\nimport { SlackPermissionHandler } from \"./permission-handler.js\";\nimport { SlackEventRouter } from \"./event-router.js\";\nimport { SlackTextBuffer } from \"./text-buffer.js\";\nimport { toSlug } from \"./slug.js\";\nimport { isAudioClip } from \"./utils.js\";\n\nexport class SlackAdapter extends ChannelAdapter<OpenACPCore> {\n private app!: App;\n private webClient!: WebClient;\n private queue!: SlackSendQueue;\n private formatter: SlackFormatter;\n private channelManager!: SlackChannelManager;\n private permissionHandler!: SlackPermissionHandler;\n private eventRouter!: SlackEventRouter;\n private sessions = new Map<string, SlackSessionMeta>();\n private textBuffers = new Map<string, SlackTextBuffer>();\n private botUserId = \"\";\n private slackConfig: SlackChannelConfig;\n private fileService!: FileService;\n\n constructor(core: OpenACPCore, config: SlackChannelConfig) {\n super(core, config as unknown as ChannelConfig);\n this.slackConfig = config;\n this.formatter = new SlackFormatter();\n }\n\n async start(): Promise<void> {\n const { botToken, appToken, signingSecret } = this.slackConfig;\n\n if (!botToken || !appToken || !signingSecret) {\n throw new Error(\"Slack adapter requires botToken, appToken, and signingSecret\");\n }\n\n this.app = new App({\n token: botToken,\n appToken,\n signingSecret,\n socketMode: true,\n });\n\n this.webClient = new WebClient(botToken);\n this.queue = new SlackSendQueue(this.webClient);\n this.fileService = this.core.fileService;\n\n // Resolve bot user ID — required to filter bot's own messages (prevent infinite loop)\n const authResult = await this.webClient.auth.test();\n if (!authResult.user_id) {\n throw new Error(\"Slack auth.test() did not return user_id — verify botToken is valid\");\n }\n this.botUserId = authResult.user_id as string;\n log.info({ botUserId: this.botUserId }, \"Slack bot authenticated\");\n\n this.channelManager = new SlackChannelManager(this.queue, this.slackConfig);\n\n // Permission handler — resolve permission gate when user clicks a button\n this.permissionHandler = new SlackPermissionHandler(\n this.queue,\n (requestId, optionId) => {\n for (const [sessionId, _meta] of this.sessions) {\n const session = this.core.sessionManager.getSession(sessionId);\n if (session && session.permissionGate.requestId === requestId) {\n session.permissionGate.resolve(optionId);\n log.info({ sessionId, requestId, optionId }, \"Permission resolved\");\n return;\n }\n }\n log.warn({ requestId, optionId }, \"No matching session found for permission response\");\n },\n );\n this.permissionHandler.register(this.app);\n\n // Event router — dispatch incoming messages from session channels to core\n this.eventRouter = new SlackEventRouter(\n (slackChannelId) => {\n for (const meta of this.sessions.values()) {\n if (meta.channelId === slackChannelId) return meta;\n }\n return undefined;\n },\n (sessionChannelSlug, text, userId, files) => {\n const processFiles = async (): Promise<Attachment[] | undefined> => {\n if (!files?.length) return undefined;\n const audioFiles = files.filter((f) => isAudioClip(f));\n if (!audioFiles.length) return undefined;\n\n const attachments: Attachment[] = [];\n for (const file of audioFiles) {\n const buffer = await this.downloadSlackFile(file.url_private);\n if (!buffer) continue;\n const mimeType = file.mimetype === \"video/mp4\" ? \"audio/mp4\" : file.mimetype;\n const sessionId = this.core.sessionManager.getSessionByThread(\"slack\", sessionChannelSlug)?.id;\n if (!sessionId) continue;\n const att = await this.fileService.saveFile(sessionId, file.name, buffer, mimeType);\n attachments.push(att);\n }\n return attachments.length > 0 ? attachments : undefined;\n };\n\n processFiles()\n .then((attachments) => {\n this.core\n .handleMessage({\n channelId: \"slack\",\n threadId: sessionChannelSlug,\n userId,\n text,\n attachments,\n })\n .catch((err) => log.error({ err }, \"handleMessage error\"));\n })\n .catch((err) => log.error({ err }, \"Failed to process audio files\"));\n },\n this.botUserId,\n this.slackConfig.notificationChannelId,\n // onNewSession: reply with guidance when user messages the notification channel\n async (_text, _userId) => {\n if (this.slackConfig.notificationChannelId) {\n await this.queue.enqueue(\"chat.postMessage\", {\n channel: this.slackConfig.notificationChannelId,\n text: \"💬 To start a new session, use the `/openacp-new` slash command in any channel.\",\n }).catch((err: unknown) => log.warn({ err }, \"Failed to send onNewSession reply\"));\n }\n },\n this.slackConfig,\n this.core.configManager.get().security.allowedUserIds,\n );\n this.eventRouter.register(this.app);\n\n // Start Bolt (Socket Mode)\n await this.app.start();\n log.info(\"Slack adapter started (Socket Mode)\");\n\n // Create startup session + channel (configurable — set autoCreateSession: false to skip)\n if (this.slackConfig.autoCreateSession !== false) {\n await this._createStartupSession();\n }\n }\n\n private async downloadSlackFile(url: string): Promise<Buffer | null> {\n try {\n const resp = await fetch(url, {\n headers: { Authorization: `Bearer ${this.slackConfig.botToken}` },\n });\n if (!resp.ok) {\n log.warn({ status: resp.status }, \"Failed to download Slack file\");\n return null;\n }\n // Slack returns 200 with HTML login page if bot lacks files:read scope\n const contentType = resp.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"text/html\")) {\n log.warn(\"Slack file download returned HTML instead of binary — bot likely missing files:read scope. Reinstall the Slack app with files:read scope.\");\n return null;\n }\n return Buffer.from(await resp.arrayBuffer());\n } catch (err) {\n log.error({ err }, \"Error downloading Slack file\");\n return null;\n }\n }\n\n private async uploadAudioFile(channelId: string, att: Attachment): Promise<void> {\n const fileBuffer = await fs.promises.readFile(att.filePath);\n await this.webClient.files.uploadV2({\n channel_id: channelId,\n file: fileBuffer,\n filename: att.fileName,\n });\n }\n\n private async _createStartupSession(): Promise<void> {\n try {\n let reuseChannelId = this.slackConfig.startupChannelId;\n\n // Try to reuse existing startup channel (Telegram ensureTopics pattern)\n if (reuseChannelId) {\n try {\n const info = await this.queue.enqueue<Record<string, unknown>>(\n \"conversations.info\", { channel: reuseChannelId },\n );\n const channel = (info as Record<string, unknown>)?.channel as Record<string, unknown> | undefined;\n if (!channel || typeof channel.is_archived !== \"boolean\") {\n log.warn({ reuseChannelId }, \"Unexpected conversations.info response shape, creating new channel\");\n reuseChannelId = undefined;\n } else if (channel.is_archived) {\n await this.queue.enqueue(\"conversations.unarchive\", { channel: reuseChannelId });\n log.info({ channelId: reuseChannelId }, \"Unarchived startup channel for reuse\");\n }\n } catch {\n // Channel deleted or inaccessible — will create new\n reuseChannelId = undefined;\n }\n }\n\n if (reuseChannelId) {\n // Reuse existing channel — create session pointing to it\n let hasSession = false;\n for (const m of this.sessions.values()) {\n if (m.channelId === reuseChannelId) { hasSession = true; break; }\n }\n if (!hasSession) {\n const session = await this.core.handleNewSession(\"slack\", undefined, undefined, { createThread: false });\n const slug = `startup-${session.id.slice(0, 8)}`;\n this.sessions.set(session.id, { channelId: reuseChannelId, channelSlug: slug });\n session.threadId = slug;\n // Persist slug to session store so session resume after restart can find it\n await this.core.sessionManager.patchRecord(session.id, {\n platform: { topicId: slug },\n });\n log.info({ sessionId: session.id, channelId: reuseChannelId }, \"Reused startup channel\");\n }\n } else {\n // Create new channel + session\n const session = await this.core.handleNewSession(\"slack\", undefined, undefined, { createThread: true });\n if (!session.threadId) {\n log.error({ sessionId: session.id }, \"Startup session created without threadId\");\n return;\n }\n\n // Persist channel ID to config for reuse on next restart\n const meta = this.sessions.get(session.id);\n if (meta) {\n await this.core.configManager.save(\n { channels: { slack: { startupChannelId: meta.channelId } } },\n );\n log.info({ sessionId: session.id, channelId: meta.channelId }, \"Saved startup channel to config\");\n }\n }\n\n // Notify\n if (this.slackConfig.notificationChannelId) {\n const startupMeta = [...this.sessions.values()].find(m =>\n m.channelId === (reuseChannelId ?? this.slackConfig.startupChannelId)\n );\n if (startupMeta) {\n await this.queue.enqueue(\"chat.postMessage\", {\n channel: this.slackConfig.notificationChannelId,\n text: `✅ OpenACP ready — chat with the agent in <#${startupMeta.channelId}>`,\n });\n }\n }\n } catch (err) {\n log.error({ err }, \"Failed to create/reuse Slack startup session\");\n }\n }\n\n async stop(): Promise<void> {\n // Flush all active text buffers before stopping to prevent data loss\n for (const [sessionId, buf] of this.textBuffers) {\n try {\n await buf.flush();\n } catch (err) {\n log.warn({ err, sessionId }, \"Flush failed during stop\");\n }\n buf.destroy();\n }\n this.textBuffers.clear();\n await this.app.stop();\n log.info(\"Slack adapter stopped\");\n }\n\n // --- ChannelAdapter implementations ---\n\n async createSessionThread(sessionId: string, name: string): Promise<string> {\n const meta = await this.channelManager.createChannel(sessionId, name);\n this.sessions.set(sessionId, meta);\n log.info({ sessionId, channelId: meta.channelId, slug: meta.channelSlug }, \"Session channel created\");\n // Return the slug as the threadId so that lookups via getSessionByThread work\n return meta.channelSlug;\n }\n\n async renameSessionThread(sessionId: string, newName: string): Promise<void> {\n const meta = this.sessions.get(sessionId);\n if (!meta) return;\n\n const newSlug = toSlug(newName, this.slackConfig.channelPrefix ?? \"openacp\");\n\n try {\n await this.queue.enqueue(\"conversations.rename\", {\n channel: meta.channelId,\n name: newSlug,\n });\n meta.channelSlug = newSlug;\n // Update session.threadId so getSessionByThread() keeps working after rename\n const session = this.core.sessionManager.getSession(sessionId);\n if (session) session.threadId = newSlug;\n const existingRecord = this.core.sessionManager.getSessionRecord(sessionId);\n await this.core.sessionManager.patchRecord(sessionId, {\n name: newName,\n platform: { ...(existingRecord?.platform ?? {}), topicId: newSlug },\n });\n log.info({ sessionId, newSlug }, \"Session channel renamed\");\n } catch (err) {\n log.warn({ err, sessionId }, \"Failed to rename Slack channel\");\n }\n }\n\n async deleteSessionThread(sessionId: string): Promise<void> {\n const meta = this.sessions.get(sessionId);\n if (!meta) return;\n\n try {\n await this.permissionHandler.cleanupSession(meta.channelId);\n } catch (err) {\n log.warn({ err, sessionId }, \"Failed to clean up permission buttons\");\n }\n\n try {\n await this.channelManager.archiveChannel(meta.channelId);\n log.info({ sessionId, channelId: meta.channelId }, \"Session channel archived\");\n } catch (err) {\n log.warn({ err, sessionId }, \"Failed to archive Slack channel\");\n }\n this.sessions.delete(sessionId);\n const buf = this.textBuffers.get(sessionId);\n if (buf) { buf.destroy(); this.textBuffers.delete(sessionId); }\n }\n\n private getTextBuffer(sessionId: string, channelId: string): SlackTextBuffer {\n let buf = this.textBuffers.get(sessionId);\n if (!buf) {\n buf = new SlackTextBuffer(channelId, sessionId, this.queue);\n this.textBuffers.set(sessionId, buf);\n }\n return buf;\n }\n\n async sendMessage(sessionId: string, content: OutgoingMessage): Promise<void> {\n const meta = this.sessions.get(sessionId);\n if (!meta) {\n log.warn({ sessionId }, \"No Slack channel for session, skipping message\");\n return;\n }\n\n // Text chunks are buffered and flushed as a single message after idle timeout\n if (content.type === \"text\") {\n const buf = this.getTextBuffer(sessionId, meta.channelId);\n buf.append(content.text ?? \"\");\n return;\n }\n\n // For session_end / error: flush any pending text first, then send the event\n if (content.type === \"session_end\" || content.type === \"error\") {\n const buf = this.textBuffers.get(sessionId);\n if (buf) {\n try {\n await buf.flush();\n } catch (err) {\n log.warn({ err, sessionId }, \"Flush failed on session_end\");\n }\n buf.destroy();\n this.textBuffers.delete(sessionId);\n }\n }\n\n if (content.type === \"attachment\" && content.attachment) {\n if (content.attachment.type === \"audio\") {\n try {\n await this.uploadAudioFile(meta.channelId, content.attachment);\n const buf = this.textBuffers.get(sessionId);\n if (buf) await buf.stripTtsBlock();\n } catch (err) {\n log.error({ err, sessionId }, \"Failed to upload audio to Slack\");\n }\n }\n return;\n }\n\n const blocks = this.formatter.formatOutgoing(content);\n if (blocks.length === 0) return;\n\n try {\n await this.queue.enqueue(\"chat.postMessage\", {\n channel: meta.channelId,\n text: content.text ?? content.type,\n blocks,\n });\n } catch (err) {\n log.error({ err, sessionId, type: content.type }, \"Failed to post Slack message\");\n }\n }\n\n // NOTE: Async flow — different from Telegram adapter.\n // Telegram: sendPermissionRequest awaits user response inline.\n // Slack: posts interactive buttons and returns immediately.\n // Resolution happens asynchronously via the Bolt action handler in\n // SlackPermissionHandler, which calls the PermissionResponseCallback\n // passed during construction. The callback iterates sessions to find\n // the matching permissionGate and resolves it.\n async sendPermissionRequest(\n sessionId: string,\n request: PermissionRequest,\n ): Promise<void> {\n const meta = this.sessions.get(sessionId);\n if (!meta) return;\n\n log.info({ sessionId, requestId: request.id }, \"Sending Slack permission request\");\n const blocks = this.formatter.formatPermissionRequest(request);\n\n try {\n const result = await this.queue.enqueue(\"chat.postMessage\", {\n channel: meta.channelId,\n text: `Permission request: ${request.description}`,\n blocks,\n });\n const ts = (result as { ts?: string })?.ts;\n if (ts) {\n this.permissionHandler.trackPendingMessage(request.id, meta.channelId, ts);\n }\n } catch (err) {\n log.error({ err, sessionId }, \"Failed to post Slack permission request\");\n }\n }\n\n async sendNotification(notification: NotificationMessage): Promise<void> {\n if (!this.slackConfig.notificationChannelId) return;\n\n const emoji: Record<string, string> = {\n completed: \"✅\",\n error: \"❌\",\n permission: \"🔐\",\n input_required: \"💬\",\n };\n const icon = emoji[notification.type] ?? \"ℹ️\";\n const text = `${icon} *${notification.sessionName ?? \"Session\"}*\\n${notification.summary}`;\n const blocks = this.formatter.formatNotification(text);\n\n try {\n await this.queue.enqueue(\"chat.postMessage\", {\n channel: this.slackConfig.notificationChannelId,\n text,\n blocks,\n });\n } catch (err) {\n log.warn({ err, sessionId: notification.sessionId }, \"Failed to send Slack notification\");\n }\n }\n}\n\nexport type { SlackChannelConfig } from \"./types.js\";\n","import PQueue from \"p-queue\";\nimport type { WebClient } from \"@slack/web-api\";\n\nexport type SlackMethod =\n | \"chat.postMessage\"\n | \"chat.update\"\n | \"conversations.create\"\n | \"conversations.rename\"\n | \"conversations.archive\"\n | \"conversations.invite\"\n | \"conversations.join\"\n | \"conversations.unarchive\"\n | \"conversations.info\";\n\n// Requests per minute per method (Slack Tier definitions)\nconst METHOD_RPM: Record<SlackMethod, number> = {\n \"chat.postMessage\": 50, // Tier 3\n \"chat.update\": 50, // Tier 3\n \"conversations.create\": 20, // Tier 2\n \"conversations.rename\": 20, // Tier 2\n \"conversations.archive\": 20, // Tier 2\n \"conversations.invite\": 20, // Tier 2\n \"conversations.join\": 20, // Tier 2\n \"conversations.unarchive\": 20, // Tier 2\n \"conversations.info\": 50, // Tier 3\n};\n\nexport interface ISlackSendQueue {\n enqueue<T = unknown>(method: SlackMethod, params: Record<string, unknown>): Promise<T>;\n}\n\nexport class SlackSendQueue implements ISlackSendQueue {\n private queues = new Map<SlackMethod, PQueue>();\n\n constructor(private client: WebClient) {\n for (const [method, rpm] of Object.entries(METHOD_RPM) as [SlackMethod, number][]) {\n // Spread requests evenly across the minute\n this.queues.set(method, new PQueue({\n interval: Math.ceil(60_000 / rpm),\n intervalCap: 1,\n carryoverConcurrencyCount: true,\n }));\n }\n }\n\n async enqueue<T = unknown>(method: SlackMethod, params: Record<string, unknown>): Promise<T> {\n const queue = this.queues.get(method);\n if (!queue) throw new Error(`Unknown Slack method: ${method}`);\n return queue.add(() => this.client.apiCall(method, params) as Promise<T>);\n }\n}\n","// src/adapters/slack/utils.ts\n// Shared utilities for Slack adapter modules.\n\nimport type { SlackFileInfo } from \"./types.js\";\n\n/** Detect Slack audio clips — MIME type or filename pattern */\nexport function isAudioClip(file: SlackFileInfo): boolean {\n return (file.mimetype === \"video/mp4\" && file.name?.startsWith(\"audio_message\")) ||\n file.mimetype?.startsWith(\"audio/\");\n}\n\nconst SECTION_LIMIT = 3000;\n\n/**\n * Split text at nearest newline boundary before `limit`.\n * Does NOT track code fence state — a triple-backtick block straddling\n * the boundary will be split mid-block.\n * Used by SlackFormatter and SlackTextBuffer to avoid exceeding Slack's\n * 3000-char section limit.\n */\nexport function splitSafe(text: string, limit = SECTION_LIMIT): string[] {\n if (text.length <= limit) return [text];\n const chunks: string[] = [];\n let remaining = text;\n while (remaining.length > 0) {\n if (remaining.length <= limit) { chunks.push(remaining); break; }\n let cut = remaining.lastIndexOf(\"\\n\", limit);\n if (cut <= 0) cut = limit;\n chunks.push(remaining.slice(0, cut));\n remaining = remaining.slice(cut).trimStart();\n }\n return chunks;\n}\n","// src/adapters/slack/formatter.ts\nimport type { types } from \"@slack/bolt\";\nimport type { OutgoingMessage, PermissionRequest } from \"../../core/types.js\";\nimport { splitSafe } from \"./utils.js\";\n\ntype KnownBlock = types.KnownBlock;\n\nexport interface ISlackFormatter {\n formatOutgoing(message: OutgoingMessage): KnownBlock[];\n formatPermissionRequest(req: PermissionRequest): KnownBlock[];\n formatNotification(text: string): KnownBlock[];\n formatSessionEnd(reason?: string): KnownBlock[];\n}\n\n/**\n * Convert a markdown string to Slack mrkdwn format.\n * Handles the most common patterns from AI responses.\n */\nexport function markdownToMrkdwn(text: string): string {\n return text\n // Fenced code blocks — preserve as-is (Slack supports ``` natively)\n // Headers: # H1 → placeholder (protected from italic regex)\n .replace(/^#{1,6}\\s+(.+)$/gm, \"\\x00BOLD\\x00$1\\x00BOLD\\x00\")\n // Bold: **text** → placeholder\n .replace(/\\*\\*(.+?)\\*\\*/g, \"\\x00BOLD\\x00$1\\x00BOLD\\x00\")\n // Italic: *text* → _text_ (won't match placeholder tokens)\n .replace(/(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)/g, \"_$1_\")\n // Restore bold/header placeholders → *text*\n .replace(/\\x00BOLD\\x00(.+?)\\x00BOLD\\x00/g, \"*$1*\")\n // Inline code: `code` — kept as-is (Slack supports backtick)\n // Strikethrough: ~~text~~ → ~text~\n .replace(/~~(.+?)~~/g, \"~$1~\")\n // Links: [text](url) → <url|text>\n .replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^)]+)\\)/g, \"<$2|$1>\")\n // Unordered lists: \"- item\" or \"* item\" → \"• item\"\n .replace(/^[ \\t]*[-*]\\s+/gm, \"• \")\n // Ordered lists: \"1. item\" → \"1. item\" (already fine in mrkdwn)\n .trim();\n}\n\n// Slack mrkdwn text block, max 3000 chars per section\nconst SECTION_LIMIT = 3000;\n\nfunction section(text: string): KnownBlock {\n return { type: \"section\", text: { type: \"mrkdwn\", text: text.slice(0, SECTION_LIMIT) } };\n}\n\nfunction context(text: string): KnownBlock {\n return { type: \"context\", elements: [{ type: \"mrkdwn\", text }] };\n}\n\nexport class SlackFormatter implements ISlackFormatter {\n formatOutgoing(message: OutgoingMessage): KnownBlock[] {\n switch (message.type) {\n case \"text\": {\n const text = message.text ?? \"\";\n if (!text.trim()) return [];\n const converted = markdownToMrkdwn(text);\n return splitSafe(converted).map(chunk => section(chunk));\n }\n\n case \"thought\":\n return [context(`💭 _${(message.text ?? \"\").slice(0, 500)}_`)];\n\n case \"tool_call\": {\n const name = (message as OutgoingMessage & { metadata?: { name?: string; input?: unknown } }).metadata?.name ?? \"tool\";\n const input = (message as OutgoingMessage & { metadata?: { input?: unknown } }).metadata?.input;\n const inputStr = input ? `\\n\\`\\`\\`\\n${JSON.stringify(input, null, 2).slice(0, 500)}\\n\\`\\`\\`` : \"\";\n return [context(`🔧 \\`${name}\\`${inputStr}`)];\n }\n\n case \"tool_update\": {\n const name = (message as OutgoingMessage & { metadata?: { name?: string; status?: string } }).metadata?.name ?? \"tool\";\n const status = (message as OutgoingMessage & { metadata?: { status?: string } }).metadata?.status ?? \"done\";\n const icon = status === \"error\" ? \"❌\" : \"✅\";\n return [context(`${icon} \\`${name}\\` — ${status}`)];\n }\n\n case \"plan\":\n return [\n { type: \"divider\" },\n section(`📋 *Plan*\\n${message.text ?? \"\"}`),\n ];\n\n case \"usage\": {\n const meta = (message as OutgoingMessage & { metadata?: { input_tokens?: number; output_tokens?: number; cost_usd?: number } }).metadata ?? {};\n const parts = [\n meta.input_tokens != null ? `in: ${meta.input_tokens}` : null,\n meta.output_tokens != null ? `out: ${meta.output_tokens}` : null,\n meta.cost_usd != null ? `$${Number(meta.cost_usd).toFixed(4)}` : null,\n ].filter((p): p is string => p !== null);\n return parts.length ? [context(`📊 ${parts.join(\" · \")}`)] : [];\n }\n\n case \"session_end\":\n return this.formatSessionEnd(message.text);\n\n case \"error\":\n return [section(`⚠️ *Error:* ${message.text ?? \"Unknown error\"}`)];\n\n default:\n return [];\n }\n }\n\n formatPermissionRequest(req: PermissionRequest): KnownBlock[] {\n return [\n section(`🔐 *Permission Request*\\n${req.description}`),\n {\n type: \"actions\",\n block_id: `perm_${req.id}`,\n elements: req.options.map(opt => ({\n type: \"button\" as const,\n text: { type: \"plain_text\" as const, text: opt.label, emoji: true },\n value: `${req.id}:${opt.id}`,\n action_id: `perm_action_${opt.id}_${req.id}`,\n style: (opt.isAllow ? \"primary\" : \"danger\") as \"primary\" | \"danger\",\n })),\n } as KnownBlock,\n ];\n }\n\n formatNotification(text: string): KnownBlock[] {\n return [section(text)];\n }\n\n formatSessionEnd(reason?: string): KnownBlock[] {\n return [\n { type: \"divider\" },\n context(`✅ Session ended${reason ? ` — ${reason}` : \"\"}`),\n ];\n }\n}\n","// src/adapters/slack/slug.ts\nimport { customAlphabet } from \"nanoid\";\n\nconst nanoidAlpha = customAlphabet(\"abcdefghijklmnopqrstuvwxyz0123456789\", 4);\n\n/**\n * Convert a human-readable session name to a valid Slack channel name.\n * Rules: lowercase, ≤80 chars, only [a-z0-9-], unique suffix appended.\n *\n * Examples:\n * \"Fix authentication bug\" → \"openacp-fix-authentication-bug-a3k9\"\n * \"New Session\" → \"openacp-new-session-x7p2\"\n * \"Implement OAuth 2.0 & JWT refresh\" → \"openacp-implement-oauth-20-jwt-refresh-b8qr\"\n */\nexport function toSlug(name: string, prefix = \"openacp\"): string {\n const base = name\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, \"\") // strip special chars\n .trim()\n .replace(/\\s+/g, \"-\") // spaces → dashes\n .replace(/-+/g, \"-\") // collapse consecutive dashes\n .slice(0, 60); // leave room for prefix and suffix\n\n const suffix = nanoidAlpha();\n return `${prefix}-${base}-${suffix}`.replace(/-+/g, \"-\");\n}\n","// src/adapters/slack/channel-manager.ts\nimport type { ISlackSendQueue } from \"./send-queue.js\";\nimport { toSlug } from \"./slug.js\";\nimport type { SlackSessionMeta } from \"./types.js\";\nimport type { SlackChannelConfig } from \"./types.js\";\n\nexport interface ISlackChannelManager {\n createChannel(sessionId: string, sessionName: string): Promise<SlackSessionMeta>;\n archiveChannel(channelId: string): Promise<void>;\n notifyChannel(text: string): Promise<void>;\n}\n\nexport class SlackChannelManager implements ISlackChannelManager {\n constructor(\n private queue: ISlackSendQueue,\n private config: SlackChannelConfig,\n ) {}\n\n async createChannel(sessionId: string, sessionName: string): Promise<SlackSessionMeta> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt < 3; attempt++) {\n const finalSlug = toSlug(sessionName, this.config.channelPrefix ?? \"openacp\");\n\n try {\n const res = await this.queue.enqueue<{ channel: { id: string } }>(\n \"conversations.create\",\n { name: finalSlug, is_private: true },\n );\n const channelId = res.channel.id;\n\n // Bot is automatically a member of private channels it creates — no join/invite needed.\n // Invite configured users so they can access the channel.\n const userIds = this.config.allowedUserIds ?? [];\n if (userIds.length > 0) {\n await this.queue.enqueue(\"conversations.invite\", {\n channel: channelId,\n users: userIds.join(\",\"),\n });\n }\n\n return { channelId, channelSlug: finalSlug };\n } catch (err: any) {\n if (err?.data?.error === \"name_taken\" && attempt < 2) {\n lastError = err;\n continue;\n }\n throw err;\n }\n }\n\n throw lastError;\n }\n\n async archiveChannel(channelId: string): Promise<void> {\n await this.queue.enqueue(\"conversations.archive\", { channel: channelId });\n }\n\n async notifyChannel(text: string): Promise<void> {\n if (this.config.notificationChannelId) {\n await this.queue.enqueue(\"chat.postMessage\", {\n channel: this.config.notificationChannelId,\n text,\n });\n }\n }\n}\n","import type { App, BlockAction, ButtonAction } from \"@slack/bolt\";\nimport type { ISlackSendQueue } from \"./send-queue.js\";\n\nexport type PermissionResponseCallback = (requestId: string, optionId: string) => void;\n\nexport interface ISlackPermissionHandler {\n register(app: App): void;\n trackPendingMessage(requestId: string, channelId: string, messageTs: string): void;\n cleanupSession(channelId: string): Promise<void>;\n}\n\nexport class SlackPermissionHandler implements ISlackPermissionHandler {\n private pendingMessages = new Map<string, { channelId: string; messageTs: string }>();\n\n constructor(\n private queue: ISlackSendQueue,\n private onResponse: PermissionResponseCallback,\n ) {}\n\n trackPendingMessage(requestId: string, channelId: string, messageTs: string): void {\n this.pendingMessages.set(requestId, { channelId, messageTs });\n }\n\n async cleanupSession(channelId: string): Promise<void> {\n for (const [requestId, info] of this.pendingMessages) {\n if (info.channelId !== channelId) continue;\n await this.queue.enqueue(\"chat.update\", {\n channel: info.channelId,\n ts: info.messageTs,\n blocks: [],\n });\n this.pendingMessages.delete(requestId);\n }\n }\n\n register(app: App): void {\n // Match any action starting with \"perm_action_\"\n app.action<BlockAction<ButtonAction>>(\n /^perm_action_/,\n async ({ ack, body, action }) => {\n await ack();\n\n const value: string = action.value ?? \"\";\n const colonIdx = value.indexOf(\":\");\n if (colonIdx === -1) return;\n\n const requestId = value.slice(0, colonIdx);\n const optionId = value.slice(colonIdx + 1);\n\n this.onResponse(requestId, optionId);\n\n // Remove from pending tracking since the user has responded\n this.pendingMessages.delete(requestId);\n\n // Update message to remove action buttons and show confirmation\n const message = body.message;\n if (message) {\n await this.queue.enqueue(\"chat.update\", {\n channel: body.channel?.id ?? \"\",\n ts: message.ts,\n text: `✅ Permission response: *${optionId}*`,\n blocks: [],\n });\n }\n }\n );\n }\n}\n","// src/adapters/slack/event-router.ts\nimport type { App } from \"@slack/bolt\";\nimport type { SlackSessionMeta, SlackFileInfo } from \"./types.js\";\nimport type { SlackChannelConfig } from \"./types.js\";\nimport { createChildLogger } from \"../../core/log.js\";\nconst log = createChildLogger({ module: \"slack-event-router\" });\n\n/** Subset of Bolt's message event fields used by the router */\ninterface SlackMessageEvent {\n bot_id?: string;\n subtype?: string;\n channel: string;\n text?: string;\n user?: string;\n files?: Array<{\n id: string;\n name: string;\n mimetype: string;\n size: number;\n url_private: string;\n }>;\n}\n\n// Callback to look up which session (if any) owns a Slack channelId\nexport type SessionLookup = (channelId: string) => SlackSessionMeta | undefined;\n\n// Callback to dispatch an incoming message to core\nexport type IncomingMessageCallback = (sessionId: string, text: string, userId: string, files?: SlackFileInfo[]) => void;\n\n// Callback to create a new session when user messages the notification channel\nexport type NewSessionCallback = (text: string, userId: string) => void;\n\nexport interface ISlackEventRouter {\n register(app: App): void;\n}\n\nexport class SlackEventRouter implements ISlackEventRouter {\n constructor(\n private sessionLookup: SessionLookup,\n private onIncoming: IncomingMessageCallback,\n private botUserId: string,\n private notificationChannelId: string | undefined,\n private onNewSession: NewSessionCallback,\n private config: SlackChannelConfig,\n private globalAllowedUserIds: string[] = [],\n ) {}\n\n private isAllowedUser(userId: string): boolean {\n const slackAllowed = this.config.allowedUserIds ?? [];\n const allowed = slackAllowed.length > 0 ? slackAllowed : this.globalAllowedUserIds;\n if (allowed.length === 0) return true;\n return allowed.includes(userId);\n }\n\n register(app: App): void {\n app.message(async ({ message }) => {\n log.debug({ message }, \"Slack raw message event\");\n\n const msg = message as unknown as SlackMessageEvent;\n\n if (msg.bot_id) return;\n const subtype = msg.subtype;\n if (subtype && subtype !== \"file_share\") return; // edited, deleted, etc.\n\n const channelId = msg.channel;\n const text: string = msg.text ?? \"\";\n const userId: string = msg.user ?? \"\";\n\n const files: SlackFileInfo[] | undefined = msg.files?.map((f) => ({\n id: f.id,\n name: f.name,\n mimetype: f.mimetype,\n size: f.size,\n url_private: f.url_private,\n }));\n\n log.debug({ channelId, userId, text }, \"Slack message received\");\n\n // Ignore messages from the bot itself\n if (userId === this.botUserId) return;\n\n // Enforce allowedUserIds\n if (!this.isAllowedUser(userId)) {\n log.warn({ userId }, \"slack: message from non-allowed user rejected\");\n return;\n }\n\n const session = this.sessionLookup(channelId);\n if (session) {\n // Message to an existing session channel\n log.debug({ channelId, sessionSlug: session.channelSlug }, \"Routing to session\");\n this.onIncoming(session.channelSlug, text, userId, files);\n return;\n }\n\n log.debug({ channelId, notificationChannelId: this.notificationChannelId }, \"No session found for channel\");\n\n // Message to the notification channel → create new session\n if (this.notificationChannelId && channelId === this.notificationChannelId) {\n this.onNewSession(text, userId);\n return;\n }\n });\n }\n}\n","// src/adapters/slack/text-buffer.ts\n// Buffers streamed text chunks per session and flushes as a single Slack message.\n// This prevents the \"many tiny messages\" problem from streaming AI responses.\n\nimport type { ISlackSendQueue } from \"./send-queue.js\";\nimport { markdownToMrkdwn } from \"./formatter.js\";\nimport { splitSafe } from \"./utils.js\";\nimport { createChildLogger } from \"../../core/log.js\";\n\nconst log = createChildLogger({ module: \"slack-text-buffer\" });\n\nconst FLUSH_IDLE_MS = 2000; // flush after 2s of no new chunks\n\nexport class SlackTextBuffer {\n private buffer = \"\";\n private timer: ReturnType<typeof setTimeout> | undefined;\n private flushPromise: Promise<void> | undefined;\n private lastMessageTs: string | undefined;\n private lastPostedText: string | undefined;\n\n constructor(\n private channelId: string,\n private sessionId: string,\n private queue: ISlackSendQueue,\n ) {}\n\n append(text: string): void {\n if (!text) return;\n this.buffer += text;\n this.resetTimer();\n }\n\n private resetTimer(): void {\n if (this.timer) clearTimeout(this.timer);\n this.timer = setTimeout(() => {\n this.timer = undefined;\n this.flush().catch((err) => log.error({ err, sessionId: this.sessionId }, \"Text buffer flush error\"));\n }, FLUSH_IDLE_MS);\n }\n\n async flush(): Promise<void> {\n if (this.flushPromise) return this.flushPromise;\n const text = this.buffer.trim();\n if (!text) return;\n this.buffer = \"\";\n if (this.timer) { clearTimeout(this.timer); this.timer = undefined; }\n\n this.flushPromise = (async () => {\n try {\n const converted = markdownToMrkdwn(text);\n const chunks = splitSafe(converted);\n for (const chunk of chunks) {\n if (!chunk.trim()) continue;\n const result = await this.queue.enqueue(\"chat.postMessage\", {\n channel: this.channelId,\n text: chunk,\n blocks: [{ type: \"section\", text: { type: \"mrkdwn\", text: chunk } }],\n });\n // Track last posted message for potential TTS block editing\n this.lastMessageTs = (result as { ts?: string } | undefined)?.ts;\n this.lastPostedText = chunk;\n }\n } finally {\n this.flushPromise = undefined;\n // Re-flush if content arrived while we were flushing\n if (this.buffer.trim()) {\n await this.flush();\n }\n }\n })();\n\n return this.flushPromise;\n }\n\n destroy(): void {\n if (this.timer) { clearTimeout(this.timer); this.timer = undefined; }\n this.buffer = \"\";\n }\n\n /** Remove [TTS]...[/TTS] blocks — from buffer if unflushed, or edit posted message */\n async stripTtsBlock(): Promise<void> {\n // Case 1: TTS block still in unflushed buffer\n if (/\\[TTS\\][\\s\\S]*?\\[\\/TTS\\]/.test(this.buffer)) {\n this.buffer = this.buffer.replace(/\\[TTS\\][\\s\\S]*?\\[\\/TTS\\]/g, \"\").replace(/\\s{2,}/g, \" \").trim();\n return;\n }\n\n // Case 2: Already flushed — edit the posted message via chat.update\n if (this.lastMessageTs && this.lastPostedText && /\\[TTS\\][\\s\\S]*?\\[\\/TTS\\]/.test(this.lastPostedText)) {\n const cleaned = this.lastPostedText.replace(/\\[TTS\\][\\s\\S]*?\\[\\/TTS\\]/g, \"\").replace(/\\s{2,}/g, \" \").trim();\n if (cleaned) {\n await this.queue.enqueue(\"chat.update\", {\n channel: this.channelId,\n ts: this.lastMessageTs,\n text: cleaned,\n blocks: [{ type: \"section\", text: { type: \"mrkdwn\", text: cleaned } }],\n });\n }\n this.lastPostedText = cleaned;\n }\n }\n}\n"],"mappings":"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","names":["SECTION_LIMIT","log","log"]}
@@ -8,10 +8,10 @@ import {
8
8
  validateBotToken,
9
9
  validateChatId,
10
10
  validateDiscordToken
11
- } from "./chunk-N6E3HE42.js";
11
+ } from "./chunk-ZKTIZME6.js";
12
12
  import {
13
13
  expandHome
14
- } from "./chunk-L5KZXYJD.js";
14
+ } from "./chunk-3ZO3MHZN.js";
15
15
 
16
16
  // src/core/config-editor.ts
17
17
  import * as clack from "@clack/prompts";
@@ -735,4 +735,4 @@ function flattenToPaths(obj, prefix = "") {
735
735
  export {
736
736
  runConfigEditor
737
737
  };
738
- //# sourceMappingURL=chunk-DJIXG62C.js.map
738
+ //# sourceMappingURL=chunk-3IRAWHMC.js.map
@@ -138,6 +138,20 @@ var TunnelSchema = z.object({
138
138
  storeTtlMinutes: z.number().default(60),
139
139
  auth: TunnelAuthSchema
140
140
  }).default({});
141
+ var SlackChannelConfigSchema = z.object({
142
+ enabled: z.boolean().default(false),
143
+ adapter: z.literal("slack").optional(),
144
+ botToken: z.string().optional(),
145
+ // xoxb-...
146
+ appToken: z.string().optional(),
147
+ // xapp-... (Socket Mode)
148
+ signingSecret: z.string().optional(),
149
+ notificationChannelId: z.string().optional(),
150
+ allowedUserIds: z.array(z.string()).default([]),
151
+ channelPrefix: z.string().default("openacp"),
152
+ autoCreateSession: z.boolean().default(true),
153
+ startupChannelId: z.string().optional()
154
+ });
141
155
  var UsageSchema = z.object({
142
156
  enabled: z.boolean().default(true),
143
157
  monthlyBudget: z.number().optional(),
@@ -160,7 +174,9 @@ var SpeechSchema = z.object({
160
174
  }).default({})
161
175
  }).optional().default({});
162
176
  var ConfigSchema = z.object({
163
- channels: z.record(z.string(), BaseChannelSchema),
177
+ channels: z.object({
178
+ slack: SlackChannelConfigSchema.optional()
179
+ }).catchall(BaseChannelSchema),
164
180
  agents: z.record(z.string(), AgentSchema).optional().default({}),
165
181
  defaultAgent: z.string(),
166
182
  workspace: z.object({
@@ -330,6 +346,9 @@ var ConfigManager = class extends EventEmitter {
330
346
  ["OPENACP_TELEGRAM_CHAT_ID", ["channels", "telegram", "chatId"]],
331
347
  ["OPENACP_DISCORD_BOT_TOKEN", ["channels", "discord", "botToken"]],
332
348
  ["OPENACP_DISCORD_GUILD_ID", ["channels", "discord", "guildId"]],
349
+ ["OPENACP_SLACK_BOT_TOKEN", ["channels", "slack", "botToken"]],
350
+ ["OPENACP_SLACK_APP_TOKEN", ["channels", "slack", "appToken"]],
351
+ ["OPENACP_SLACK_SIGNING_SECRET", ["channels", "slack", "signingSecret"]],
333
352
  ["OPENACP_DEFAULT_AGENT", ["defaultAgent"]],
334
353
  ["OPENACP_RUN_MODE", ["runMode"]],
335
354
  ["OPENACP_API_PORT", ["api", "port"]]
@@ -412,4 +431,4 @@ export {
412
431
  expandHome,
413
432
  ConfigManager
414
433
  };
415
- //# sourceMappingURL=chunk-L5KZXYJD.js.map
434
+ //# sourceMappingURL=chunk-3ZO3MHZN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/config.ts","../../src/core/config-migrations.ts"],"sourcesContent":["import { z } from \"zod\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport { EventEmitter } from \"node:events\";\nimport { applyMigrations } from \"./config-migrations.js\";\nimport { createChildLogger } from \"./log.js\";\nconst log = createChildLogger({ module: \"config\" });\n\nconst BaseChannelSchema = z\n .object({\n enabled: z.boolean().default(false),\n adapter: z.string().optional(), // package name for plugin adapters\n })\n .passthrough();\n\nexport const PLUGINS_DIR = path.join(os.homedir(), \".openacp\", \"plugins\");\n\nconst AgentSchema = z.object({\n command: z.string(),\n args: z.array(z.string()).default([]),\n workingDirectory: z.string().optional(),\n env: z.record(z.string(), z.string()).default({}),\n});\n\nconst LoggingSchema = z\n .object({\n level: z\n .enum([\"silent\", \"debug\", \"info\", \"warn\", \"error\", \"fatal\"])\n .default(\"info\"),\n logDir: z.string().default(\"~/.openacp/logs\"),\n maxFileSize: z.union([z.string(), z.number()]).default(\"10m\"),\n maxFiles: z.number().default(7),\n sessionLogRetentionDays: z.number().default(30),\n })\n .default({});\n\nexport type LoggingConfig = z.infer<typeof LoggingSchema>;\n\nconst TunnelAuthSchema = z\n .object({\n enabled: z.boolean().default(false),\n token: z.string().optional(),\n })\n .default({});\n\nconst TunnelSchema = z\n .object({\n enabled: z.boolean().default(false),\n port: z.number().default(3100),\n provider: z\n .enum([\"cloudflare\", \"ngrok\", \"bore\", \"tailscale\"])\n .default(\"cloudflare\"),\n options: z.record(z.string(), z.unknown()).default({}),\n maxUserTunnels: z.number().default(5),\n storeTtlMinutes: z.number().default(60),\n auth: TunnelAuthSchema,\n })\n .default({});\n\nexport type TunnelConfig = z.infer<typeof TunnelSchema>;\n\nconst SlackChannelConfigSchema = z.object({\n enabled: z.boolean().default(false),\n adapter: z.literal(\"slack\").optional(),\n botToken: z.string().optional(), // xoxb-...\n appToken: z.string().optional(), // xapp-... (Socket Mode)\n signingSecret: z.string().optional(),\n notificationChannelId: z.string().optional(),\n allowedUserIds: z.array(z.string()).default([]),\n channelPrefix: z.string().default(\"openacp\"),\n autoCreateSession: z.boolean().default(true),\n startupChannelId: z.string().optional(),\n});\n\nexport type SlackChannelConfig = z.infer<typeof SlackChannelConfigSchema>;\n\nconst UsageSchema = z\n .object({\n enabled: z.boolean().default(true),\n monthlyBudget: z.number().optional(),\n warningThreshold: z.number().default(0.8),\n currency: z.string().default(\"USD\"),\n retentionDays: z.number().default(90),\n })\n .default({});\n\nexport type UsageConfig = z.infer<typeof UsageSchema>;\n\nconst SpeechProviderSchema = z\n .object({\n apiKey: z.string().min(1).optional(),\n model: z.string().optional(),\n })\n .passthrough();\n\nconst SpeechSchema = z\n .object({\n stt: z\n .object({\n provider: z.string().nullable().default(null),\n providers: z.record(SpeechProviderSchema).default({}),\n })\n .default({}),\n tts: z\n .object({\n provider: z.string().nullable().default(null),\n providers: z.record(SpeechProviderSchema).default({}),\n })\n .default({}),\n })\n .optional()\n .default({});\n\nexport const ConfigSchema = z.object({\n channels: z.object({\n slack: SlackChannelConfigSchema.optional(),\n }).catchall(BaseChannelSchema),\n agents: z.record(z.string(), AgentSchema).optional().default({}),\n defaultAgent: z.string(),\n workspace: z\n .object({\n baseDir: z.string().default(\"~/openacp-workspace\"),\n })\n .default({}),\n security: z\n .object({\n allowedUserIds: z.array(z.string()).default([]),\n maxConcurrentSessions: z.number().default(20),\n sessionTimeoutMinutes: z.number().default(60),\n })\n .default({}),\n logging: LoggingSchema,\n runMode: z.enum([\"foreground\", \"daemon\"]).default(\"foreground\"),\n autoStart: z.boolean().default(false),\n api: z\n .object({\n port: z.number().default(21420),\n host: z.string().default(\"127.0.0.1\"),\n })\n .default({}),\n sessionStore: z\n .object({\n ttlDays: z.number().default(30),\n })\n .default({}),\n tunnel: TunnelSchema,\n usage: UsageSchema,\n integrations: z\n .record(\n z.string(),\n z.object({\n installed: z.boolean(),\n installedAt: z.string().optional(),\n }),\n )\n .default({}),\n speech: SpeechSchema,\n});\n\nexport type Config = z.infer<typeof ConfigSchema>;\n\nexport function expandHome(p: string): string {\n if (p.startsWith(\"~\")) {\n return path.join(os.homedir(), p.slice(1));\n }\n return p;\n}\n\nconst DEFAULT_CONFIG = {\n channels: {\n telegram: {\n enabled: false,\n botToken: \"YOUR_BOT_TOKEN_HERE\",\n chatId: 0,\n notificationTopicId: null,\n assistantTopicId: null,\n },\n discord: {\n enabled: false,\n botToken: \"YOUR_DISCORD_BOT_TOKEN_HERE\",\n guildId: \"\",\n forumChannelId: null,\n notificationChannelId: null,\n assistantThreadId: null,\n },\n },\n agents: {\n claude: { command: \"claude-agent-acp\", args: [], env: {} },\n codex: { command: \"codex\", args: [\"--acp\"], env: {} },\n },\n defaultAgent: \"claude\",\n workspace: { baseDir: \"~/openacp-workspace\" },\n security: {\n allowedUserIds: [],\n maxConcurrentSessions: 20,\n sessionTimeoutMinutes: 60,\n },\n sessionStore: { ttlDays: 30 },\n tunnel: {\n enabled: true,\n port: 3100,\n provider: \"cloudflare\",\n options: {},\n storeTtlMinutes: 60,\n auth: { enabled: false },\n },\n usage: {},\n};\n\nexport class ConfigManager extends EventEmitter {\n private config!: Config;\n private configPath: string;\n\n constructor() {\n super();\n this.configPath =\n process.env.OPENACP_CONFIG_PATH || expandHome(\"~/.openacp/config.json\");\n }\n\n async load(): Promise<void> {\n // 1. Ensure directory exists\n const dir = path.dirname(this.configPath);\n fs.mkdirSync(dir, { recursive: true });\n\n // 2. If config file doesn't exist, create default\n if (!fs.existsSync(this.configPath)) {\n fs.writeFileSync(\n this.configPath,\n JSON.stringify(DEFAULT_CONFIG, null, 2),\n );\n log.info({ configPath: this.configPath }, \"Config created\");\n log.info(\n \"Please edit it with your channel credentials (Telegram bot token, Discord bot token, etc.), then restart.\",\n );\n process.exit(1);\n }\n\n // 3. Read and parse\n const raw = JSON.parse(fs.readFileSync(this.configPath, \"utf-8\"));\n\n // 3.5. Auto-migrate config\n const { changed: configUpdated } = applyMigrations(raw);\n if (configUpdated) {\n fs.writeFileSync(this.configPath, JSON.stringify(raw, null, 2));\n }\n\n // 4. Apply env var overrides\n this.applyEnvOverrides(raw);\n\n // 5. Validate with Zod\n const result = ConfigSchema.safeParse(raw);\n if (!result.success) {\n log.error(\"Config validation failed\");\n for (const issue of result.error.issues) {\n log.error(\n { path: issue.path.join(\".\"), message: issue.message },\n \"Validation error\",\n );\n }\n process.exit(1);\n }\n this.config = result.data;\n }\n\n get(): Config {\n return this.config;\n }\n\n async save(\n updates: Record<string, unknown>,\n changePath?: string,\n ): Promise<void> {\n const oldConfig = this.config ? structuredClone(this.config) : undefined;\n // Read current file, merge updates, write back\n const raw = JSON.parse(fs.readFileSync(this.configPath, \"utf-8\"));\n this.deepMerge(raw, updates);\n fs.writeFileSync(this.configPath, JSON.stringify(raw, null, 2));\n // Re-validate and update in-memory config\n const result = ConfigSchema.safeParse(raw);\n if (result.success) {\n this.config = result.data;\n }\n // Emit change event if path provided\n if (changePath) {\n const { getConfigValue } = await import(\"./config-registry.js\");\n const value = getConfigValue(this.config, changePath);\n const oldValue = oldConfig\n ? getConfigValue(oldConfig, changePath)\n : undefined;\n this.emit(\"config:changed\", { path: changePath, value, oldValue });\n }\n }\n\n resolveWorkspace(input?: string): string {\n if (!input) {\n const resolved = expandHome(this.config.workspace.baseDir);\n fs.mkdirSync(resolved, { recursive: true });\n return resolved;\n }\n if (input.startsWith(\"/\") || input.startsWith(\"~\")) {\n const resolved = expandHome(input);\n fs.mkdirSync(resolved, { recursive: true });\n return resolved;\n }\n // Named workspace → lowercase, under baseDir\n const name = input.toLowerCase();\n const resolved = path.join(expandHome(this.config.workspace.baseDir), name);\n fs.mkdirSync(resolved, { recursive: true });\n return resolved;\n }\n\n async exists(): Promise<boolean> {\n return fs.existsSync(this.configPath);\n }\n\n getConfigPath(): string {\n return this.configPath;\n }\n\n async writeNew(config: Config): Promise<void> {\n const dir = path.dirname(this.configPath);\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2));\n }\n\n private applyEnvOverrides(raw: Record<string, unknown>): void {\n const overrides: [string, string[]][] = [\n [\"OPENACP_TELEGRAM_BOT_TOKEN\", [\"channels\", \"telegram\", \"botToken\"]],\n [\"OPENACP_TELEGRAM_CHAT_ID\", [\"channels\", \"telegram\", \"chatId\"]],\n [\"OPENACP_DISCORD_BOT_TOKEN\", [\"channels\", \"discord\", \"botToken\"]],\n [\"OPENACP_DISCORD_GUILD_ID\", [\"channels\", \"discord\", \"guildId\"]],\n [\"OPENACP_SLACK_BOT_TOKEN\", [\"channels\", \"slack\", \"botToken\"]],\n [\"OPENACP_SLACK_APP_TOKEN\", [\"channels\", \"slack\", \"appToken\"]],\n [\"OPENACP_SLACK_SIGNING_SECRET\", [\"channels\", \"slack\", \"signingSecret\"]],\n [\"OPENACP_DEFAULT_AGENT\", [\"defaultAgent\"]],\n [\"OPENACP_RUN_MODE\", [\"runMode\"]],\n [\"OPENACP_API_PORT\", [\"api\", \"port\"]],\n ];\n for (const [envVar, configPath] of overrides) {\n const value = process.env[envVar];\n if (value !== undefined) {\n let target: Record<string, unknown> = raw;\n for (let i = 0; i < configPath.length - 1; i++) {\n if (!target[configPath[i]]) target[configPath[i]] = {};\n target = target[configPath[i]] as Record<string, unknown>;\n }\n const key = configPath[configPath.length - 1];\n // Convert numeric fields to number\n target[key] =\n key === \"chatId\" || key === \"port\" ? Number(value) : value;\n }\n }\n\n // Logging env var overrides\n if (process.env.OPENACP_LOG_LEVEL) {\n raw.logging = raw.logging || {};\n (raw.logging as Record<string, unknown>).level =\n process.env.OPENACP_LOG_LEVEL;\n }\n if (process.env.OPENACP_LOG_DIR) {\n raw.logging = raw.logging || {};\n (raw.logging as Record<string, unknown>).logDir =\n process.env.OPENACP_LOG_DIR;\n }\n if (process.env.OPENACP_DEBUG && !process.env.OPENACP_LOG_LEVEL) {\n raw.logging = raw.logging || {};\n (raw.logging as Record<string, unknown>).level = \"debug\";\n }\n\n // Tunnel env var overrides\n if (process.env.OPENACP_TUNNEL_ENABLED) {\n raw.tunnel = raw.tunnel || {};\n (raw.tunnel as Record<string, unknown>).enabled =\n process.env.OPENACP_TUNNEL_ENABLED === \"true\";\n }\n if (process.env.OPENACP_TUNNEL_PORT) {\n raw.tunnel = raw.tunnel || {};\n (raw.tunnel as Record<string, unknown>).port = Number(\n process.env.OPENACP_TUNNEL_PORT,\n );\n }\n if (process.env.OPENACP_TUNNEL_PROVIDER) {\n raw.tunnel = raw.tunnel || {};\n (raw.tunnel as Record<string, unknown>).provider =\n process.env.OPENACP_TUNNEL_PROVIDER;\n }\n\n // Speech env var overrides\n if (process.env.OPENACP_SPEECH_STT_PROVIDER) {\n raw.speech = raw.speech || {};\n const speech = raw.speech as Record<string, unknown>;\n speech.stt = speech.stt || {};\n (speech.stt as Record<string, unknown>).provider = process.env.OPENACP_SPEECH_STT_PROVIDER;\n }\n if (process.env.OPENACP_SPEECH_GROQ_API_KEY) {\n raw.speech = raw.speech || {};\n const speech = raw.speech as Record<string, unknown>;\n speech.stt = speech.stt || {};\n const stt = speech.stt as Record<string, unknown>;\n stt.providers = stt.providers || {};\n const providers = stt.providers as Record<string, unknown>;\n providers.groq = providers.groq || {};\n (providers.groq as Record<string, unknown>).apiKey =\n process.env.OPENACP_SPEECH_GROQ_API_KEY;\n }\n }\n\n private deepMerge(\n target: Record<string, unknown>,\n source: Record<string, unknown>,\n ): void {\n for (const key of Object.keys(source)) {\n const val = source[key];\n if (\n val &&\n typeof val === \"object\" &&\n !Array.isArray(val)\n ) {\n if (!target[key]) target[key] = {};\n this.deepMerge(\n target[key] as Record<string, unknown>,\n val as Record<string, unknown>,\n );\n } else {\n target[key] = val;\n }\n }\n }\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport { createChildLogger } from \"./log.js\";\nconst log = createChildLogger({ module: \"config-migrations\" });\n\ntype RawConfig = Record<string, unknown>;\n\nexport interface Migration {\n name: string;\n apply: (raw: RawConfig) => boolean; // returns true if config was modified\n}\n\nexport const migrations: Migration[] = [\n {\n name: \"add-tunnel-section\",\n apply(raw) {\n if (raw.tunnel) return false;\n raw.tunnel = {\n enabled: true,\n port: 3100,\n provider: \"cloudflare\",\n options: {},\n storeTtlMinutes: 60,\n auth: { enabled: false },\n };\n log.info(\"Added tunnel section to config (enabled by default with cloudflare)\");\n return true;\n },\n },\n {\n name: \"fix-agent-commands\",\n apply(raw) {\n const COMMAND_MIGRATIONS: Record<string, string[]> = {\n \"claude-agent-acp\": [\"claude\", \"claude-code\"],\n };\n\n const agents = raw.agents;\n if (!agents || typeof agents !== \"object\") return false;\n\n let changed = false;\n for (const [agentName, agentDef] of Object.entries(agents as Record<string, unknown>)) {\n if (!agentDef || typeof agentDef !== \"object\" || !(\"command\" in agentDef)) continue;\n const def = agentDef as Record<string, unknown>;\n if (typeof def.command !== \"string\") continue;\n for (const [correctCmd, legacyCmds] of Object.entries(COMMAND_MIGRATIONS)) {\n if (legacyCmds.includes(def.command as string)) {\n log.warn(\n { agent: agentName, oldCommand: def.command, newCommand: correctCmd },\n `Auto-migrating agent command: \"${def.command}\" → \"${correctCmd}\"`,\n );\n def.command = correctCmd;\n changed = true;\n }\n }\n }\n return changed;\n },\n },\n {\n name: \"migrate-agents-to-store\",\n apply(raw) {\n const agentsJsonPath = path.join(os.homedir(), \".openacp\", \"agents.json\");\n if (fs.existsSync(agentsJsonPath)) return false;\n\n const agents = raw.agents as Record<string, unknown> | undefined;\n if (!agents || Object.keys(agents).length === 0) return false;\n\n const COMMAND_TO_REGISTRY: Record<string, string> = {\n \"claude-agent-acp\": \"claude-acp\",\n \"codex\": \"codex-acp\",\n };\n\n const installed: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(agents)) {\n const cfg = val as Record<string, unknown>;\n const command = typeof cfg.command === \"string\" ? cfg.command : \"\";\n const registryId = COMMAND_TO_REGISTRY[command] ?? null;\n installed[key] = {\n registryId,\n name: key.charAt(0).toUpperCase() + key.slice(1),\n version: \"unknown\",\n distribution: \"custom\",\n command: cfg.command,\n args: cfg.args ?? [],\n env: cfg.env ?? {},\n workingDirectory: cfg.workingDirectory ?? undefined,\n installedAt: new Date().toISOString(),\n binaryPath: null,\n };\n }\n\n fs.mkdirSync(path.dirname(agentsJsonPath), { recursive: true });\n fs.writeFileSync(agentsJsonPath, JSON.stringify({ version: 1, installed }, null, 2));\n\n raw.agents = {};\n return true;\n },\n },\n];\n\n/**\n * Apply all migrations to raw config (mutates in place).\n * Returns whether any changes were made.\n */\nexport function applyMigrations(\n raw: RawConfig,\n migrationList: Migration[] = migrations,\n): { changed: boolean } {\n let changed = false;\n for (const migration of migrationList) {\n if (migration.apply(raw)) {\n changed = true;\n }\n }\n return { changed };\n}\n"],"mappings":"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","names":["fs","path","os","log","resolved"]}
@@ -0,0 +1,23 @@
1
+ // src/core/channel.ts
2
+ var ChannelAdapter = class {
3
+ constructor(core, config) {
4
+ this.core = core;
5
+ this.config = config;
6
+ }
7
+ async deleteSessionThread(_sessionId) {
8
+ }
9
+ // Skill commands — override in adapters that support dynamic commands
10
+ async sendSkillCommands(_sessionId, _commands) {
11
+ }
12
+ async cleanupSkillCommands(_sessionId) {
13
+ }
14
+ // Archive — override in adapters that support topic archiving
15
+ async archiveSessionTopic(_sessionId) {
16
+ return null;
17
+ }
18
+ };
19
+
20
+ export {
21
+ ChannelAdapter
22
+ };
23
+ //# sourceMappingURL=chunk-4GQ3I65A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/channel.ts"],"sourcesContent":["import type { OutgoingMessage, PermissionRequest, NotificationMessage, AgentCommand } from './types.js'\n\nexport interface ChannelConfig {\n enabled: boolean\n [key: string]: unknown\n}\n\nexport interface IChannelAdapter {\n start(): Promise<void>\n stop(): Promise<void>\n\n // Outgoing: core → channel\n sendMessage(sessionId: string, content: OutgoingMessage): Promise<void>\n sendPermissionRequest(sessionId: string, request: PermissionRequest): Promise<void>\n sendNotification(notification: NotificationMessage): Promise<void>\n\n // Session lifecycle on channel side\n createSessionThread(sessionId: string, name: string): Promise<string> // returns threadId\n renameSessionThread(sessionId: string, newName: string): Promise<void>\n deleteSessionThread(sessionId: string): Promise<void>\n\n // Skill commands — optional\n sendSkillCommands(sessionId: string, commands: AgentCommand[]): Promise<void>\n cleanupSkillCommands(sessionId: string): Promise<void>\n}\n\n/**\n * Base class providing default no-op implementations for optional methods.\n * Adapters can extend this or implement IChannelAdapter directly.\n */\nexport abstract class ChannelAdapter<TCore = unknown> implements IChannelAdapter {\n constructor(public readonly core: TCore, protected config: ChannelConfig) {}\n\n abstract start(): Promise<void>\n abstract stop(): Promise<void>\n\n // Outgoing: core → channel\n abstract sendMessage(sessionId: string, content: OutgoingMessage): Promise<void>\n abstract sendPermissionRequest(sessionId: string, request: PermissionRequest): Promise<void>\n abstract sendNotification(notification: NotificationMessage): Promise<void>\n\n // Session lifecycle on channel side\n abstract createSessionThread(sessionId: string, name: string): Promise<string> // returns threadId\n abstract renameSessionThread(sessionId: string, newName: string): Promise<void>\n async deleteSessionThread(_sessionId: string): Promise<void> {}\n\n // Skill commands — override in adapters that support dynamic commands\n async sendSkillCommands(_sessionId: string, _commands: AgentCommand[]): Promise<void> {}\n async cleanupSkillCommands(_sessionId: string): Promise<void> {}\n\n // Archive — override in adapters that support topic archiving\n async archiveSessionTopic(_sessionId: string): Promise<{ newThreadId: string } | null> {\n return null;\n }\n}\n"],"mappings":";AA8BO,IAAe,iBAAf,MAA0E;AAAA,EAC/E,YAA4B,MAAuB,QAAuB;AAA9C;AAAuB;AAAA,EAAwB;AAAA,EAa3E,MAAM,oBAAoB,YAAmC;AAAA,EAAC;AAAA;AAAA,EAG9D,MAAM,kBAAkB,YAAoB,WAA0C;AAAA,EAAC;AAAA,EACvF,MAAM,qBAAqB,YAAmC;AAAA,EAAC;AAAA;AAAA,EAG/D,MAAM,oBAAoB,YAA6D;AACrF,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DoctorEngine
3
- } from "./chunk-TOQPQB5Q.js";
3
+ } from "./chunk-PJVKOZTR.js";
4
4
  import {
5
5
  log
6
6
  } from "./chunk-GAK6PIBW.js";
@@ -131,4 +131,4 @@ export {
131
131
  runDoctorInline,
132
132
  handleDoctorButton
133
133
  };
134
- //# sourceMappingURL=chunk-TNFXJQZP.js.map
134
+ //# sourceMappingURL=chunk-7KZI2236.js.map
@@ -1,22 +1,3 @@
1
- // src/core/channel.ts
2
- var ChannelAdapter = class {
3
- constructor(core, config) {
4
- this.core = core;
5
- this.config = config;
6
- }
7
- async deleteSessionThread(_sessionId) {
8
- }
9
- // Skill commands — override in adapters that support dynamic commands
10
- async sendSkillCommands(_sessionId, _commands) {
11
- }
12
- async cleanupSkillCommands(_sessionId) {
13
- }
14
- // Archive — override in adapters that support topic archiving
15
- async archiveSessionTopic(_sessionId) {
16
- return null;
17
- }
18
- };
19
-
20
1
  // src/adapters/shared/message-dispatcher.ts
21
2
  async function dispatchMessage(handlers, ctx, content) {
22
3
  switch (content.type) {
@@ -464,8 +445,7 @@ Session logs auto-cleanup: 30 days (configurable via \`logging.sessionLogRetenti
464
445
  `;
465
446
 
466
447
  export {
467
- ChannelAdapter,
468
448
  PRODUCT_GUIDE,
469
449
  dispatchMessage
470
450
  };
471
- //# sourceMappingURL=chunk-FMWSVLRM.js.map
451
+ //# sourceMappingURL=chunk-FCLGYYTY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/shared/message-dispatcher.ts","../../src/product-guide.ts"],"sourcesContent":["import type { OutgoingMessage } from \"../../core/types.js\";\n\nexport interface MessageHandlers<TCtx = unknown> {\n onText(ctx: TCtx, content: OutgoingMessage): Promise<void>;\n onThought(ctx: TCtx, content: OutgoingMessage): Promise<void>;\n onToolCall(ctx: TCtx, content: OutgoingMessage): Promise<void>;\n onToolUpdate(ctx: TCtx, content: OutgoingMessage): Promise<void>;\n onPlan(ctx: TCtx, content: OutgoingMessage): Promise<void>;\n onUsage(ctx: TCtx, content: OutgoingMessage): Promise<void>;\n onSessionEnd(ctx: TCtx, content: OutgoingMessage): Promise<void>;\n onError(ctx: TCtx, content: OutgoingMessage): Promise<void>;\n onAttachment(ctx: TCtx, content: OutgoingMessage): Promise<void>;\n onSystemMessage(ctx: TCtx, content: OutgoingMessage): Promise<void>;\n}\n\nexport async function dispatchMessage<TCtx>(\n handlers: MessageHandlers<TCtx>,\n ctx: TCtx,\n content: OutgoingMessage,\n): Promise<void> {\n switch (content.type) {\n case \"text\":\n return handlers.onText(ctx, content);\n case \"thought\":\n return handlers.onThought(ctx, content);\n case \"tool_call\":\n return handlers.onToolCall(ctx, content);\n case \"tool_update\":\n return handlers.onToolUpdate(ctx, content);\n case \"plan\":\n return handlers.onPlan(ctx, content);\n case \"usage\":\n return handlers.onUsage(ctx, content);\n case \"session_end\":\n return handlers.onSessionEnd(ctx, content);\n case \"error\":\n return handlers.onError(ctx, content);\n case \"attachment\":\n return handlers.onAttachment(ctx, content);\n case \"system_message\":\n return handlers.onSystemMessage(ctx, content);\n default:\n return;\n }\n}\n","/**\n * OpenACP Product Guide — comprehensive reference for the AI assistant.\n * The assistant reads this at runtime to answer user questions about features.\n */\nexport const PRODUCT_GUIDE = `\n# OpenACP — Product Guide\n\nOpenACP lets you chat with AI coding agents (like Claude Code) through messaging platforms (Telegram, Discord).\nYou type messages in your chat platform, the agent reads/writes/runs code in your project folder, and results stream back in real time.\n\n---\n\n## Quick Start\n\n1. Start OpenACP: \\`openacp\\` (or \\`openacp start\\` for background daemon)\n2. Open your messaging platform (Telegram group or Discord server) — you'll see the Assistant topic/thread\n3. Tap/click 🆕 New Session or type /new\n4. Pick an agent and a project folder\n5. Chat in the session topic/thread — the agent works on your code\n\n---\n\n## Core Concepts\n\n### Sessions\nA session = one conversation with one AI agent working in one project folder.\nEach session gets its own topic (Telegram) or forum thread (Discord). Chat there to give instructions to the agent.\n\n### Agents\nAn agent is an AI coding tool (e.g., Claude Code, Gemini, Cursor, Codex, etc.).\nOpenACP supports 28+ agents from the official ACP Registry (agentclientprotocol.com).\nYou can install multiple agents and choose which one to use per session.\nThe default agent is used when you don't specify one.\n\n### Agent Management\n- Browse agents: \\`/agents\\` in your chat platform or \\`openacp agents\\` in CLI\n- Install: tap the install button in /agents, or \\`openacp agents install <name>\\`\n- Uninstall: \\`openacp agents uninstall <name>\\`\n- Setup/login: \\`openacp agents run <name> -- <args>\\` (e.g., \\`openacp agents run gemini -- auth login\\`)\n- Details: \\`openacp agents info <name>\\` shows version, dependencies, and setup steps\n\nSome agents need additional setup before they can be used:\n- Claude: requires \\`claude login\\`\n- Gemini: requires \\`openacp agents run gemini -- auth login\\`\n- Codex: requires setting \\`OPENAI_API_KEY\\` environment variable\n- GitHub Copilot: requires \\`openacp agents run copilot -- auth login\\`\n\nAgents are installed in three ways depending on the agent:\n- **npx** — Node.js agents, downloaded automatically on first use\n- **uvx** — Python agents, downloaded automatically on first use\n- **binary** — Platform-specific binaries, downloaded to \\`~/.openacp/agents/\\`\n\n### Project Folder (Workspace)\nThe directory where the agent reads, writes, and runs code.\nWhen creating a session, you choose which folder the agent works in.\nYou can type a full path like \\`~/code/my-project\\` or just a name like \\`my-project\\` (it becomes \\`<base-dir>/my-project\\`).\n\n### System Topics\n- **Assistant** — Always-on helper that can answer questions, create sessions, check status, troubleshoot\n- **Notifications** — System alerts (permission requests, session errors, completions)\n\n---\n\n## Creating Sessions\n\n### From menu\nTap 🆕 New Session → choose agent (if multiple) → choose project folder → confirm\n\n### From command\n- \\`/new\\` — Interactive flow (asks agent + folder)\n- \\`/new claude ~/code/my-project\\` — Create directly with specific agent and folder\n\n### From Assistant topic\nJust ask: \"Create a session for my-project with claude\" — the assistant handles it\n\n### Quick new chat\n\\`/newchat\\` in a session topic — creates new session with same agent and folder as current one\n\n---\n\n## Working with Sessions\n\n### Chat\nType messages in the session topic. The agent responds with code changes, explanations, tool outputs.\n\n### What you see while the agent works\n- **💭 Thinking indicator** — Shows when the agent is reasoning, with elapsed time\n- **Text responses** — Streamed in real time, updated every few seconds\n- **Tool calls** — When the agent runs commands or edits files, you see tool name, input, status, and output\n- **📋 Plan card** — Visual task progress with completed/in-progress/pending items and progress bar\n- **\"View File\" / \"View Diff\" buttons** — Opens in browser with Monaco editor (requires tunnel)\n\n### Session lifecycle\n1. **Creating** — Topic created, agent spawning\n2. **Warming up** — Agent primes its cache (happens automatically, invisible to you)\n3. **Active** — Ready for your messages\n4. **Auto-naming** — After your first message, the session gets a descriptive name (agent summarizes in ~5 words). The topic title updates automatically.\n5. **Finished/Error** — Session completed or hit an error\n\n### Agent skills\nSome agents provide slash commands (e.g., /compact, /review). Available skills are pinned in the session topic.\n\n### Permission requests\nWhen the agent wants to run a command, it asks for permission.\nYou see buttons: ✅ Allow, ❌ Reject (and sometimes \"Always Allow\").\nA notification also appears in the Notifications topic with a link to the request.\n\n### Dangerous mode\nAuto-approves ALL permission requests — the agent runs any command without asking.\n- Enable: \\`/enable_dangerous\\` or tap the ☠️ button in the session\n- Disable: \\`/disable_dangerous\\` or tap the 🔐 button\n- ⚠️ Use with caution — the agent can execute anything\n\n### Session timeout\nIdle sessions are automatically cancelled after a configurable timeout (default: 60 minutes).\nConfigure via \\`security.sessionTimeoutMinutes\\` in config.\n\n---\n\n## Session Transfer (Handoff)\n\n### Chat → Terminal\n1. Type \\`/handoff\\` in a session topic/thread\n2. You get a command like \\`claude --resume <SESSION_ID>\\`\n3. Copy and run it in your terminal — the session continues there with full conversation history\n\n### Terminal → Chat\n1. First time: run \\`openacp integrate claude\\` to install the handoff skill (one-time setup)\n2. In Claude Code, use the /openacp:handoff slash command\n3. The session appears as a new topic/thread and you can continue chatting there\n\n### How it works\n- The agent session ID is shared between platforms\n- Conversation history is preserved — pick up where you left off\n- The agent that supports resume (e.g., Claude with \\`--resume\\`) handles the actual transfer\n\n---\n\n## Managing Sessions\n\n### Status\n- \\`/status\\` — Shows active sessions count and details\n- Ask the Assistant: \"What sessions are running?\"\n\n### List all sessions\n- \\`/sessions\\` — Shows all sessions with status (active, finished, error)\n\n### Cancel\n- \\`/cancel\\` in a session topic — cancels that session\n- Ask the Assistant: \"Cancel the stuck session\"\n\n### Cleanup\n- From \\`/sessions\\` → tap cleanup buttons (finished, errors, all)\n- Ask the Assistant: \"Clean up old sessions\"\n\n---\n\n## Assistant Topic\n\nThe Assistant is an always-on AI helper in its own topic. It can:\n- Answer questions about OpenACP\n- Create sessions for you\n- Check status and health\n- Cancel sessions\n- Clean up old sessions\n- Troubleshoot issues\n- Manage configuration\n\nJust chat naturally: \"How do I create a session?\", \"What's the status?\", \"Something is stuck\"\n\n### Clear history\n\\`/clear\\` in the Assistant topic — resets the conversation\n\n---\n\n## System Commands\n\n| Command | Where | What it does |\n|---------|-------|-------------|\n| \\`/new [agent] [path]\\` | Anywhere | Create new session |\n| \\`/newchat\\` | Session topic | New session, same agent + folder |\n| \\`/cancel\\` | Session topic | Cancel current session |\n| \\`/status\\` | Anywhere | Show status |\n| \\`/sessions\\` | Anywhere | List all sessions |\n| \\`/agents\\` | Anywhere | Browse & install agents from ACP Registry |\n| \\`/install <name>\\` | Anywhere | Install an agent |\n| \\`/enable_dangerous\\` | Session topic | Auto-approve all permissions |\n| \\`/disable_dangerous\\` | Session topic | Restore permission prompts |\n| \\`/handoff\\` | Session topic | Transfer session to terminal |\n| \\`/clear\\` | Assistant topic | Clear assistant history |\n| \\`/menu\\` | Anywhere | Show action menu |\n| \\`/help\\` | Anywhere | Show help |\n| \\`/restart\\` | Anywhere | Restart OpenACP |\n| \\`/update\\` | Anywhere | Update to latest version |\n| \\`/integrate\\` | Anywhere | Manage agent integrations |\n\n---\n\n## Menu Buttons\n\n| Button | Action |\n|--------|--------|\n| 🆕 New Session | Create new session (interactive) |\n| 📋 Sessions | List all sessions with cleanup options |\n| 📊 Status | Show active/total session count |\n| 🤖 Agents | List available agents |\n| 🔗 Integrate | Manage agent integrations |\n| ❓ Help | Show help text |\n| 🔄 Restart | Restart OpenACP |\n| ⬆️ Update | Check and install updates |\n\n---\n\n## CLI Commands\n\n### Server\n- \\`openacp\\` — Start (uses configured mode: foreground or daemon)\n- \\`openacp start\\` — Start as background daemon\n- \\`openacp stop\\` — Stop daemon\n- \\`openacp status\\` — Show daemon status\n- \\`openacp logs\\` — Tail daemon logs\n- \\`openacp --foreground\\` — Force foreground mode (useful for debugging or containers)\n\n### Auto-start (run on boot)\n- macOS: installs a LaunchAgent in \\`~/Library/LaunchAgents/\\`\n- Linux: installs a systemd user service in \\`~/.config/systemd/user/\\`\n- Enabled automatically when you start the daemon. Remove with \\`openacp stop\\`.\n\n### Configuration\n- \\`openacp config\\` — Interactive config editor\n- \\`openacp reset\\` — Delete all data and start fresh\n\n### Agent Management (CLI)\n- \\`openacp agents\\` — List all agents (installed + available from ACP Registry)\n- \\`openacp agents install <name>\\` — Install an agent\n- \\`openacp agents uninstall <name>\\` — Remove an agent\n- \\`openacp agents info <name>\\` — Show details, dependencies, and setup guide\n- \\`openacp agents run <name> [-- args]\\` — Run agent CLI directly (for login, config, etc.)\n- \\`openacp agents refresh\\` — Force-refresh registry cache\n\n### Plugins\n- \\`openacp install <package>\\` — Install adapter plugin\n- \\`openacp uninstall <package>\\` — Remove adapter plugin\n- \\`openacp plugins\\` — List installed plugins\n\n### Integration\n- \\`openacp integrate <agent>\\` — Install agent integration (e.g., Claude handoff skill)\n- \\`openacp integrate <agent> --uninstall\\` — Remove integration\n\n### API (requires running daemon)\n\\`openacp api <command>\\` — Interact with running daemon:\n\n| Command | Description |\n|---------|-------------|\n| \\`status\\` | List active sessions |\n| \\`session <id>\\` | Session details |\n| \\`new <agent> <path>\\` | Create session |\n| \\`send <id> \"text\"\\` | Send prompt |\n| \\`cancel <id>\\` | Cancel session |\n| \\`dangerous <id> on/off\\` | Toggle dangerous mode |\n| \\`topics [--status x,y]\\` | List topics |\n| \\`delete-topic <id> [--force]\\` | Delete topic |\n| \\`cleanup [--status x,y]\\` | Cleanup old topics |\n| \\`agents\\` | List agents |\n| \\`health\\` | System health |\n| \\`config\\` | Show config |\n| \\`config set <key> <value>\\` | Update config |\n| \\`adapters\\` | List adapters |\n| \\`tunnel\\` | Tunnel status |\n| \\`notify \"message\"\\` | Send notification |\n| \\`version\\` | Daemon version |\n| \\`restart\\` | Restart daemon |\n\n---\n\n## File Viewer (Tunnel)\n\nWhen tunnel is enabled, file edits and diffs get \"View\" buttons that open in your browser:\n- **Monaco Editor** — Full VS Code editor with syntax highlighting\n- **Diff viewer** — Side-by-side or inline comparison\n- **Line highlighting** — Click lines to highlight\n- Dark/light theme toggle\n\n### Setup\nEnable in config: set \\`tunnel.enabled\\` to \\`true\\`.\nProviders: Cloudflare (default, free), ngrok, bore, Tailscale Funnel.\n\n### Port Tunneling\n\nExpose any local port (dev servers, APIs, etc.) to the internet:\n\n**CLI commands** (agent can call these directly):\n- \\`openacp tunnel add <port> --label <name>\\` — Create tunnel to a local port\n- \\`openacp tunnel list\\` — List active tunnels\n- \\`openacp tunnel stop <port>\\` — Stop a tunnel\n- \\`openacp tunnel stop-all\\` — Stop all user tunnels\n\n**Telegram commands**:\n- \\`/tunnel <port> [label]\\` — Create tunnel\n- \\`/tunnels\\` — List active tunnels\n- \\`/tunnel stop <port>\\` — Stop tunnel\n\nExample: after starting a dev server on port 3000, run \\`openacp tunnel add 3000 --label my-app\\` to get a public URL.\n\n---\n\n## Configuration\n\nConfig file: \\`~/.openacp/config.json\\`\n\n### Channels\n- **channels.telegram.botToken** — Your Telegram bot token\n- **channels.telegram.chatId** — Your Telegram supergroup ID\n- **channels.discord.botToken** — Your Discord bot token\n- **channels.discord.guildId** — Your Discord server (guild) ID\n\n### Agents\n- **defaultAgent** — Which agent to use by default\n- Agents are managed via \\`/agents\\` (Telegram) or \\`openacp agents\\` (CLI)\n- Installed agents are stored in \\`~/.openacp/agents.json\\`\n- Agent list is fetched from the ACP Registry CDN and cached locally (24h)\n\n### Workspace\n- **workspace.baseDir** — Base directory for project folders (default: \\`~/openacp-workspace\\`)\n\n### Security\n- **security.allowedUserIds** — Restrict who can use the bot (empty = everyone)\n- **security.maxConcurrentSessions** — Max parallel sessions (default: 5)\n- **security.sessionTimeoutMinutes** — Auto-cancel idle sessions (default: 60)\n\n### Tunnel / File Viewer\n- **tunnel.enabled** — Enable file viewer tunnel\n- **tunnel.provider** — Tunnel provider: cloudflare (default, free), ngrok, bore, tailscale\n- **tunnel.port** — Local port for tunnel server (default: 3100)\n- **tunnel.auth.enabled** — Enable authentication for tunnel URLs\n- **tunnel.auth.token** — Auth token for tunnel access\n- **tunnel.storeTtlMinutes** — How long viewer links stay cached (default: 60)\n\n### Logging\n- **logging.level** — Log level: silent, debug, info, warn, error, fatal (default: info)\n- **logging.logDir** — Log directory (default: \\`~/.openacp/logs\\`)\n- **logging.maxFileSize** — Max log file size before rotation\n- **logging.maxFiles** — Max number of rotated log files\n- **logging.sessionLogRetentionDays** — Auto-delete old session logs (default: 30)\n\n### Data Retention\n- **sessionStore.ttlDays** — How long session records persist (default: 30). Old records are cleaned up automatically.\n\n### Environment variables\nOverride config with env vars:\n- \\`OPENACP_TELEGRAM_BOT_TOKEN\\`\n- \\`OPENACP_TELEGRAM_CHAT_ID\\`\n- \\`OPENACP_DISCORD_BOT_TOKEN\\`\n- \\`OPENACP_DISCORD_GUILD_ID\\`\n- \\`OPENACP_DEFAULT_AGENT\\`\n- \\`OPENACP_RUN_MODE\\` — foreground or daemon\n- \\`OPENACP_API_PORT\\` — API server port (default: 21420)\n- \\`OPENACP_TUNNEL_ENABLED\\`\n- \\`OPENACP_TUNNEL_PORT\\`\n- \\`OPENACP_TUNNEL_PROVIDER\\`\n- \\`OPENACP_LOG_LEVEL\\`\n- \\`OPENACP_LOG_DIR\\`\n- \\`OPENACP_DEBUG\\` — Sets log level to debug\n\n---\n\n## Troubleshooting\n\n### Session stuck / not responding\n- Check status: ask Assistant \"Is anything stuck?\"\n- Cancel and create new: \\`/cancel\\` then \\`/new\\`\n- Check system health: Assistant can run health check\n\n### Agent not found\n- Check available agents: \\`/agents\\` or \\`openacp agents\\`\n- Install missing agent: \\`openacp agents install <name>\\`\n- Some agents need login first: \\`openacp agents info <name>\\` to see setup steps\n- Run agent CLI for setup: \\`openacp agents run <name> -- <args>\\`\n\n### Permission request not showing\n- Check Notifications topic for the alert\n- Try \\`/enable_dangerous\\` to auto-approve (if you trust the agent)\n\n### Session disappeared after restart\n- Sessions persist across restarts\n- Send a message in the old topic — it auto-resumes\n- If topic was deleted, the session record may still exist in status\n\n### Bot not responding at all\n- Check daemon: \\`openacp status\\`\n- Check logs: \\`openacp logs\\`\n- Restart: \\`openacp start\\` or \\`/restart\\`\n\n### Messages going to wrong topic\n- Each session is bound to a specific topic/thread\n- If you see messages appearing in the Assistant topic instead of the session topic, try creating a new session\n\n### Viewing logs\n- Session-specific logs: \\`~/.openacp/logs/sessions/\\`\n- System logs: \\`openacp logs\\` to tail live\n- Set \\`OPENACP_DEBUG=true\\` for verbose output\n\n---\n\n## Data & Storage\n\nAll data is stored in \\`~/.openacp/\\`:\n- \\`config.json\\` — Configuration\n- \\`agents.json\\` — Installed agents (managed by AgentCatalog)\n- \\`registry-cache.json\\` — Cached ACP Registry data (refreshes every 24h)\n- \\`agents/\\` — Downloaded binary agents\n- \\`sessions/\\` — Session records and state\n- \\`topics/\\` — Topic-to-session mappings\n- \\`logs/\\` — System and session logs\n- \\`plugins/\\` — Installed adapter plugins\n- \\`openacp.pid\\` — Daemon PID file\n\nSession records auto-cleanup: 30 days (configurable via \\`sessionStore.ttlDays\\`).\nSession logs auto-cleanup: 30 days (configurable via \\`logging.sessionLogRetentionDays\\`).\n`;\n"],"mappings":";AAeA,eAAsB,gBACpB,UACA,KACA,SACe;AACf,UAAQ,QAAQ,MAAM;AAAA,IACpB,KAAK;AACH,aAAO,SAAS,OAAO,KAAK,OAAO;AAAA,IACrC,KAAK;AACH,aAAO,SAAS,UAAU,KAAK,OAAO;AAAA,IACxC,KAAK;AACH,aAAO,SAAS,WAAW,KAAK,OAAO;AAAA,IACzC,KAAK;AACH,aAAO,SAAS,aAAa,KAAK,OAAO;AAAA,IAC3C,KAAK;AACH,aAAO,SAAS,OAAO,KAAK,OAAO;AAAA,IACrC,KAAK;AACH,aAAO,SAAS,QAAQ,KAAK,OAAO;AAAA,IACtC,KAAK;AACH,aAAO,SAAS,aAAa,KAAK,OAAO;AAAA,IAC3C,KAAK;AACH,aAAO,SAAS,QAAQ,KAAK,OAAO;AAAA,IACtC,KAAK;AACH,aAAO,SAAS,aAAa,KAAK,OAAO;AAAA,IAC3C,KAAK;AACH,aAAO,SAAS,gBAAgB,KAAK,OAAO;AAAA,IAC9C;AACE;AAAA,EACJ;AACF;;;ACxCO,IAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;","names":[]}