relaybot 1.0.3 → 1.0.4

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.
@@ -0,0 +1,15 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(*)",
5
+ "Read(*)",
6
+ "Edit(*)",
7
+ "Write(*)"
8
+ ],
9
+ "deny": [
10
+ "Bash(rm *)",
11
+ "Bash(rm -rf *)",
12
+ "Bash(rmdir *)"
13
+ ]
14
+ }
15
+ }
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` | Your Slack User ID (`U0XXXXXXXX`) |
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
 
@@ -1,4 +1,6 @@
1
1
  SLACK_BOT_TOKEN=xoxb-your-bot-token
2
2
  SLACK_APP_TOKEN=xapp-your-app-token
3
+ # Optional: restricts replies to a single user
3
4
  SLACK_USER_ID=U0XXXXXXXX
5
+ # Optional: sets working directory for better context
4
6
  WORKING_DIR=/path/to/directory
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 || !config.SLACK_USER_ID) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relaybot",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "An AI assistant that lives in your Slack DMs — powered by Claude or Codex",
5
5
  "main": "main.js",
6
6
  "bin": {
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\n');
11
- claudeProcess.write('\x0D');
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
- claudeProcess = pty.spawn(shell, useCodex ? ['--yolo'] : ['--dangerously-skip-permissions'], {
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 || !config.SLACK_USER_ID) {
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 userId = config.SLACK_USER_ID;
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
- const message = process.argv[2];
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: userId,
47
+ channel: channelId,
26
48
  text: message,
27
49
  });
28
- console.log(`Successfully sent message to ${userId}`);
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);
@@ -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,10 +6,25 @@ 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) {
11
- return `\nIMPORTANT: Read and follow the instructions in ${skillPath}`;
27
+ return `\nIMPORTANT: Use the relay-bot skill if it exists. Otherwise, read and follow the instructions in ${skillPath}`;
12
28
  }
13
29
  return '\nIMPORTANT: Use the relay-bot skill.';
14
30
  }
@@ -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
- console.log('New message:', message.text);
39
+ storeLastMessage(message);
24
40
 
25
41
  if (agent.isRunning()) {
26
42
  const fullPrompt = message.text + getPromptSuffix();