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 +21 -0
- package/README.md +145 -0
- package/cli.js +64 -0
- package/package.json +50 -0
- package/src/agent.js +269 -0
- package/src/chunker.js +50 -0
- package/src/commands.js +74 -0
- package/src/config.js +24 -0
- package/src/main.js +177 -0
- package/src/onboard.js +81 -0
- package/src/platforms/slack.js +66 -0
- package/src/platforms/telegram.js +85 -0
- package/src/service.js +101 -0
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
|
+
}
|
package/src/commands.js
ADDED
|
@@ -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
|
+
}
|