relaybot 1.0.3 → 1.0.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.
- package/.claude/settings.local.json +15 -0
- package/README.md +21 -7
- package/cli.js +2 -0
- package/config.conf.example +2 -0
- package/main.js +1 -1
- package/package.json +1 -1
- package/setup.js +11 -5
- package/skills/relay-bot/SKILL.md +3 -1
- package/src/agent.js +30 -3
- package/src/send-slack-message.js +27 -5
- package/src/slack-handlers.js +18 -2
package/README.md
CHANGED
|
@@ -107,8 +107,14 @@ Then run:
|
|
|
107
107
|
# Configure Slack credentials
|
|
108
108
|
relaybot setup
|
|
109
109
|
|
|
110
|
-
# Start the bot
|
|
110
|
+
# Start the bot (Claude by default)
|
|
111
111
|
relaybot start
|
|
112
|
+
|
|
113
|
+
# Start the bot with Codex
|
|
114
|
+
relaybot start --codex
|
|
115
|
+
|
|
116
|
+
# Start the bot without auto-approve flags
|
|
117
|
+
relaybot start --noyolo
|
|
112
118
|
```
|
|
113
119
|
|
|
114
120
|
### Option 2: Run from Source
|
|
@@ -124,8 +130,16 @@ npm install
|
|
|
124
130
|
# Configure Slack credentials
|
|
125
131
|
npm run setup
|
|
126
132
|
|
|
127
|
-
# Start the bot
|
|
133
|
+
# Start the bot (Claude by default)
|
|
128
134
|
npm start
|
|
135
|
+
|
|
136
|
+
# Start the bot with Codex
|
|
137
|
+
node main.js --codex
|
|
138
|
+
|
|
139
|
+
# Start the bot without auto-approve flags
|
|
140
|
+
node main.js --noyolo
|
|
141
|
+
|
|
142
|
+
`--noyolo` disables passing `--yolo` (Codex) or `--dangerously-skip-permissions` (Claude) to the underlying CLI.
|
|
129
143
|
```
|
|
130
144
|
|
|
131
145
|
---
|
|
@@ -149,8 +163,8 @@ npm run setup
|
|
|
149
163
|
You will be asked for:
|
|
150
164
|
- **SLACK_BOT_TOKEN** - Bot User OAuth Token (starts with `xoxb-`)
|
|
151
165
|
- **SLACK_APP_TOKEN** - App-Level Token (starts with `xapp-`)
|
|
152
|
-
- **SLACK_USER_ID** - Your Slack User ID (starts with `U`)
|
|
153
|
-
- **WORKING_DIR** - Directory where the AI will operate
|
|
166
|
+
- **SLACK_USER_ID** (optional) - Your Slack User ID (starts with `U`). Provide this to prevent the bot from replying to other users.
|
|
167
|
+
- **WORKING_DIR** (optional) - Directory where the AI will operate. Set this to give the bot better context about where to work.
|
|
154
168
|
|
|
155
169
|
### Manual Setup
|
|
156
170
|
|
|
@@ -234,7 +248,7 @@ Socket Mode allows the bot to receive events via WebSocket instead of HTTP endpo
|
|
|
234
248
|
|-------|---------------|--------|
|
|
235
249
|
| `SLACK_BOT_TOKEN` | OAuth & Permissions → Bot User OAuth Token | `xoxb-...` |
|
|
236
250
|
| `SLACK_APP_TOKEN` | Basic Information → App-Level Tokens | `xapp-...` |
|
|
237
|
-
| `SLACK_USER_ID` | Slack Profile → Copy member ID | `U0XXXXXXXX` |
|
|
251
|
+
| `SLACK_USER_ID` | Slack Profile → Copy member ID | `U0XXXXXXXX` (optional) |
|
|
238
252
|
|
|
239
253
|
---
|
|
240
254
|
|
|
@@ -253,5 +267,5 @@ Configuration is stored in `~/.relaybot/config.conf`.
|
|
|
253
267
|
|----------|-------------|
|
|
254
268
|
| `SLACK_BOT_TOKEN` | Slack Bot OAuth token (`xoxb-...`) |
|
|
255
269
|
| `SLACK_APP_TOKEN` | Slack App-level token for Socket Mode (`xapp-...`) |
|
|
256
|
-
| `SLACK_USER_ID` |
|
|
257
|
-
| `WORKING_DIR` | Directory where the AI will operate |
|
|
270
|
+
| `SLACK_USER_ID` | Optional. Restricts replies to a single user (`U0XXXXXXXX`) |
|
|
271
|
+
| `WORKING_DIR` | Optional. Directory where the AI will operate for better context |
|
package/cli.js
CHANGED
|
@@ -15,11 +15,13 @@ Commands:
|
|
|
15
15
|
|
|
16
16
|
Options:
|
|
17
17
|
--codex Use Codex instead of Claude (for start command)
|
|
18
|
+
--noyolo Skip --yolo / --dangerously-skip-permissions
|
|
18
19
|
|
|
19
20
|
Examples:
|
|
20
21
|
relaybot setup # Run interactive setup
|
|
21
22
|
relaybot start # Start with Claude (default)
|
|
22
23
|
relaybot start --codex # Start with Codex
|
|
24
|
+
relaybot start --noyolo # Start without auto-approve flags
|
|
23
25
|
`);
|
|
24
26
|
}
|
|
25
27
|
|
package/config.conf.example
CHANGED
package/main.js
CHANGED
|
@@ -7,7 +7,7 @@ const slackHandlers = require('./src/slack-handlers');
|
|
|
7
7
|
|
|
8
8
|
const config = loadConfig();
|
|
9
9
|
|
|
10
|
-
if (!config || !config.SLACK_BOT_TOKEN || !config.SLACK_APP_TOKEN
|
|
10
|
+
if (!config || !config.SLACK_BOT_TOKEN || !config.SLACK_APP_TOKEN) {
|
|
11
11
|
console.error('❌ Configuration not found or incomplete.\n');
|
|
12
12
|
console.log('Please run setup first:\n');
|
|
13
13
|
console.log(' relaybot setup\n');
|
package/package.json
CHANGED
package/setup.js
CHANGED
|
@@ -23,18 +23,24 @@ async function setup() {
|
|
|
23
23
|
|
|
24
24
|
const botToken = await question('SLACK_BOT_TOKEN (xoxb-...): ');
|
|
25
25
|
const appToken = await question('SLACK_APP_TOKEN (xapp-...): ');
|
|
26
|
-
const userId = await question('SLACK_USER_ID (U0XXXXXXXX): ');
|
|
26
|
+
const userId = await question('SLACK_USER_ID (optional, restricts replies) (U0XXXXXXXX): ');
|
|
27
27
|
|
|
28
28
|
console.log('\nWorking directory (folder where the AI will operate).\n');
|
|
29
|
-
const workingDir = await question('WORKING_DIR: ');
|
|
29
|
+
const workingDir = await question('WORKING_DIR (optional): ');
|
|
30
30
|
|
|
31
31
|
const configLines = [
|
|
32
32
|
`SLACK_BOT_TOKEN=${botToken.trim()}`,
|
|
33
|
-
`SLACK_APP_TOKEN=${appToken.trim()}
|
|
34
|
-
`SLACK_USER_ID=${userId.trim()}`,
|
|
35
|
-
`WORKING_DIR=${workingDir.trim()}`
|
|
33
|
+
`SLACK_APP_TOKEN=${appToken.trim()}`
|
|
36
34
|
];
|
|
37
35
|
|
|
36
|
+
if (userId.trim()) {
|
|
37
|
+
configLines.push(`SLACK_USER_ID=${userId.trim()}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (workingDir.trim()) {
|
|
41
|
+
configLines.push(`WORKING_DIR=${workingDir.trim()}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
// Create config directory if it doesn't exist
|
|
39
45
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
40
46
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
@@ -21,7 +21,7 @@ WORKING_DIR=/path/to/directory
|
|
|
21
21
|
|
|
22
22
|
### 1) Resolve project context
|
|
23
23
|
|
|
24
|
-
- Use the directory configured in `WORKING_DIR` as the working context.
|
|
24
|
+
- Use the directory configured in `WORKING_DIR` as the working context when set; otherwise use the current process working directory.
|
|
25
25
|
- Run all commands and file operations within this directory.
|
|
26
26
|
- Avoid asking the user for clarification; pick the best default and proceed automatically.
|
|
27
27
|
- If a question is truly required to continue, send it as a Slack DM using the script, then wait for the user's response via the Slack socket before proceeding.
|
|
@@ -44,6 +44,8 @@ node ./src/send-slack-message.js "Your message here"
|
|
|
44
44
|
- Keep messages concise and actionable
|
|
45
45
|
- Include relevant links (PR URLs, documentation, etc.)
|
|
46
46
|
- For long answers, summarize key points
|
|
47
|
+
- Format replies with clean, readable line breaks; do not use literal `\n` sequences
|
|
48
|
+
- Use emojis sparingly when they improve clarity or tone
|
|
47
49
|
|
|
48
50
|
## Notes
|
|
49
51
|
|
package/src/agent.js
CHANGED
|
@@ -2,13 +2,17 @@ const pty = require('@lydell/node-pty');
|
|
|
2
2
|
|
|
3
3
|
let claudeProcess = null;
|
|
4
4
|
const useCodex = process.argv.includes('--codex');
|
|
5
|
+
const noYolo = process.argv.includes('--noyolo');
|
|
5
6
|
const shell = useCodex ? 'codex' : 'claude';
|
|
6
7
|
|
|
7
8
|
function sendCommand(text) {
|
|
8
9
|
if (claudeProcess) {
|
|
9
10
|
setTimeout(() => {
|
|
10
|
-
claudeProcess.write(text + '\r
|
|
11
|
-
|
|
11
|
+
claudeProcess.write(text + '\r');
|
|
12
|
+
setTimeout(() => {
|
|
13
|
+
claudeProcess.write('\r');
|
|
14
|
+
claudeProcess.write('\x0D');
|
|
15
|
+
}, 500);
|
|
12
16
|
}, 500);
|
|
13
17
|
}
|
|
14
18
|
}
|
|
@@ -18,7 +22,9 @@ function isRunning() {
|
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
function start() {
|
|
21
|
-
|
|
25
|
+
const defaultArgs = useCodex ? ['--yolo'] : ['--dangerously-skip-permissions'];
|
|
26
|
+
const spawnArgs = noYolo ? [] : defaultArgs;
|
|
27
|
+
claudeProcess = pty.spawn(shell, spawnArgs, {
|
|
22
28
|
name: 'xterm-color',
|
|
23
29
|
cols: 80,
|
|
24
30
|
rows: 30,
|
|
@@ -28,6 +34,27 @@ function start() {
|
|
|
28
34
|
|
|
29
35
|
console.log(`--- Persistent ${useCodex ? 'Codex' : 'Claude'} Session Started ---`);
|
|
30
36
|
|
|
37
|
+
const hadRawMode = Boolean(process.stdin.isTTY && process.stdin.isRaw);
|
|
38
|
+
if (process.stdin.isTTY) {
|
|
39
|
+
process.stdin.setRawMode(true);
|
|
40
|
+
}
|
|
41
|
+
process.stdin.resume();
|
|
42
|
+
process.stdin.on('data', (data) => {
|
|
43
|
+
if (data && data.length === 1 && data[0] === 3) {
|
|
44
|
+
// Ctrl+C: restore terminal and exit.
|
|
45
|
+
if (process.stdin.isTTY) {
|
|
46
|
+
process.stdin.setRawMode(Boolean(hadRawMode));
|
|
47
|
+
}
|
|
48
|
+
if (claudeProcess) {
|
|
49
|
+
claudeProcess.kill('SIGINT');
|
|
50
|
+
}
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
if (claudeProcess) {
|
|
54
|
+
claudeProcess.write(data);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
31
58
|
claudeProcess.onData((data) => {
|
|
32
59
|
const dataStr = data.toString();
|
|
33
60
|
const byPassPrompts = ['Do you want to proceed?'];
|
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
const { WebClient } = require('@slack/web-api');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
2
4
|
const loadConfig = require('./load-config');
|
|
3
5
|
|
|
4
6
|
const config = loadConfig();
|
|
5
7
|
|
|
6
|
-
if (!config || !config.SLACK_BOT_TOKEN
|
|
8
|
+
if (!config || !config.SLACK_BOT_TOKEN) {
|
|
7
9
|
console.error('❌ Configuration not found. Run: node setup.js');
|
|
8
10
|
process.exit(1);
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
const web = new WebClient(config.SLACK_BOT_TOKEN);
|
|
12
|
-
const
|
|
14
|
+
const lastMessagePath = path.join(loadConfig.CONFIG_DIR, 'last_message.json');
|
|
15
|
+
|
|
16
|
+
function getLastChannel() {
|
|
17
|
+
try {
|
|
18
|
+
if (!fs.existsSync(lastMessagePath)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const payload = JSON.parse(fs.readFileSync(lastMessagePath, 'utf-8'));
|
|
22
|
+
return payload?.channel || null;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Failed to read last message:', error);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const channelId = getLastChannel() || config.SLACK_USER_ID;
|
|
13
29
|
|
|
14
30
|
// Get the message from command line arguments
|
|
15
|
-
|
|
31
|
+
// Convert literal \n to actual newlines
|
|
32
|
+
const message = process.argv[2]?.replace(/\\n/g, '\n');
|
|
16
33
|
|
|
17
34
|
if (!message) {
|
|
18
35
|
console.error('Please provide a message to send.');
|
|
@@ -21,11 +38,16 @@ if (!message) {
|
|
|
21
38
|
|
|
22
39
|
(async () => {
|
|
23
40
|
try {
|
|
41
|
+
if (!channelId) {
|
|
42
|
+
console.error('No recent channel found and SLACK_USER_ID is not set.');
|
|
43
|
+
console.error('Send a Slack message to the bot first or set SLACK_USER_ID in the config.');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
24
46
|
const result = await web.chat.postMessage({
|
|
25
|
-
channel:
|
|
47
|
+
channel: channelId,
|
|
26
48
|
text: message,
|
|
27
49
|
});
|
|
28
|
-
console.log(`Successfully sent message to ${
|
|
50
|
+
console.log(`Successfully sent message to ${channelId}`);
|
|
29
51
|
} catch (error) {
|
|
30
52
|
console.error(`Error sending message: ${error}`);
|
|
31
53
|
process.exit(1);
|
package/src/slack-handlers.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
1
2
|
const path = require('path');
|
|
2
3
|
const agent = require('./agent');
|
|
3
4
|
const loadConfig = require('./load-config');
|
|
@@ -5,6 +6,21 @@ const loadConfig = require('./load-config');
|
|
|
5
6
|
const config = loadConfig();
|
|
6
7
|
const isProduction = __dirname.includes('node_modules');
|
|
7
8
|
const skillPath = path.join(__dirname, '..', 'skills', 'relay-bot', 'SKILL.md');
|
|
9
|
+
const lastMessagePath = path.join(loadConfig.CONFIG_DIR, 'last_message.json');
|
|
10
|
+
|
|
11
|
+
function storeLastMessage(message) {
|
|
12
|
+
try {
|
|
13
|
+
const payload = {
|
|
14
|
+
channel: message.channel,
|
|
15
|
+
user: message.user,
|
|
16
|
+
ts: message.ts
|
|
17
|
+
};
|
|
18
|
+
fs.mkdirSync(loadConfig.CONFIG_DIR, { recursive: true });
|
|
19
|
+
fs.writeFileSync(lastMessagePath, JSON.stringify(payload));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error('Failed to store last message:', error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
8
24
|
|
|
9
25
|
function getPromptSuffix() {
|
|
10
26
|
if (isProduction) {
|
|
@@ -16,11 +32,11 @@ function getPromptSuffix() {
|
|
|
16
32
|
function registerHandlers(app) {
|
|
17
33
|
app.message(async ({ message, say }) => {
|
|
18
34
|
// Only respond to messages from the configured user
|
|
19
|
-
if (message.user !== config.SLACK_USER_ID) {
|
|
35
|
+
if (config.SLACK_USER_ID && message.user !== config.SLACK_USER_ID) {
|
|
20
36
|
return;
|
|
21
37
|
}
|
|
22
38
|
|
|
23
|
-
|
|
39
|
+
storeLastMessage(message);
|
|
24
40
|
|
|
25
41
|
if (agent.isRunning()) {
|
|
26
42
|
const fullPrompt = message.text + getPromptSuffix();
|