codexbot 1.0.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Quan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # codexbot
2
+
3
+ Control AI coding agents from your phone. codexbot connects **Telegram** or **Slack** to **OpenAI Codex** and **Anthropic Claude**, so you can ask them to read, write, and refactor code — all from a chat message.
4
+
5
+ Send a prompt from Telegram while you're on the go. The agent works in your codebase and streams every step back to you in real time: what it's thinking, which files it's reading, the commands it runs, and the changes it makes.
6
+
7
+ ## Why
8
+
9
+ Sometimes you want to kick off a coding task, review progress, or give follow-up instructions without sitting at your desk. codexbot turns your favorite chat app into a remote terminal for AI agents that can actually write and run code.
10
+
11
+ ## Features
12
+
13
+ - **Two backends** — Codex (default) or Claude, switchable at any time
14
+ - **Real-time streaming** — see agent thoughts, tool usage, command output, and file changes as they happen
15
+ - **Background service** — runs as a daemon with start/stop/status management
16
+ - **Interactive mode** — test locally in your terminal before connecting a chat platform
17
+ - **Chat commands** — control the agent directly from Telegram or Slack (`/status`, `/stop`, `/restart`, etc.)
18
+ - **Telegram command menu** — commands appear in Telegram's autocomplete for easy access
19
+ - **Multi-turn context** — the agent remembers your conversation across messages
20
+ - **Long message handling** — responses are split intelligently to fit platform limits
21
+
22
+ ## Prerequisites
23
+
24
+ - **Node.js** 18 or later
25
+ - **Codex CLI** or **Claude CLI** installed and signed in (either via OAuth or API key — however you normally authenticate)
26
+ - A **Telegram bot token** or a **Slack app** with Socket Mode enabled
27
+
28
+ ## Getting Started
29
+
30
+ ### 1. Install
31
+
32
+ ```bash
33
+ npm install -g codexbot
34
+ ```
35
+
36
+ Or install from source:
37
+
38
+ ```bash
39
+ git clone https://github.com/nicklama/codexbot.git
40
+ cd codexbot
41
+ npm install
42
+ npm link
43
+ ```
44
+
45
+ ### 2. Configure
46
+
47
+ Run the setup wizard:
48
+
49
+ ```bash
50
+ codexbot onboard
51
+ ```
52
+
53
+ You'll be asked to choose a platform (Telegram or Slack), enter your bot tokens, and set a working directory — the folder where the agent will operate.
54
+
55
+ Configuration is stored in `~/.codexbot/config.json`. Run `codexbot onboard` again at any time to update it.
56
+
57
+ ### 3. Start
58
+
59
+ ```bash
60
+ codexbot start
61
+ ```
62
+
63
+ That's it. The bot launches in the background, connects to your chat platform, and is ready to accept prompts.
64
+
65
+ ## Usage
66
+
67
+ ### Managing the service
68
+
69
+ | Command | What it does |
70
+ |---------|-------------|
71
+ | `codexbot start` | Start the bot as a background service (Codex by default) |
72
+ | `codexbot start --claude` | Start with Claude instead of Codex |
73
+ | `codexbot start --disable-yolo` | Start without auto-accepting agent actions |
74
+ | `codexbot stop` | Stop the background service |
75
+ | `codexbot status` | Check whether the service is running |
76
+
77
+ ### Interactive mode
78
+
79
+ If you want to test things locally before connecting a chat platform:
80
+
81
+ ```bash
82
+ codexbot start --interactive
83
+ ```
84
+
85
+ You type messages in the terminal and see the same output that would be sent to Telegram or Slack.
86
+
87
+ ### Chat commands
88
+
89
+ Once the bot is running, send these commands from Telegram or Slack. Both `/` and `$` prefixes work:
90
+
91
+ | Command | What it does |
92
+ |---------|-------------|
93
+ | `/status` | Show the agent's backend, uptime, and working directory |
94
+ | `/stop` | Stop the agent |
95
+ | `/start` | Start the agent (add `--claude` or `--codex` to switch backends) |
96
+ | `/restart` | Restart the agent |
97
+ | `/dir` | Show the current working directory |
98
+ | `/dir /path/to/project` | Change the working directory (restart to apply) |
99
+
100
+ Anything else you type is sent directly to the agent as a prompt.
101
+
102
+ ## Setting Up Telegram
103
+
104
+ 1. Open Telegram and message [@BotFather](https://t.me/BotFather)
105
+ 2. Create a new bot and copy the token
106
+ 3. Run `codexbot onboard` and enter the token
107
+ 4. Optionally restrict access by entering your Telegram user ID (find it via [@userinfobot](https://t.me/userinfobot))
108
+ 5. Run `codexbot start`
109
+
110
+ The bot will register its commands with Telegram automatically, so you'll see them in the autocomplete menu when you type `/`.
111
+
112
+ ## Setting Up Slack
113
+
114
+ 1. Create a new app at [api.slack.com/apps](https://api.slack.com/apps)
115
+ 2. Enable **Socket Mode** and generate an App-Level Token (`xapp-...`)
116
+ 3. Add Bot Token Scopes: `app_mentions:read`, `chat:write`, `im:history`, `im:read`, `im:write`
117
+ 4. Subscribe to events: `app_mention`, `message.im`
118
+ 5. Install the app to your workspace and copy the Bot User OAuth Token (`xoxb-...`)
119
+ 6. Run `codexbot onboard` and enter both tokens
120
+ 7. Run `codexbot start`
121
+
122
+ ## How It Works
123
+
124
+ When you send a message, codexbot forwards it to the AI agent running in your configured working directory. The agent can read files, edit code, run shell commands, search the web, and more — all autonomously.
125
+
126
+ Every action the agent takes is streamed back to your chat in real time:
127
+
128
+ - **Text responses** — the agent's reasoning and answers
129
+ - **Tool usage** — which files it's reading, editing, or creating
130
+ - **Command output** — shell commands and their results
131
+ - **File changes** — a summary of files added, updated, or deleted
132
+
133
+ The agent maintains context across messages, so you can have a natural back-and-forth conversation about your code.
134
+
135
+ ## Logs and Files
136
+
137
+ | Path | Purpose |
138
+ |------|---------|
139
+ | `~/.codexbot/config.json` | Your bot configuration |
140
+ | `~/.codexbot/codexbot.log` | Service logs (useful for debugging) |
141
+ | `~/.codexbot/codexbot.pid` | Process ID of the running service |
142
+
143
+ ## License
144
+
145
+ MIT
package/cli.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+
3
+ const args = process.argv.slice(2);
4
+ const command = args[0];
5
+
6
+ function showHelp() {
7
+ console.log(`
8
+ codexbot - Bridge Slack/Telegram to Claude/Codex via their SDKs
9
+
10
+ Usage:
11
+ codexbot start [options] Start the bot as a background service
12
+ codexbot stop Stop the background service
13
+ codexbot status Show service status
14
+ codexbot onboard Interactive setup (platform, tokens, config)
15
+
16
+ Options:
17
+ --interactive Run in interactive terminal mode
18
+ --claude Use Claude Agent SDK instead of Codex SDK (default: Codex)
19
+ --disable-yolo Disable auto-accept mode
20
+ --help, -h Show this help message
21
+ `);
22
+ }
23
+
24
+ switch (command) {
25
+ case 'start': {
26
+ const rest = args.slice(1);
27
+ if (rest.includes('--interactive')) {
28
+ const { start } = await import('./src/main.js');
29
+ start(rest);
30
+ } else {
31
+ const { startService } = await import('./src/service.js');
32
+ startService(rest);
33
+ }
34
+ break;
35
+ }
36
+ case 'stop': {
37
+ const { stopService } = await import('./src/service.js');
38
+ stopService();
39
+ break;
40
+ }
41
+ case 'status': {
42
+ const { showStatus } = await import('./src/service.js');
43
+ showStatus();
44
+ break;
45
+ }
46
+ case '_serve': {
47
+ // Internal: runs the actual bot (used by background service)
48
+ const { start } = await import('./src/main.js');
49
+ start(args.slice(1));
50
+ break;
51
+ }
52
+ case 'onboard':
53
+ await import('./src/onboard.js');
54
+ break;
55
+ case '--help':
56
+ case '-h':
57
+ case undefined:
58
+ showHelp();
59
+ break;
60
+ default:
61
+ console.error(`Unknown command: ${command}`);
62
+ showHelp();
63
+ process.exit(1);
64
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "codexbot",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Control Claude and Codex AI agents from Telegram or Slack",
6
+ "keywords": [
7
+ "claude",
8
+ "codex",
9
+ "openai",
10
+ "anthropic",
11
+ "telegram",
12
+ "slack",
13
+ "chatbot",
14
+ "ai-agent",
15
+ "cli"
16
+ ],
17
+ "author": "Quan <dinhquan191@gmail.com>",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/dinhquan/codexbot.git"
22
+ },
23
+ "homepage": "https://github.com/dinhquan/codexbot#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/dinhquan/codexbot/issues"
26
+ },
27
+ "main": "cli.js",
28
+ "bin": {
29
+ "codexbot": "./cli.js"
30
+ },
31
+ "files": [
32
+ "cli.js",
33
+ "src/",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "scripts": {
41
+ "start": "node cli.js start"
42
+ },
43
+ "dependencies": {
44
+ "@anthropic-ai/claude-agent-sdk": "^0.2.74",
45
+ "@openai/codex-sdk": "^0.114.0",
46
+ "@slack/bolt": "^4.6.0",
47
+ "@slack/web-api": "^7.13.0",
48
+ "telegraf": "^4.16.0"
49
+ }
50
+ }
package/src/agent.js ADDED
@@ -0,0 +1,269 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ export default class Agent extends EventEmitter {
4
+ constructor(opts = {}) {
5
+ super();
6
+ this.backend = opts.backend || 'codex'; // 'codex' or 'claude'
7
+ this.workingDir = opts.workingDir || process.cwd();
8
+ this.disableYolo = opts.disableYolo || false;
9
+ this.running = false;
10
+ this.busy = false;
11
+ this.startTime = null;
12
+ this._abortController = null;
13
+
14
+ // Claude-specific
15
+ this._claudeSessionId = null;
16
+
17
+ // Codex-specific
18
+ this._codexClient = null;
19
+ this._codexThread = null;
20
+ }
21
+
22
+ start() {
23
+ this.running = true;
24
+ this.startTime = Date.now();
25
+
26
+ if (this.backend === 'codex') {
27
+ this._initCodex();
28
+ }
29
+
30
+ console.log(`[codexbot] Agent started (${this.backend}) in ${this.workingDir}`);
31
+ }
32
+
33
+ async _initCodex() {
34
+ const { Codex } = await import('@openai/codex-sdk');
35
+ this._codexClient = new Codex();
36
+ this._codexThread = this._codexClient.startThread({
37
+ workingDirectory: this.workingDir,
38
+ approvalPolicy: this.disableYolo ? 'on-request' : 'never',
39
+ sandboxMode: this.disableYolo ? 'read-only' : 'danger-full-access',
40
+ });
41
+ }
42
+
43
+ async sendCommand(text) {
44
+ if (!this.running) return false;
45
+ if (this.busy) return true;
46
+
47
+ this.busy = true;
48
+ this._abortController = new AbortController();
49
+
50
+ try {
51
+ if (this.backend === 'claude') {
52
+ await this._runClaude(text);
53
+ } else {
54
+ await this._runCodex(text);
55
+ }
56
+ } catch (err) {
57
+ if (err.name !== 'AbortError') {
58
+ console.error(`[codexbot] Agent error:`, err.message);
59
+ this.emit('message', `Error: ${err.message}`);
60
+ }
61
+ } finally {
62
+ this.busy = false;
63
+ this._abortController = null;
64
+ this.emit('done');
65
+ }
66
+
67
+ return true;
68
+ }
69
+
70
+ async _runClaude(text) {
71
+ const { query } = await import('@anthropic-ai/claude-agent-sdk');
72
+
73
+ const options = {
74
+ cwd: this.workingDir,
75
+ allowedTools: ['Read', 'Edit', 'Write', 'Bash', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Agent'],
76
+ permissionMode: this.disableYolo ? 'default' : 'bypassPermissions',
77
+ allowDangerouslySkipPermissions: !this.disableYolo,
78
+ abortController: this._abortController,
79
+ maxTurns: 50,
80
+ };
81
+
82
+ if (this._claudeSessionId) {
83
+ options.resume = this._claudeSessionId;
84
+ }
85
+
86
+ const stream = query({ prompt: text, options });
87
+
88
+ for await (const message of stream) {
89
+ console.log(`[codexbot:claude] message: ${message.type}${message.subtype ? `:${message.subtype}` : ''}`);
90
+
91
+ if (message.type === 'system' && message.subtype === 'init') {
92
+ this._claudeSessionId = message.session_id;
93
+ console.log(`[codexbot:claude] session: ${message.session_id}`);
94
+ }
95
+
96
+ if (message.type === 'assistant') {
97
+ for (const block of message.message.content) {
98
+ if (block.type === 'text' && block.text.trim()) {
99
+ console.log(`[codexbot:claude] text (${block.text.length} chars)`);
100
+ this.emit('message', block.text);
101
+ }
102
+ if (block.type === 'tool_use') {
103
+ console.log(`[codexbot:claude] tool_use: ${block.name}`);
104
+ const msg = this._formatClaudeToolUse(block);
105
+ if (msg) this.emit('message', msg);
106
+ }
107
+ }
108
+ }
109
+
110
+ if (message.type === 'user') {
111
+ // Tool results
112
+ const content = message.message?.content;
113
+ if (Array.isArray(content)) {
114
+ for (const block of content) {
115
+ if (block.type === 'tool_result') {
116
+ const msg = this._formatClaudeToolResult(block);
117
+ if (msg) this.emit('message', msg);
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ if (message.type === 'result') {
124
+ console.log(`[codexbot:claude] result: ${message.subtype}${message.total_cost_usd ? ` ($${message.total_cost_usd.toFixed(4)})` : ''}`);
125
+ if (message.subtype !== 'success') {
126
+ this.emit('message', `Agent finished with: ${message.subtype}`);
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ async _runCodex(text) {
133
+ if (!this._codexThread) {
134
+ await this._initCodex();
135
+ }
136
+
137
+ const { events } = await this._codexThread.runStreamed(text, {
138
+ signal: this._abortController.signal,
139
+ });
140
+
141
+ for await (const event of events) {
142
+ console.log(`[codexbot:codex] event: ${event.type}`, event.type === 'item.completed' || event.type === 'item.started' ? `item.type=${event.item?.type}` : '');
143
+
144
+ if (event.type === 'item.completed') {
145
+ const item = event.item;
146
+ console.log(`[codexbot:codex] item.completed:`, JSON.stringify(item, null, 2).slice(0, 500));
147
+
148
+ const msg = this._formatCodexItem(item);
149
+ if (msg) this.emit('message', msg);
150
+ }
151
+
152
+ if (event.type === 'turn.completed') {
153
+ console.log(`[codexbot:codex] turn.completed`);
154
+ }
155
+
156
+ if (event.type === 'turn.failed') {
157
+ console.log(`[codexbot:codex] turn.failed:`, event.error);
158
+ this.emit('message', `Error: ${event.error?.message || 'Unknown error'}`);
159
+ }
160
+ }
161
+ }
162
+
163
+ _formatClaudeToolUse(block) {
164
+ const name = block.name;
165
+ const input = block.input || {};
166
+
167
+ if (name === 'Bash') {
168
+ return `\`$ ${input.command || ''}\``;
169
+ }
170
+ if (name === 'Read') {
171
+ return `Reading \`${input.file_path || ''}\``;
172
+ }
173
+ if (name === 'Edit') {
174
+ return `Editing \`${input.file_path || ''}\``;
175
+ }
176
+ if (name === 'Write') {
177
+ return `Writing \`${input.file_path || ''}\``;
178
+ }
179
+ if (name === 'Glob') {
180
+ return `Searching files: \`${input.pattern || ''}\``;
181
+ }
182
+ if (name === 'Grep') {
183
+ return `Searching for: \`${input.pattern || ''}\``;
184
+ }
185
+ // Other tools — just show the name
186
+ return `Using tool: ${name}`;
187
+ }
188
+
189
+ _formatClaudeToolResult(block) {
190
+ const content = block.content;
191
+ if (!content) return null;
192
+
193
+ // Content can be a string or array of blocks
194
+ if (typeof content === 'string') {
195
+ if (!content.trim()) return null;
196
+ const trimmed = content.length > 2000 ? content.slice(0, 2000) + '...' : content;
197
+ return `\`\`\`\n${trimmed}\n\`\`\``;
198
+ }
199
+
200
+ if (Array.isArray(content)) {
201
+ const texts = content
202
+ .filter(b => b.type === 'text' && b.text?.trim())
203
+ .map(b => b.text);
204
+ if (!texts.length) return null;
205
+ const joined = texts.join('\n');
206
+ const trimmed = joined.length > 2000 ? joined.slice(0, 2000) + '...' : joined;
207
+ return `\`\`\`\n${trimmed}\n\`\`\``;
208
+ }
209
+
210
+ return null;
211
+ }
212
+
213
+ _formatCodexItem(item) {
214
+ switch (item.type) {
215
+ case 'agent_message': {
216
+ const text = item.text || '';
217
+ return text.trim() || null;
218
+ }
219
+
220
+ case 'command_execution': {
221
+ const cmd = item.command || '';
222
+ const output = item.aggregated_output || '';
223
+ const failed = item.status === 'failed' || (item.exit_code && item.exit_code !== 0);
224
+ const statusTag = failed ? ' (failed)' : '';
225
+ let msg = `\`$ ${cmd}\`${statusTag}`;
226
+ if (output) {
227
+ const trimmed = output.length > 2000 ? output.slice(0, 2000) + '...' : output;
228
+ msg += `\n\`\`\`\n${trimmed}\n\`\`\``;
229
+ }
230
+ return msg;
231
+ }
232
+
233
+ case 'file_change': {
234
+ const changes = item.changes || [];
235
+ if (!changes.length) return null;
236
+ const lines = changes.map(c => ` ${c.kind}: ${c.path}`);
237
+ return `File changes:\n${lines.join('\n')}`;
238
+ }
239
+
240
+ default: {
241
+ // Forward any other item type with its text if present
242
+ if (item.text && item.text.trim()) return item.text;
243
+ return null;
244
+ }
245
+ }
246
+ }
247
+
248
+ stop() {
249
+ if (this._abortController) {
250
+ this._abortController.abort();
251
+ }
252
+ this._codexClient = null;
253
+ this._codexThread = null;
254
+ this._claudeSessionId = null;
255
+ this.running = false;
256
+ this.busy = false;
257
+ console.log('[codexbot] Agent stopped');
258
+ }
259
+
260
+ getStatus() {
261
+ return {
262
+ running: this.running,
263
+ busy: this.busy,
264
+ uptime: this.startTime ? Math.floor((Date.now() - this.startTime) / 1000) : 0,
265
+ backend: this.backend,
266
+ cwd: this.workingDir,
267
+ };
268
+ }
269
+ }
package/src/chunker.js ADDED
@@ -0,0 +1,50 @@
1
+ export function chunkMessage(text, maxLen = 4000) {
2
+ if (text.length <= maxLen) return [text];
3
+
4
+ const chunks = [];
5
+ let remaining = text;
6
+
7
+ while (remaining.length > 0) {
8
+ if (remaining.length <= maxLen) {
9
+ chunks.push(remaining);
10
+ break;
11
+ }
12
+
13
+ let splitAt = -1;
14
+
15
+ // Try splitting at double newline (paragraph boundary)
16
+ const paraIdx = remaining.lastIndexOf('\n\n', maxLen);
17
+ if (paraIdx > maxLen * 0.3) {
18
+ splitAt = paraIdx + 2;
19
+ }
20
+
21
+ // Try splitting at single newline
22
+ if (splitAt === -1) {
23
+ const lineIdx = remaining.lastIndexOf('\n', maxLen);
24
+ if (lineIdx > maxLen * 0.3) {
25
+ splitAt = lineIdx + 1;
26
+ }
27
+ }
28
+
29
+ // Try splitting at space
30
+ if (splitAt === -1) {
31
+ const spaceIdx = remaining.lastIndexOf(' ', maxLen);
32
+ if (spaceIdx > maxLen * 0.3) {
33
+ splitAt = spaceIdx + 1;
34
+ }
35
+ }
36
+
37
+ // Hard split as last resort
38
+ if (splitAt === -1) {
39
+ splitAt = maxLen;
40
+ }
41
+
42
+ chunks.push(remaining.slice(0, splitAt));
43
+ remaining = remaining.slice(splitAt);
44
+ }
45
+
46
+ if (chunks.length > 1) {
47
+ return chunks.map((chunk, i) => `[${i + 1}/${chunks.length}]\n${chunk}`);
48
+ }
49
+ return chunks;
50
+ }
@@ -0,0 +1,74 @@
1
+ import fs from 'fs';
2
+
3
+ export const COMMANDS = [
4
+ { command: 'status', description: 'Show agent status' },
5
+ { command: 'stop', description: 'Stop the agent' },
6
+ { command: 'start', description: 'Start the agent' },
7
+ { command: 'restart', description: 'Restart the agent' },
8
+ { command: 'dir', description: 'Show or change working directory' },
9
+ ];
10
+
11
+ export function handle(text, agent) {
12
+ const trimmed = text.trim();
13
+ if (!trimmed.startsWith('$') && !trimmed.startsWith('/')) return { handled: false };
14
+
15
+ const parts = trimmed.split(/\s+/);
16
+ // Normalize: strip leading $ or / to get the command name
17
+ const cmd = parts[0].slice(1).toLowerCase();
18
+
19
+ switch (cmd) {
20
+ case 'status': {
21
+ const s = agent.getStatus();
22
+ const upMin = Math.floor(s.uptime / 60);
23
+ const upSec = s.uptime % 60;
24
+ const response = s.running
25
+ ? `Running: ${s.backend}${s.busy ? ' (busy)' : ''}\nUptime: ${upMin}m ${upSec}s\nCWD: ${s.cwd}`
26
+ : 'Agent is not running.';
27
+ return { handled: true, response };
28
+ }
29
+
30
+ case 'stop': {
31
+ agent.stop();
32
+ return { handled: true, response: 'Agent stopped.' };
33
+ }
34
+
35
+ case 'start': {
36
+ if (agent.running) {
37
+ return { handled: true, response: 'Agent is already running.' };
38
+ }
39
+ applyFlags(agent, parts.slice(1));
40
+ agent.start();
41
+ return { handled: true, response: 'Agent started.' };
42
+ }
43
+
44
+ case 'restart': {
45
+ agent.stop();
46
+ applyFlags(agent, parts.slice(1));
47
+ agent.start();
48
+ return { handled: true, response: 'Agent restarted.' };
49
+ }
50
+
51
+ case 'dir': {
52
+ const dir = parts[1];
53
+ if (!dir) {
54
+ return { handled: true, response: `Current directory: ${agent.workingDir}` };
55
+ }
56
+ if (!fs.existsSync(dir)) {
57
+ return { handled: true, response: `Directory not found: ${dir}` };
58
+ }
59
+ agent.workingDir = dir;
60
+ return { handled: true, response: `Working directory set to: ${dir}\nRestart agent for it to take effect.` };
61
+ }
62
+
63
+ default:
64
+ return { handled: false };
65
+ }
66
+ }
67
+
68
+ function applyFlags(agent, flags) {
69
+ for (const flag of flags) {
70
+ if (flag === '--codex') agent.backend = 'codex';
71
+ if (flag === '--claude') agent.backend = 'claude';
72
+ if (flag === '--disable-yolo') agent.disableYolo = true;
73
+ }
74
+ }
package/src/config.js ADDED
@@ -0,0 +1,24 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ export const CONFIG_DIR = path.join(os.homedir(), '.codexbot');
6
+ export const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
7
+
8
+ export function loadConfig() {
9
+ try {
10
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
11
+ } catch {
12
+ return {};
13
+ }
14
+ }
15
+
16
+ export function saveConfig(obj) {
17
+ if (!fs.existsSync(CONFIG_DIR)) {
18
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
19
+ }
20
+ const clean = Object.fromEntries(
21
+ Object.entries(obj).filter(([, v]) => v !== undefined && v !== '')
22
+ );
23
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(clean, null, 2) + '\n');
24
+ }
package/src/main.js ADDED
@@ -0,0 +1,177 @@
1
+ import readline from 'readline';
2
+ import { loadConfig } from './config.js';
3
+ import Agent from './agent.js';
4
+ import { handle as handleCommand, COMMANDS } from './commands.js';
5
+ import { chunkMessage } from './chunker.js';
6
+
7
+ export async function start(args = []) {
8
+ const interactive = args.includes('--interactive');
9
+ const config = loadConfig();
10
+ const useClaude = args.includes('--claude');
11
+ const disableYolo = args.includes('--disable-yolo');
12
+
13
+ // Create agent
14
+ const agent = new Agent({
15
+ backend: useClaude ? 'claude' : 'codex',
16
+ disableYolo,
17
+ workingDir: config.WORKING_DIR || process.cwd(),
18
+ });
19
+
20
+ // Platform adapter (only for non-interactive mode)
21
+ let platform = null;
22
+
23
+ if (!interactive) {
24
+ if (!config.PLATFORM) {
25
+ console.error('No platform configured. Run "codexbot onboard" first.');
26
+ process.exit(1);
27
+ }
28
+
29
+ if (config.PLATFORM === 'slack') {
30
+ if (!config.SLACK_BOT_TOKEN || !config.SLACK_APP_TOKEN) {
31
+ console.error('Missing Slack tokens. Run "codexbot onboard" to configure.');
32
+ process.exit(1);
33
+ }
34
+ const { default: SlackAdapter } = await import('./platforms/slack.js');
35
+ platform = new SlackAdapter(config);
36
+ } else if (config.PLATFORM === 'telegram') {
37
+ if (!config.TELEGRAM_BOT_TOKEN) {
38
+ console.error('Missing Telegram bot token. Run "codexbot onboard" to configure.');
39
+ process.exit(1);
40
+ }
41
+ const { default: TelegramAdapter } = await import('./platforms/telegram.js');
42
+ platform = new TelegramAdapter(config);
43
+ } else {
44
+ console.error(`Unknown platform: ${config.PLATFORM}`);
45
+ process.exit(1);
46
+ }
47
+ }
48
+
49
+ if (interactive) {
50
+ startInteractive(agent);
51
+ } else {
52
+ startService(agent, platform, config);
53
+ }
54
+ }
55
+
56
+ function startInteractive(agent) {
57
+ const rl = readline.createInterface({
58
+ input: process.stdin,
59
+ output: process.stdout,
60
+ prompt: '\nyou> ',
61
+ });
62
+
63
+ agent.on('message', (text) => {
64
+ console.log(`\nbot> ${text}`);
65
+ rl.prompt();
66
+ });
67
+
68
+ agent.on('done', () => {
69
+ rl.prompt();
70
+ });
71
+
72
+ agent.start();
73
+ console.log(`[codexbot] Interactive mode (${agent.backend}). Type your message or Ctrl+C to quit.\n`);
74
+ rl.prompt();
75
+
76
+ rl.on('line', async (line) => {
77
+ const text = line.trim();
78
+ if (!text) { rl.prompt(); return; }
79
+
80
+ const result = handleCommand(text, agent);
81
+ if (result.handled) {
82
+ if (result.response) console.log(result.response);
83
+ rl.prompt();
84
+ return;
85
+ }
86
+
87
+ const sent = await agent.sendCommand(text);
88
+ if (!sent) {
89
+ console.log('Agent is not running. Use $start to start it.');
90
+ rl.prompt();
91
+ }
92
+ });
93
+
94
+ rl.on('close', () => {
95
+ agent.stop();
96
+ process.exit(0);
97
+ });
98
+ }
99
+
100
+ function startService(agent, platform, config) {
101
+ let currentCtx = null;
102
+
103
+ agent.on('message', async (text) => {
104
+ if (!currentCtx) return;
105
+ if (platform.stopTyping) platform.stopTyping();
106
+ await sendResponse(platform, currentCtx, text);
107
+ });
108
+
109
+ agent.on('done', () => {
110
+ if (platform.stopTyping) platform.stopTyping();
111
+ });
112
+
113
+ platform.onMessage(async (msg) => {
114
+ currentCtx = {
115
+ chatId: msg.chatId,
116
+ threadId: msg.threadId,
117
+ replyToMessageId: msg.replyToMessageId,
118
+ platform: config.PLATFORM,
119
+ };
120
+
121
+ if (platform.startTyping) {
122
+ platform.startTyping(msg.chatId);
123
+ }
124
+
125
+ const result = handleCommand(msg.text, agent);
126
+ if (result.handled) {
127
+ if (result.response) {
128
+ if (platform.stopTyping) platform.stopTyping();
129
+ await sendResponse(platform, currentCtx, result.response);
130
+ }
131
+ return;
132
+ }
133
+
134
+ const sent = await agent.sendCommand(msg.text);
135
+ if (!sent) {
136
+ if (platform.stopTyping) platform.stopTyping();
137
+ await sendResponse(platform, currentCtx, 'Agent is not running. Use $start to start it.');
138
+ }
139
+ });
140
+
141
+ agent.start();
142
+
143
+ // Register bot commands if platform supports it
144
+ if (platform.setCommands) {
145
+ platform.setCommands(COMMANDS);
146
+ }
147
+
148
+ platform.start().then(() => {
149
+ console.log('[codexbot] Bot is running. Press Ctrl+C to stop.');
150
+ });
151
+
152
+ const shutdown = async () => {
153
+ console.log('\n[codexbot] Shutting down...');
154
+ agent.stop();
155
+ await platform.stop().catch(() => { });
156
+ process.exit(0);
157
+ };
158
+
159
+ process.on('SIGINT', shutdown);
160
+ process.on('SIGTERM', shutdown);
161
+ }
162
+
163
+ async function sendResponse(platform, ctx, text) {
164
+ const maxLen = platform.maxMessageLen || 4000;
165
+ const chunks = chunkMessage(text, maxLen);
166
+
167
+ for (const chunk of chunks) {
168
+ try {
169
+ await platform.sendMessage(ctx.chatId, chunk, {
170
+ threadId: ctx.threadId,
171
+ replyToMessageId: ctx.replyToMessageId,
172
+ });
173
+ } catch (err) {
174
+ console.error('[codexbot] Failed to send message:', err.message);
175
+ }
176
+ }
177
+ }
package/src/onboard.js ADDED
@@ -0,0 +1,81 @@
1
+ import fs from 'fs';
2
+ import readline from 'readline';
3
+ import { loadConfig, saveConfig, CONFIG_PATH } from './config.js';
4
+
5
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6
+
7
+ function ask(question, defaultVal) {
8
+ return new Promise(resolve => {
9
+ const suffix = defaultVal ? ` [${defaultVal}]` : '';
10
+ rl.question(`${question}${suffix}: `, answer => {
11
+ resolve(answer.trim() || defaultVal || '');
12
+ });
13
+ });
14
+ }
15
+
16
+ async function onboard() {
17
+ console.log('\n=== codexbot Setup ===\n');
18
+
19
+ const existing = loadConfig();
20
+
21
+ // If config exists, offer to keep it
22
+ if (existing.PLATFORM) {
23
+ console.log('Current config:');
24
+ console.log(` Platform: ${existing.PLATFORM}`);
25
+ if (existing.WORKING_DIR) console.log(` Working dir: ${existing.WORKING_DIR}`);
26
+ console.log();
27
+ const keep = await ask('Keep current config? (y/n)', 'y');
28
+ if (keep.toLowerCase() === 'y') {
29
+ console.log('Config unchanged.');
30
+ rl.close();
31
+ return;
32
+ }
33
+ console.log();
34
+ }
35
+
36
+ const config = { ...existing };
37
+
38
+ console.log('Choose platform:');
39
+ console.log(' 1) Slack');
40
+ console.log(' 2) Telegram');
41
+ const platformChoice = await ask('Platform (1 or 2)', config.PLATFORM === 'telegram' ? '2' : '1');
42
+ config.PLATFORM = platformChoice === '2' ? 'telegram' : 'slack';
43
+
44
+ if (config.PLATFORM === 'slack') {
45
+ console.log('\n--- Slack Configuration ---');
46
+ config.SLACK_BOT_TOKEN = await ask('Bot Token (xoxb-...)', config.SLACK_BOT_TOKEN);
47
+ config.SLACK_APP_TOKEN = await ask('App-Level Token (xapp-...)', config.SLACK_APP_TOKEN);
48
+ config.SLACK_USER_ID = await ask('Your Slack User ID (optional, for auth)', config.SLACK_USER_ID);
49
+ // Clear telegram keys
50
+ delete config.TELEGRAM_BOT_TOKEN;
51
+ delete config.TELEGRAM_ALLOWED_USER_IDS;
52
+ } else {
53
+ console.log('\n--- Telegram Configuration ---');
54
+ config.TELEGRAM_BOT_TOKEN = await ask('Bot Token', config.TELEGRAM_BOT_TOKEN);
55
+ config.TELEGRAM_ALLOWED_USER_IDS = await ask('Allowed User IDs (comma-separated, optional)', config.TELEGRAM_ALLOWED_USER_IDS);
56
+ // Clear slack keys
57
+ delete config.SLACK_BOT_TOKEN;
58
+ delete config.SLACK_APP_TOKEN;
59
+ delete config.SLACK_USER_ID;
60
+ }
61
+
62
+ let workingDir = config.WORKING_DIR || process.cwd();
63
+ while (true) {
64
+ workingDir = await ask('\nWorking directory (optional)', workingDir);
65
+ if (!workingDir || fs.existsSync(workingDir)) break;
66
+ console.log(`Directory does not exist: ${workingDir}`);
67
+ }
68
+ config.WORKING_DIR = workingDir;
69
+
70
+ saveConfig(config);
71
+ console.log(`\nConfig saved to ${CONFIG_PATH}`);
72
+ console.log('Run "codexbot start" to launch the bot.\n');
73
+
74
+ rl.close();
75
+ }
76
+
77
+ onboard().catch(err => {
78
+ console.error('Setup failed:', err.message);
79
+ rl.close();
80
+ process.exit(1);
81
+ });
@@ -0,0 +1,66 @@
1
+ import pkg from '@slack/bolt';
2
+ const { App } = pkg;
3
+
4
+ export default class SlackAdapter {
5
+ constructor(config) {
6
+ this.config = config;
7
+ this.app = new App({
8
+ token: config.SLACK_BOT_TOKEN,
9
+ appToken: config.SLACK_APP_TOKEN,
10
+ socketMode: true,
11
+ });
12
+ this._messageCallback = null;
13
+ this.maxMessageLen = 4000;
14
+ }
15
+
16
+ async start() {
17
+ // Listen for app mentions
18
+ this.app.event('app_mention', async ({ event }) => {
19
+ if (this._messageCallback) {
20
+ const text = event.text.replace(/<@[A-Z0-9]+>/g, '').trim();
21
+ this._messageCallback({
22
+ text,
23
+ chatId: event.channel,
24
+ threadId: event.thread_ts || event.ts,
25
+ userId: event.user,
26
+ });
27
+ }
28
+ });
29
+
30
+ // Listen for DMs
31
+ this.app.event('message', async ({ event }) => {
32
+ if (event.channel_type !== 'im') return;
33
+ if (event.subtype) return;
34
+ if (this._messageCallback) {
35
+ this._messageCallback({
36
+ text: event.text || '',
37
+ chatId: event.channel,
38
+ threadId: event.thread_ts || event.ts,
39
+ userId: event.user,
40
+ });
41
+ }
42
+ });
43
+
44
+ await this.app.start();
45
+ console.log('[codexbot] Slack connected (Socket Mode)');
46
+ }
47
+
48
+ onMessage(callback) {
49
+ this._messageCallback = callback;
50
+ }
51
+
52
+ async sendMessage(chatId, text, opts = {}) {
53
+ const params = {
54
+ channel: chatId,
55
+ text,
56
+ };
57
+ if (opts.threadId) {
58
+ params.thread_ts = opts.threadId;
59
+ }
60
+ await this.app.client.chat.postMessage(params);
61
+ }
62
+
63
+ async stop() {
64
+ await this.app.stop();
65
+ }
66
+ }
@@ -0,0 +1,85 @@
1
+ import { Telegraf } from 'telegraf';
2
+
3
+ export default class TelegramAdapter {
4
+ constructor(config) {
5
+ this.config = config;
6
+ this.bot = new Telegraf(config.TELEGRAM_BOT_TOKEN);
7
+ this._messageCallback = null;
8
+ this.maxMessageLen = 4096;
9
+
10
+ // Parse allowed user IDs
11
+ this.allowedUserIds = [];
12
+ if (config.TELEGRAM_ALLOWED_USER_IDS) {
13
+ this.allowedUserIds = config.TELEGRAM_ALLOWED_USER_IDS
14
+ .split(',')
15
+ .map(id => id.trim())
16
+ .filter(Boolean);
17
+ }
18
+ }
19
+
20
+ async start() {
21
+ this.bot.on('text', (ctx) => {
22
+ const userId = String(ctx.from.id);
23
+
24
+ // Filter by allowed users if configured
25
+ if (this.allowedUserIds.length > 0 && !this.allowedUserIds.includes(userId)) {
26
+ return;
27
+ }
28
+
29
+ if (this._messageCallback) {
30
+ this._messageCallback({
31
+ text: ctx.message.text,
32
+ chatId: ctx.chat.id,
33
+ replyToMessageId: ctx.message.message_id,
34
+ userId,
35
+ });
36
+ }
37
+ });
38
+
39
+ this.bot.launch();
40
+
41
+ // Register bot commands so they appear in Telegram's command menu
42
+ if (this._commands) {
43
+ this.bot.telegram.setMyCommands(this._commands).catch(() => { });
44
+ }
45
+
46
+ console.log('[codexbot] Telegram bot started');
47
+
48
+ // Graceful stop on signals
49
+ process.once('SIGINT', () => this.bot.stop('SIGINT'));
50
+ process.once('SIGTERM', () => this.bot.stop('SIGTERM'));
51
+ }
52
+
53
+ onMessage(callback) {
54
+ this._messageCallback = callback;
55
+ }
56
+
57
+ setCommands(commands) {
58
+ this._commands = commands;
59
+ }
60
+
61
+ startTyping(chatId) {
62
+ this.stopTyping();
63
+ const send = () => {
64
+ this.bot.telegram.sendChatAction(chatId, 'typing').catch(() => { });
65
+ };
66
+ send();
67
+ this._typingInterval = setInterval(send, 5000);
68
+ }
69
+
70
+ stopTyping() {
71
+ if (this._typingInterval) {
72
+ clearInterval(this._typingInterval);
73
+ this._typingInterval = null;
74
+ }
75
+ }
76
+
77
+ async sendMessage(chatId, text) {
78
+ this.stopTyping();
79
+ await this.bot.telegram.sendMessage(chatId, text);
80
+ }
81
+
82
+ async stop() {
83
+ this.bot.stop();
84
+ }
85
+ }
package/src/service.js ADDED
@@ -0,0 +1,101 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { spawn } from 'child_process';
4
+ import { fileURLToPath } from 'url';
5
+ import { CONFIG_DIR, loadConfig } from './config.js';
6
+
7
+ const PID_FILE = path.join(CONFIG_DIR, 'codexbot.pid');
8
+ const LOG_FILE = path.join(CONFIG_DIR, 'codexbot.log');
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const CLI_PATH = path.join(__dirname, '..', 'cli.js');
11
+
12
+ export function startService(args = []) {
13
+ const config = loadConfig();
14
+ if (!config.PLATFORM) {
15
+ console.error('No platform configured. Run "codexbot onboard" first.');
16
+ process.exit(1);
17
+ }
18
+
19
+ const existing = getServicePid();
20
+ if (existing) {
21
+ console.error(`codexbot is already running (PID ${existing}). Use "codexbot stop" first.`);
22
+ process.exit(1);
23
+ }
24
+
25
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
26
+
27
+ const logFd = fs.openSync(LOG_FILE, 'a');
28
+
29
+ const child = spawn(process.execPath, [CLI_PATH, '_serve', ...args], {
30
+ detached: true,
31
+ stdio: ['ignore', logFd, logFd],
32
+ env: process.env,
33
+ });
34
+
35
+ fs.writeFileSync(PID_FILE, String(child.pid));
36
+ child.unref();
37
+ fs.closeSync(logFd);
38
+
39
+ console.log(`codexbot started (PID ${child.pid})`);
40
+ console.log(`Logs: ${LOG_FILE}`);
41
+ }
42
+
43
+ export function stopService() {
44
+ const pid = getServicePid();
45
+ if (!pid) {
46
+ console.log('codexbot is not running.');
47
+ return;
48
+ }
49
+
50
+ try {
51
+ process.kill(pid, 'SIGTERM');
52
+ console.log(`codexbot stopped (PID ${pid})`);
53
+ } catch (err) {
54
+ if (err.code === 'ESRCH') {
55
+ console.log('codexbot process not found (stale PID file). Cleaning up.');
56
+ } else {
57
+ console.error(`Failed to stop codexbot: ${err.message}`);
58
+ }
59
+ }
60
+
61
+ cleanupPid();
62
+ }
63
+
64
+ export function showStatus() {
65
+ const pid = getServicePid();
66
+ if (!pid) {
67
+ console.log('codexbot is not running.');
68
+ return;
69
+ }
70
+
71
+ const alive = isProcessAlive(pid);
72
+ if (alive) {
73
+ console.log(`codexbot is running (PID ${pid})`);
74
+ console.log(`Logs: ${LOG_FILE}`);
75
+ } else {
76
+ console.log('codexbot is not running (stale PID file). Cleaning up.');
77
+ cleanupPid();
78
+ }
79
+ }
80
+
81
+ function getServicePid() {
82
+ try {
83
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim(), 10);
84
+ return isNaN(pid) ? null : pid;
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ function isProcessAlive(pid) {
91
+ try {
92
+ process.kill(pid, 0);
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ function cleanupPid() {
100
+ try { fs.unlinkSync(PID_FILE); } catch { }
101
+ }