relaybot 1.0.5 → 1.0.7
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/README.md +30 -6
- package/guide.md +18 -0
- package/main.js +8 -0
- package/manifest.json +3 -1
- package/package.json +1 -1
- package/src/agent.js +105 -19
- package/src/send-slack-message.js +76 -20
- package/src/slack-handlers.js +142 -6
- package/.claude/settings.local.json +0 -15
- package/relaybot-1.0.0.tgz +0 -0
- package/skills/relay-bot/SKILL.md +0 -53
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# RelayBot
|
|
2
2
|
|
|
3
|
-
An AI assistant that lives in your Slack DMs — powered by Claude or Codex.
|
|
3
|
+
An AI assistant that lives in your Slack channels and DMs — powered by Claude or Codex.
|
|
4
4
|
|
|
5
5
|
<img src="demo/demo.jpg" width="30%">
|
|
6
6
|
|
|
@@ -8,11 +8,11 @@ An AI assistant that lives in your Slack DMs — powered by Claude or Codex.
|
|
|
8
8
|
|
|
9
9
|
## What is RelayBot?
|
|
10
10
|
|
|
11
|
-
RelayBot acts as a bridge between Slack and AI coding agents, allowing you to interact with Claude or Codex
|
|
11
|
+
RelayBot acts as a bridge between Slack and AI coding agents, allowing you to interact with Claude or Codex by mentioning the bot in channels or sending a DM. Instead of switching between tools, you can request code changes, ask questions, and manage development tasks without leaving Slack.
|
|
12
12
|
|
|
13
13
|
### Key Features
|
|
14
14
|
|
|
15
|
-
- **Conversational AI Access** — Chat with Claude or Codex
|
|
15
|
+
- **Conversational AI Access** — Chat with Claude or Codex from Slack channels via mentions or via DM
|
|
16
16
|
- **Code Execution** — AI can read, write, and modify code in your projects
|
|
17
17
|
- **Task Automation** — Request file changes, refactoring, bug fixes, or new features
|
|
18
18
|
- **Context-Aware Responses** — Maintains project directory context across conversations
|
|
@@ -37,7 +37,7 @@ RelayBot acts as a bridge between Slack and AI coding agents, allowing you to in
|
|
|
37
37
|
│ SLACK │
|
|
38
38
|
│ ┌──────────┐ ┌──────────────┐ │
|
|
39
39
|
│ │ User │ ───── sends message ─────────────► │ Channel/ │ │
|
|
40
|
-
│ │ │ ◄──── receives reply ───────────── │
|
|
40
|
+
│ │ │ ◄──── receives reply ───────────── │ Channel │ │
|
|
41
41
|
│ └──────────┘ └──────────────┘ │
|
|
42
42
|
└─────────────────────────────────────────────────────────────────────┘
|
|
43
43
|
│ ▲
|
|
@@ -84,12 +84,34 @@ RelayBot acts as a bridge between Slack and AI coding agents, allowing you to in
|
|
|
84
84
|
|
|
85
85
|
## How It Works
|
|
86
86
|
|
|
87
|
-
1. **Slack Connection** — RelayBot connects to Slack via WebSocket (Socket Mode) and listens for DMs
|
|
87
|
+
1. **Slack Connection** — RelayBot connects to Slack via WebSocket (Socket Mode) and listens for mentions and DMs
|
|
88
88
|
2. **Message Reception** — When you send a message, Slack forwards it to RelayBot
|
|
89
89
|
3. **AI Bridge** — RelayBot spawns a persistent Claude or Codex CLI session and forwards your message
|
|
90
90
|
4. **AI Processing** — The AI processes your request with full access to your codebase
|
|
91
91
|
5. **Response Summarization** — Long outputs are summarized into concise, actionable messages
|
|
92
|
-
6. **Slack Reply** — The summarized response is sent back to
|
|
92
|
+
6. **Slack Reply** — The summarized response is sent back to the same channel or DM
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Slack Commands
|
|
97
|
+
|
|
98
|
+
Send these as a DM to the bot (or in a channel mention) to control the agent session:
|
|
99
|
+
|
|
100
|
+
- `$status` — Show whether the agent is running, plus PID, start time, uptime, cwd, WORKING_DIR, and last exit
|
|
101
|
+
- `$stop` — Stop the agent session (equivalent to Ctrl-C)
|
|
102
|
+
- `$start [--codex] [--noyolo]` — Start the agent with optional flags
|
|
103
|
+
- `$restart [--codex] [--noyolo]` — Restart the agent with optional flags
|
|
104
|
+
- `$dir /path/to/working/dir` — Update the configured working directory (must exist).
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
$status
|
|
110
|
+
$stop
|
|
111
|
+
$start --codex
|
|
112
|
+
$restart --noyolo
|
|
113
|
+
$dir /Users/you/work/project
|
|
114
|
+
```
|
|
93
115
|
|
|
94
116
|
---
|
|
95
117
|
|
|
@@ -215,6 +237,7 @@ Socket Mode allows the bot to receive events via WebSocket instead of HTTP endpo
|
|
|
215
237
|
2. Scroll to **Scopes** → **Bot Token Scopes**
|
|
216
238
|
3. Add these scopes:
|
|
217
239
|
- `chat:write` — Send messages
|
|
240
|
+
- `app_mentions:read` — Read mentions of your app
|
|
218
241
|
- `im:history` — Read DM history
|
|
219
242
|
- `im:read` — View DM metadata
|
|
220
243
|
- `im:write` — Start DMs with users
|
|
@@ -226,6 +249,7 @@ Socket Mode allows the bot to receive events via WebSocket instead of HTTP endpo
|
|
|
226
249
|
2. Toggle **Enable Events** to ON
|
|
227
250
|
3. Expand **Subscribe to bot events**
|
|
228
251
|
4. Add these events:
|
|
252
|
+
- `app_mention` — Receive mentions in channels
|
|
229
253
|
- `message.im` — Receive DM messages
|
|
230
254
|
|
|
231
255
|
#### 5. Install the App
|
package/guide.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Send a Slack DM after tasks or answers using `send-slack-message.js`, replying naturally as a bot (avoid meta summaries).
|
|
2
|
+
Choose the best default and proceed automatically; do not ask for clarification.
|
|
3
|
+
|
|
4
|
+
## Output rules
|
|
5
|
+
|
|
6
|
+
- Keep messages concise and actionable
|
|
7
|
+
- Include relevant links (PR URLs, documentation, etc.)
|
|
8
|
+
- For long answers, distill key points in a natural voice (no meta summary framing)
|
|
9
|
+
- Use clean line breaks; do not include literal `\n` sequences
|
|
10
|
+
- Use emojis sparingly when they improve clarity or tone
|
|
11
|
+
|
|
12
|
+
## Quick commands
|
|
13
|
+
|
|
14
|
+
Send a Slack DM:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
node ./src/send-slack-message.js "Your message here"
|
|
18
|
+
```
|
package/main.js
CHANGED
|
@@ -20,6 +20,14 @@ const app = new App({
|
|
|
20
20
|
socketMode: true
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
if (config.WORKING_DIR) {
|
|
24
|
+
try {
|
|
25
|
+
process.chdir(config.WORKING_DIR);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(`Failed to change WORKING_DIR to ${config.WORKING_DIR}:`, error.message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
slackHandlers.registerHandlers(app);
|
|
24
32
|
|
|
25
33
|
(async () => {
|
package/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"display_information": {
|
|
3
3
|
"name": "RelayBot",
|
|
4
|
-
"description": "An AI assistant that lives in your Slack DMs — powered by Claude or Codex",
|
|
4
|
+
"description": "An AI assistant that lives in your Slack channels and DMs — powered by Claude or Codex",
|
|
5
5
|
"background_color": "#4A154B"
|
|
6
6
|
},
|
|
7
7
|
"features": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"scopes": {
|
|
15
15
|
"bot": [
|
|
16
16
|
"chat:write",
|
|
17
|
+
"app_mentions:read",
|
|
17
18
|
"im:history",
|
|
18
19
|
"im:read",
|
|
19
20
|
"im:write",
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
"settings": {
|
|
25
26
|
"event_subscriptions": {
|
|
26
27
|
"bot_events": [
|
|
28
|
+
"app_mention",
|
|
27
29
|
"message.im"
|
|
28
30
|
]
|
|
29
31
|
},
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
const pty = require('@lydell/node-pty');
|
|
2
2
|
|
|
3
3
|
let claudeProcess = null;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
let startTime = null;
|
|
5
|
+
let lastExit = null;
|
|
6
|
+
let stdinHandler = null;
|
|
7
|
+
let stdinAttached = false;
|
|
8
|
+
let rawModeBefore = null;
|
|
9
|
+
let useCodex = process.argv.includes('--codex');
|
|
10
|
+
let noYolo = process.argv.includes('--noyolo');
|
|
11
|
+
let shell = useCodex ? 'codex' : 'claude';
|
|
7
12
|
|
|
8
13
|
function sendCommand(text) {
|
|
9
14
|
if (claudeProcess) {
|
|
@@ -21,29 +26,20 @@ function isRunning() {
|
|
|
21
26
|
return claudeProcess !== null;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
const spawnArgs = noYolo ? [] : defaultArgs;
|
|
27
|
-
claudeProcess = pty.spawn(shell, spawnArgs, {
|
|
28
|
-
name: 'xterm-color',
|
|
29
|
-
cols: 80,
|
|
30
|
-
rows: 30,
|
|
31
|
-
cwd: process.cwd(),
|
|
32
|
-
env: { ...process.env, TERM: process.env.TERM || 'xterm-256color' }
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
console.log(`--- Persistent ${useCodex ? 'Codex' : 'Claude'} Session Started ---`);
|
|
29
|
+
function attachStdin() {
|
|
30
|
+
if (stdinAttached) return;
|
|
36
31
|
|
|
37
|
-
|
|
32
|
+
rawModeBefore = Boolean(process.stdin.isTTY && process.stdin.isRaw);
|
|
38
33
|
if (process.stdin.isTTY) {
|
|
39
34
|
process.stdin.setRawMode(true);
|
|
40
35
|
}
|
|
41
36
|
process.stdin.resume();
|
|
42
|
-
|
|
37
|
+
|
|
38
|
+
stdinHandler = (data) => {
|
|
43
39
|
if (data && data.length === 1 && data[0] === 3) {
|
|
44
40
|
// Ctrl+C: restore terminal and exit.
|
|
45
41
|
if (process.stdin.isTTY) {
|
|
46
|
-
process.stdin.setRawMode(Boolean(
|
|
42
|
+
process.stdin.setRawMode(Boolean(rawModeBefore));
|
|
47
43
|
}
|
|
48
44
|
if (claudeProcess) {
|
|
49
45
|
claudeProcess.kill('SIGINT');
|
|
@@ -53,8 +49,50 @@ function start() {
|
|
|
53
49
|
if (claudeProcess) {
|
|
54
50
|
claudeProcess.write(data);
|
|
55
51
|
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
process.stdin.on('data', stdinHandler);
|
|
55
|
+
stdinAttached = true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function detachStdin() {
|
|
59
|
+
if (!stdinAttached) return;
|
|
60
|
+
process.stdin.off('data', stdinHandler);
|
|
61
|
+
stdinHandler = null;
|
|
62
|
+
stdinAttached = false;
|
|
63
|
+
if (process.stdin.isTTY) {
|
|
64
|
+
process.stdin.setRawMode(Boolean(rawModeBefore));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function start(options = {}) {
|
|
69
|
+
if (claudeProcess) {
|
|
70
|
+
return claudeProcess;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof options.useCodex === 'boolean') {
|
|
74
|
+
useCodex = options.useCodex;
|
|
75
|
+
}
|
|
76
|
+
if (typeof options.noYolo === 'boolean') {
|
|
77
|
+
noYolo = options.noYolo;
|
|
78
|
+
}
|
|
79
|
+
shell = useCodex ? 'codex' : 'claude';
|
|
80
|
+
|
|
81
|
+
const defaultArgs = useCodex ? ['--yolo'] : ['--dangerously-skip-permissions'];
|
|
82
|
+
const spawnArgs = noYolo ? [] : defaultArgs;
|
|
83
|
+
claudeProcess = pty.spawn(shell, spawnArgs, {
|
|
84
|
+
name: 'xterm-color',
|
|
85
|
+
cols: 80,
|
|
86
|
+
rows: 30,
|
|
87
|
+
cwd: process.cwd(),
|
|
88
|
+
env: { ...process.env, TERM: process.env.TERM || 'xterm-256color' }
|
|
56
89
|
});
|
|
57
90
|
|
|
91
|
+
console.log(`--- Persistent ${useCodex ? 'Codex' : 'Claude'} Session Started ---`);
|
|
92
|
+
startTime = new Date();
|
|
93
|
+
|
|
94
|
+
attachStdin();
|
|
95
|
+
|
|
58
96
|
claudeProcess.onData((data) => {
|
|
59
97
|
const dataStr = data.toString();
|
|
60
98
|
const byPassPrompts = ['Do you want to proceed?'];
|
|
@@ -76,11 +114,59 @@ function start() {
|
|
|
76
114
|
process.stdout.write(data);
|
|
77
115
|
});
|
|
78
116
|
|
|
117
|
+
claudeProcess.onExit(({ exitCode, signal }) => {
|
|
118
|
+
lastExit = {
|
|
119
|
+
exitCode,
|
|
120
|
+
signal,
|
|
121
|
+
at: new Date()
|
|
122
|
+
};
|
|
123
|
+
claudeProcess = null;
|
|
124
|
+
startTime = null;
|
|
125
|
+
detachStdin();
|
|
126
|
+
});
|
|
127
|
+
|
|
79
128
|
return claudeProcess;
|
|
80
129
|
}
|
|
81
130
|
|
|
131
|
+
function stop() {
|
|
132
|
+
if (!claudeProcess) {
|
|
133
|
+
return { stopped: false, reason: 'not_running' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
claudeProcess.write('\x03');
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
if (claudeProcess) {
|
|
140
|
+
claudeProcess.kill('SIGINT');
|
|
141
|
+
}
|
|
142
|
+
}, 250);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return { stopped: false, reason: 'error', error };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { stopped: true };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getStatus() {
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
const uptimeSeconds = startTime ? Math.floor((now - startTime.getTime()) / 1000) : null;
|
|
153
|
+
return {
|
|
154
|
+
running: Boolean(claudeProcess),
|
|
155
|
+
shell,
|
|
156
|
+
pid: claudeProcess ? claudeProcess.pid : null,
|
|
157
|
+
startedAt: startTime ? startTime.toISOString() : null,
|
|
158
|
+
uptimeSeconds,
|
|
159
|
+
noYolo,
|
|
160
|
+
useCodex,
|
|
161
|
+
cwd: process.cwd(),
|
|
162
|
+
lastExit
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
82
166
|
module.exports = {
|
|
83
167
|
sendCommand,
|
|
84
168
|
isRunning,
|
|
85
|
-
start
|
|
169
|
+
start,
|
|
170
|
+
stop,
|
|
171
|
+
getStatus
|
|
86
172
|
};
|
|
@@ -27,29 +27,85 @@ function getLastChannel() {
|
|
|
27
27
|
}
|
|
28
28
|
const channelId = getLastChannel() || config.SLACK_USER_ID;
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
30
|
+
function parseArgs() {
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
const useStdin = args.includes('--stdin');
|
|
33
|
+
const filtered = args.filter((arg) => arg !== '--stdin');
|
|
34
|
+
const argMessage = filtered.join(' ');
|
|
35
|
+
return { useStdin, argMessage };
|
|
36
|
+
}
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
process.
|
|
38
|
+
function readStdin() {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
if (process.stdin.isTTY) {
|
|
41
|
+
resolve('');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let data = '';
|
|
45
|
+
process.stdin.setEncoding('utf-8');
|
|
46
|
+
process.stdin.on('data', (chunk) => {
|
|
47
|
+
data += chunk;
|
|
48
|
+
});
|
|
49
|
+
process.stdin.on('end', () => resolve(data));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function chunkMessage(text, maxLen) {
|
|
54
|
+
const normalized = text.replace(/\r\n/g, '\n').trim();
|
|
55
|
+
if (!normalized) return [];
|
|
56
|
+
if (normalized.length <= maxLen) return [normalized];
|
|
57
|
+
|
|
58
|
+
const chunks = [];
|
|
59
|
+
let remaining = normalized;
|
|
60
|
+
while (remaining.length > maxLen) {
|
|
61
|
+
const slice = remaining.slice(0, maxLen);
|
|
62
|
+
let idx = slice.lastIndexOf('\n\n');
|
|
63
|
+
if (idx < maxLen * 0.5) idx = slice.lastIndexOf('\n');
|
|
64
|
+
if (idx < maxLen * 0.5) idx = slice.lastIndexOf(' ');
|
|
65
|
+
if (idx < 1) idx = maxLen;
|
|
66
|
+
const piece = remaining.slice(0, idx).trimEnd();
|
|
67
|
+
if (piece) chunks.push(piece);
|
|
68
|
+
remaining = remaining.slice(idx).trimStart();
|
|
69
|
+
}
|
|
70
|
+
if (remaining) chunks.push(remaining);
|
|
71
|
+
return chunks;
|
|
37
72
|
}
|
|
38
73
|
|
|
39
74
|
(async () => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
const result = await web.chat.postMessage({
|
|
47
|
-
channel: channelId,
|
|
48
|
-
text: message,
|
|
49
|
-
});
|
|
50
|
-
console.log(`Successfully sent message to ${channelId}`);
|
|
51
|
-
} catch (error) {
|
|
52
|
-
console.error(`Error sending message: ${error}`);
|
|
53
|
-
process.exit(1);
|
|
75
|
+
try {
|
|
76
|
+
if (!channelId) {
|
|
77
|
+
console.error('No recent channel found and SLACK_USER_ID is not set.');
|
|
78
|
+
console.error('Send a Slack message to the bot first or set SLACK_USER_ID in the config.');
|
|
79
|
+
process.exit(1);
|
|
54
80
|
}
|
|
81
|
+
|
|
82
|
+
const { useStdin, argMessage } = parseArgs();
|
|
83
|
+
let message = argMessage.replace(/\\n/g, '\n');
|
|
84
|
+
if (useStdin || !message) {
|
|
85
|
+
const stdinMessage = await readStdin();
|
|
86
|
+
if (stdinMessage) {
|
|
87
|
+
message = stdinMessage;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!message || !message.trim()) {
|
|
92
|
+
console.error('Please provide a message to send.');
|
|
93
|
+
console.error('Usage: node ./src/send-slack-message.js "Your message here"');
|
|
94
|
+
console.error('Or: cat message.txt | node ./src/send-slack-message.js --stdin');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const chunks = chunkMessage(message, 39000);
|
|
99
|
+
for (const chunk of chunks) {
|
|
100
|
+
await web.chat.postMessage({
|
|
101
|
+
channel: channelId,
|
|
102
|
+
text: chunk,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
console.log(`Successfully sent ${chunks.length} message(s) to ${channelId}`);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const apiError = error?.data?.error ? ` (${error.data.error})` : '';
|
|
108
|
+
console.error(`Error sending message: ${error}${apiError}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
55
111
|
})();
|
package/src/slack-handlers.js
CHANGED
|
@@ -5,7 +5,7 @@ const loadConfig = require('./load-config');
|
|
|
5
5
|
|
|
6
6
|
const config = loadConfig();
|
|
7
7
|
const isProduction = __dirname.includes('node_modules');
|
|
8
|
-
const skillPath = path.join(__dirname, '..', '
|
|
8
|
+
const skillPath = path.join(__dirname, '..', 'guide.md');
|
|
9
9
|
const lastMessagePath = path.join(loadConfig.CONFIG_DIR, 'last_message.json');
|
|
10
10
|
|
|
11
11
|
function storeLastMessage(message) {
|
|
@@ -23,14 +23,57 @@ function storeLastMessage(message) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function getPromptSuffix() {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
return `\nIMPORTANT: Read and follow the instructions in ${skillPath}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function stripMentions(text) {
|
|
30
|
+
if (!text) return text;
|
|
31
|
+
return text.replace(/<@[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function updateConfigValue(key, value) {
|
|
35
|
+
fs.mkdirSync(loadConfig.CONFIG_DIR, { recursive: true });
|
|
36
|
+
let lines = [];
|
|
37
|
+
if (fs.existsSync(loadConfig.CONFIG_PATH)) {
|
|
38
|
+
const content = fs.readFileSync(loadConfig.CONFIG_PATH, 'utf-8');
|
|
39
|
+
lines = content.split('\n');
|
|
28
40
|
}
|
|
29
|
-
|
|
41
|
+
|
|
42
|
+
let updated = false;
|
|
43
|
+
lines = lines.map((line) => {
|
|
44
|
+
if (line.trim().startsWith('#') || !line.includes('=')) {
|
|
45
|
+
return line;
|
|
46
|
+
}
|
|
47
|
+
const [k] = line.split('=');
|
|
48
|
+
if (k.trim() === key) {
|
|
49
|
+
updated = true;
|
|
50
|
+
return `${key}=${value}`;
|
|
51
|
+
}
|
|
52
|
+
return line;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!updated) {
|
|
56
|
+
lines.push(`${key}=${value}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fs.writeFileSync(loadConfig.CONFIG_PATH, lines.join('\n'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseCommandOptions(text) {
|
|
63
|
+
const parts = (text || '').trim().split(/\s+/);
|
|
64
|
+
const options = {
|
|
65
|
+
useCodex: parts.includes('--codex'),
|
|
66
|
+
noYolo: parts.includes('--noyolo')
|
|
67
|
+
};
|
|
68
|
+
return options;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function delay(ms) {
|
|
72
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
30
73
|
}
|
|
31
74
|
|
|
32
75
|
function registerHandlers(app) {
|
|
33
|
-
|
|
76
|
+
async function handleMessage({ message, say, text }) {
|
|
34
77
|
// Only respond to messages from the configured user
|
|
35
78
|
if (config.SLACK_USER_ID && message.user !== config.SLACK_USER_ID) {
|
|
36
79
|
return;
|
|
@@ -38,12 +81,105 @@ function registerHandlers(app) {
|
|
|
38
81
|
|
|
39
82
|
storeLastMessage(message);
|
|
40
83
|
|
|
84
|
+
const trimmedText = (text || '').trim();
|
|
85
|
+
if (trimmedText === '$stop') {
|
|
86
|
+
const result = agent.stop();
|
|
87
|
+
if (result.stopped) {
|
|
88
|
+
await say('Stopped the agent session (SIGINT).');
|
|
89
|
+
} else if (result.reason === 'not_running') {
|
|
90
|
+
await say('Agent is not running.');
|
|
91
|
+
} else {
|
|
92
|
+
await say('Failed to stop the agent session.');
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (trimmedText.startsWith('$start')) {
|
|
98
|
+
const options = parseCommandOptions(trimmedText);
|
|
99
|
+
if (agent.isRunning()) {
|
|
100
|
+
await say('Agent is already running.');
|
|
101
|
+
} else {
|
|
102
|
+
agent.start(options);
|
|
103
|
+
await say(`Started the agent session${options.useCodex ? ' (Codex)' : ' (Claude)'}${options.noYolo ? ' without auto-approve flags' : ''}.`);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (trimmedText.startsWith('$restart')) {
|
|
109
|
+
const options = parseCommandOptions(trimmedText);
|
|
110
|
+
if (agent.isRunning()) {
|
|
111
|
+
agent.stop();
|
|
112
|
+
await delay(400);
|
|
113
|
+
}
|
|
114
|
+
agent.start(options);
|
|
115
|
+
await say(`Restarted the agent session${options.useCodex ? ' (Codex)' : ' (Claude)'}${options.noYolo ? ' without auto-approve flags' : ''}.`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (trimmedText === '$status') {
|
|
120
|
+
const status = agent.getStatus();
|
|
121
|
+
const lines = [
|
|
122
|
+
`Running: ${status.running}`,
|
|
123
|
+
`Shell: ${status.shell}`,
|
|
124
|
+
`PID: ${status.pid || 'n/a'}`,
|
|
125
|
+
`Started: ${status.startedAt || 'n/a'}`,
|
|
126
|
+
`Uptime (s): ${status.uptimeSeconds || 'n/a'}`,
|
|
127
|
+
`CWD: ${status.cwd}`,
|
|
128
|
+
`Configured WORKING_DIR: ${config.WORKING_DIR || 'n/a'}`,
|
|
129
|
+
`Use Codex: ${status.useCodex}`,
|
|
130
|
+
`No YOLO: ${status.noYolo}`
|
|
131
|
+
];
|
|
132
|
+
if (status.lastExit) {
|
|
133
|
+
lines.push(`Last exit: code=${status.lastExit.exitCode ?? 'n/a'} signal=${status.lastExit.signal || 'n/a'} at=${status.lastExit.at.toISOString()}`);
|
|
134
|
+
}
|
|
135
|
+
await say(lines.join('\n'));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (trimmedText.startsWith('$dir ')) {
|
|
140
|
+
const newDir = trimmedText.replace(/^\$dir\s+/, '').trim();
|
|
141
|
+
if (!newDir) {
|
|
142
|
+
await say('Usage: $dir /path/to/working/dir');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!fs.existsSync(newDir)) {
|
|
146
|
+
await say(`Directory does not exist: ${newDir}`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const stat = fs.statSync(newDir);
|
|
150
|
+
if (!stat.isDirectory()) {
|
|
151
|
+
await say(`Not a directory: ${newDir}`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
updateConfigValue('WORKING_DIR', newDir);
|
|
155
|
+
config.WORKING_DIR = newDir;
|
|
156
|
+
await say(`Working directory set to: ${newDir}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
41
160
|
if (agent.isRunning()) {
|
|
42
|
-
const fullPrompt =
|
|
161
|
+
const fullPrompt = trimmedText + getPromptSuffix();
|
|
43
162
|
agent.sendCommand(fullPrompt);
|
|
44
163
|
} else {
|
|
45
164
|
await say('Agent process is not running.');
|
|
46
165
|
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
app.event('app_mention', async ({ event, say }) => {
|
|
169
|
+
if (event.channel_type === 'im' || event.channel_type === 'mpim') {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const cleanedText = stripMentions(event.text);
|
|
174
|
+
await handleMessage({ message: event, say, text: cleanedText });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
app.message(async ({ message, say }) => {
|
|
178
|
+
if (message.channel_type !== 'im') {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await handleMessage({ message, say, text: message.text });
|
|
47
183
|
});
|
|
48
184
|
}
|
|
49
185
|
|
package/relaybot-1.0.0.tgz
DELETED
|
Binary file
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: relay-bot
|
|
3
|
-
description: "Workspace-aware assistant that maintains project directory context and sends a Slack DM reply after completing a task or answering a question."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# RelayBot
|
|
7
|
-
|
|
8
|
-
## Overview
|
|
9
|
-
|
|
10
|
-
Maintain working-directory context for projects and send a Slack DM after tasks or answers using `send-slack-message.js`.
|
|
11
|
-
|
|
12
|
-
## Working directory
|
|
13
|
-
|
|
14
|
-
The working directory where the AI operates is configured in `~/.relaybot/config.conf`:
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
WORKING_DIR=/path/to/directory
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Workflow
|
|
21
|
-
|
|
22
|
-
### 1) Resolve project context
|
|
23
|
-
|
|
24
|
-
- Use the directory configured in `WORKING_DIR` as the working context when set; otherwise use the current process working directory.
|
|
25
|
-
- Run all commands and file operations within this directory.
|
|
26
|
-
- Avoid asking the user for clarification; pick the best default and proceed automatically.
|
|
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.
|
|
28
|
-
|
|
29
|
-
### 2) Execute the task in the active project
|
|
30
|
-
|
|
31
|
-
- Run commands and edit files in the resolved working directory.
|
|
32
|
-
- Keep the project context for follow-up requests unless the user switches again.
|
|
33
|
-
|
|
34
|
-
### 3) Send a Slack DM reply after completion
|
|
35
|
-
|
|
36
|
-
Run from this repo root:
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
node ./src/send-slack-message.js "Your message here"
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Output rules
|
|
43
|
-
|
|
44
|
-
- Keep messages concise and actionable
|
|
45
|
-
- Include relevant links (PR URLs, documentation, etc.)
|
|
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
|
|
49
|
-
|
|
50
|
-
## Notes
|
|
51
|
-
|
|
52
|
-
- The script posts to the configured DM user in `~/.relaybot/config.conf`
|
|
53
|
-
- The Slack bot token must be configured in `~/.relaybot/config.conf`
|