remote-opencode 1.0.2 β†’ 1.0.3

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 CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  > Control your AI coding assistant from anywhere β€” your phone, tablet, or another computer.
4
4
 
5
+ <div align="center">
6
+ <img width="1024" alt="Gemini_Generated_Image_47d5gq47d5gq47d5" src="https://github.com/user-attachments/assets/1defa11d-6195-4a9c-956b-4f87470f6393" />
7
+ </div>
8
+
5
9
  **remote-opencode** is a Discord bot that bridges your local [OpenCode CLI](https://github.com/sst/opencode) to Discord, enabling you to interact with your AI coding assistant remotely. Perfect for developers who want to:
6
10
 
7
11
  - πŸ“± **Code from mobile** β€” Send coding tasks from your phone while away from your desk
@@ -75,7 +79,7 @@ npx remote-opencode
75
79
  ### Install from source
76
80
 
77
81
  ```bash
78
- git clone https://github.com/<your-username>/remote-opencode.git
82
+ git clone https://github.com/RoundTable02/remote-opencode.git
79
83
  cd remote-opencode
80
84
  npm install
81
85
  npm run build
@@ -109,9 +113,7 @@ Before using remote-opencode, you need to create a Discord Application and Bot.
109
113
  3. Enter a name (e.g., "Remote OpenCode")
110
114
  4. Copy the **Application ID** β€” you'll need this later
111
115
 
112
- <!-- SCREENSHOT: Discord Developer Portal - New Application button -->
113
- <!-- ![Create Application](./docs/images/01-create-application.png) -->
114
- *Screenshot placeholder: Discord Developer Portal showing "New Application" button*
116
+ <img width="800" alt="image" src="https://github.com/user-attachments/assets/ca2e7ff3-91e7-4d66-93dc-c166189c0107" />
115
117
 
116
118
  ### Step 2: Create Bot & Get Token
117
119
 
@@ -120,9 +122,6 @@ Before using remote-opencode, you need to create a Discord Application and Bot.
120
122
  3. **Copy the token immediately** β€” it's only shown once!
121
123
  4. Keep this token secret β€” never share it publicly
122
124
 
123
- <!-- SCREENSHOT: Bot section showing Reset Token button -->
124
- <!-- ![Get Bot Token](./docs/images/02-bot-token.png) -->
125
- *Screenshot placeholder: Bot settings page with token section highlighted*
126
125
 
127
126
  ### Step 3: Enable Required Intents
128
127
 
@@ -133,9 +132,7 @@ Still in the **"Bot"** section, scroll down to **"Privileged Gateway Intents"**
133
132
 
134
133
  Click **"Save Changes"**
135
134
 
136
- <!-- SCREENSHOT: Privileged Gateway Intents toggles -->
137
- <!-- ![Enable Intents](./docs/images/03-intents.png) -->
138
- *Screenshot placeholder: Intents section with required toggles enabled*
135
+ <img width="1500" alt="image" src="https://github.com/user-attachments/assets/d20406ff-26ad-4204-9771-b157c340846a" />
139
136
 
140
137
  ### Step 4: Configure Bot Permissions
141
138
 
@@ -164,35 +161,15 @@ The bot needs specific permissions to function properly. You can configure permi
164
161
 
165
162
  5. Copy the generated URL at the bottom β€” this is your bot invite link!
166
163
 
167
- <!-- SCREENSHOT: OAuth2 URL Generator showing selected permissions -->
168
- <!-- ![OAuth2 Permissions](./docs/images/03b-oauth2-permissions.png) -->
169
- *Screenshot placeholder: OAuth2 URL Generator with bot permissions selected*
170
164
 
171
165
  #### Option B: Manual Permission Calculation
172
166
 
173
167
  If you're building the URL manually, use this permission value:
174
168
 
175
169
  ```
176
- https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=343932274624&scope=bot%20applications.commands
170
+ https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=311385214016&integration_type=0&scope=bot+applications.commands
177
171
  ```
178
172
 
179
- **Permission Breakdown (343932274624):**
180
-
181
- | Permission | Value | Purpose |
182
- |------------|-------|---------|
183
- | View Channels | 1024 | Access channels and read messages |
184
- | Send Messages | 2048 | Send messages to channels |
185
- | Create Public Threads | 68719476736 | Create threads for each `/opencode` command |
186
- | Send Messages in Threads | 274877906944 | Reply within created threads |
187
- | Embed Links | 16384 | Send rich embed messages with formatting |
188
- | Read Message History | 65536 | Access previous messages for context |
189
- | Add Reactions | 64 | Add button components (uses emoji reactions) |
190
- | Use Slash Commands | 2147483648 | Register and respond to slash commands |
191
-
192
- **Optional Permissions (not required):**
193
- - **Attach Files** (32768) β€” Only if you want to upload files
194
- - **Mention @everyone** (131072) β€” Only if bot needs to ping everyone
195
-
196
173
  **Important:** The URL must include `applications.commands` scope for slash commands to work!
197
174
 
198
175
  ### Step 5: Get Your Server (Guild) ID
@@ -202,25 +179,20 @@ https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=34
202
179
  3. Right-click on your server name in the sidebar
203
180
  4. Click **"Copy Server ID"**
204
181
 
205
- <!-- SCREENSHOT: Discord showing Copy Server ID option -->
206
- <!-- ![Copy Server ID](./docs/images/04-guild-id.png) -->
207
- *Screenshot placeholder: Right-click menu showing "Copy Server ID" option*
182
+ <img width="184" height="530" alt="스크란샷 2026-02-03 α„‹α…©α„Œα…₯ᆫ 2 34 31" src="https://github.com/user-attachments/assets/8ecc2a28-05e5-494f-834f-95d9d0e4e730" />
208
183
 
209
184
  ### Step 6: Invite Bot to Your Server
210
185
 
211
186
  Use the URL generated in Step 4 (OAuth2 URL Generator), or construct it manually:
212
187
 
213
188
  ```
214
- https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=343932274624&scope=bot%20applications.commands
189
+ https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=218900185540&scope=bot%20applications.commands
215
190
  ```
216
191
 
217
192
  1. Replace `YOUR_CLIENT_ID` with your Application ID
218
193
  2. Open the URL in your browser
219
194
  3. Select your server and authorize
220
195
 
221
- <!-- SCREENSHOT: Bot authorization page -->
222
- <!-- ![Authorize Bot](./docs/images/05-authorize.png) -->
223
- *Screenshot placeholder: Discord OAuth2 authorization page*
224
196
 
225
197
  ---
226
198
 
@@ -306,6 +278,43 @@ Start isolated work on a new branch with its own worktree.
306
278
 
307
279
  This is perfect for working on multiple features simultaneously without branch switching.
308
280
 
281
+ ### `/code` β€” Toggle Passthrough Mode
282
+
283
+ Enable passthrough mode in a thread to send messages directly to OpenCode without slash commands.
284
+
285
+ ```
286
+ /code
287
+ ```
288
+
289
+ **How it works:**
290
+ 1. Run `/code` in any thread to enable passthrough mode
291
+ 2. Type messages naturally β€” they're sent directly to OpenCode
292
+ 3. Run `/code` again to disable
293
+
294
+ **Example:**
295
+ ```
296
+ You: /code
297
+ Bot: βœ… Passthrough mode enabled for this thread.
298
+ Your messages will be sent directly to OpenCode.
299
+
300
+ You: Add a dark mode toggle to settings
301
+ Bot: πŸ“Œ Prompt: Add a dark mode toggle to settings
302
+ [streaming response...]
303
+
304
+ You: Now add a keyboard shortcut for it
305
+ Bot: πŸ“Œ Prompt: Now add a keyboard shortcut for it
306
+ [streaming response...]
307
+
308
+ You: /code
309
+ Bot: ❌ Passthrough mode disabled.
310
+ ```
311
+
312
+ **Features:**
313
+ - πŸ“± **Mobile-friendly** β€” no more typing slash commands on phone
314
+ - 🧡 **Thread-scoped** β€” only affects the specific thread, not the whole channel
315
+ - ⏳ **Busy indicator** β€” shows ⏳ reaction if previous task is still running
316
+ - πŸ”’ **Safe** β€” ignores bot messages (no infinite loops)
317
+
309
318
  ---
310
319
 
311
320
  ## Usage Workflow
@@ -342,6 +351,8 @@ Perfect for when you're away from your desk:
342
351
  4. Watch real-time progress
343
352
  5. Use the **Interrupt** button if needed
344
353
 
354
+ **Pro tip:** Enable passthrough mode with `/code` in a thread for an even smoother mobile experience β€” just type messages directly without slash commands!
355
+
345
356
  ### Team Collaboration Workflow
346
357
 
347
358
  Share AI coding sessions with your team:
@@ -477,7 +488,7 @@ The bot maintains persistent sessions. If you encounter issues:
477
488
  ### Run from source
478
489
 
479
490
  ```bash
480
- git clone https://github.com/<your-username>/remote-opencode.git
491
+ git clone https://github.com/RoundTable02/remote-opencode.git
481
492
  cd remote-opencode
482
493
  npm install
483
494
 
@@ -504,13 +515,15 @@ src/
504
515
  β”œβ”€β”€ bot.ts # Discord client initialization
505
516
  β”œβ”€β”€ commands/ # Slash command definitions
506
517
  β”‚ β”œβ”€β”€ opencode.ts # Main AI interaction command
518
+ β”‚ β”œβ”€β”€ code.ts # Passthrough mode toggle
507
519
  β”‚ β”œβ”€β”€ work.ts # Worktree management
508
520
  β”‚ β”œβ”€β”€ setpath.ts # Project registration
509
521
  β”‚ β”œβ”€β”€ projects.ts # List projects
510
522
  β”‚ └── use.ts # Channel binding
511
523
  β”œβ”€β”€ handlers/ # Interaction handlers
512
524
  β”‚ β”œβ”€β”€ interactionHandler.ts
513
- β”‚ └── buttonHandler.ts
525
+ β”‚ β”œβ”€β”€ buttonHandler.ts
526
+ β”‚ └── messageHandler.ts # Passthrough message handling
514
527
  β”œβ”€β”€ services/ # Core business logic
515
528
  β”‚ β”œβ”€β”€ serveManager.ts # OpenCode process management
516
529
  β”‚ β”œβ”€β”€ sessionManager.ts # Session state management
package/dist/src/bot.js CHANGED
@@ -2,6 +2,7 @@ import { Client, GatewayIntentBits, Events } from 'discord.js';
2
2
  import pc from 'picocolors';
3
3
  import { getBotConfig } from './services/configStore.js';
4
4
  import { handleInteraction } from './handlers/interactionHandler.js';
5
+ import { handleMessageCreate } from './handlers/messageHandler.js';
5
6
  import * as serveManager from './services/serveManager.js';
6
7
  export async function startBot() {
7
8
  const config = getBotConfig();
@@ -9,12 +10,17 @@ export async function startBot() {
9
10
  throw new Error('Bot configuration not found. Run setup first.');
10
11
  }
11
12
  const client = new Client({
12
- intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages]
13
+ intents: [
14
+ GatewayIntentBits.Guilds,
15
+ GatewayIntentBits.GuildMessages,
16
+ GatewayIntentBits.MessageContent
17
+ ]
13
18
  });
14
19
  client.once(Events.ClientReady, (c) => {
15
20
  console.log(pc.green(`Ready! Logged in as ${pc.bold(c.user.tag)}`));
16
21
  });
17
22
  client.on(Events.InteractionCreate, handleInteraction);
23
+ client.on(Events.MessageCreate, handleMessageCreate);
18
24
  function gracefulShutdown(signal) {
19
25
  console.log(pc.yellow(`\n${signal} received. Shutting down gracefully...`));
20
26
  serveManager.stopAll();
@@ -0,0 +1,47 @@
1
+ import { SlashCommandBuilder, MessageFlags } from 'discord.js';
2
+ import * as dataStore from '../services/dataStore.js';
3
+ export const code = {
4
+ data: new SlashCommandBuilder()
5
+ .setName('code')
6
+ .setDescription('Toggle passthrough mode - send messages directly to OpenCode'),
7
+ async execute(interaction) {
8
+ const channel = interaction.channel;
9
+ if (!channel?.isThread()) {
10
+ await interaction.reply({
11
+ content: '❌ This command only works inside threads.',
12
+ flags: MessageFlags.Ephemeral
13
+ });
14
+ return;
15
+ }
16
+ const threadId = channel.id;
17
+ const parentChannelId = channel.parentId;
18
+ if (!parentChannelId) {
19
+ await interaction.reply({
20
+ content: '❌ Cannot determine parent channel.',
21
+ flags: MessageFlags.Ephemeral
22
+ });
23
+ return;
24
+ }
25
+ const projectPath = dataStore.getChannelProjectPath(parentChannelId);
26
+ if (!projectPath) {
27
+ await interaction.reply({
28
+ content: '❌ No project set for this channel. Use `/use <alias>` in the parent channel first.',
29
+ flags: MessageFlags.Ephemeral
30
+ });
31
+ return;
32
+ }
33
+ const currentState = dataStore.isPassthroughEnabled(threadId);
34
+ const newState = !currentState;
35
+ dataStore.setPassthroughMode(threadId, newState, interaction.user.id);
36
+ if (newState) {
37
+ await interaction.reply({
38
+ content: 'βœ… **Passthrough mode enabled** for this thread.\nYour messages will be sent directly to OpenCode.',
39
+ });
40
+ }
41
+ else {
42
+ await interaction.reply({
43
+ content: '❌ **Passthrough mode disabled.**\nUse `/opencode prompt:"..."` to send commands.',
44
+ });
45
+ }
46
+ }
47
+ };
@@ -4,9 +4,11 @@ import { projects } from './projects.js';
4
4
  import { use } from './use.js';
5
5
  import { opencode } from './opencode.js';
6
6
  import { work } from './work.js';
7
+ import { code } from './code.js';
7
8
  export const commands = new Collection();
8
9
  commands.set(setpath.data.name, setpath);
9
10
  commands.set(projects.data.name, projects);
10
11
  commands.set(use.data.name, use);
11
12
  commands.set(opencode.data.name, opencode);
12
13
  commands.set(work.data.name, work);
14
+ commands.set(code.data.name, code);
@@ -0,0 +1,156 @@
1
+ import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
2
+ import * as dataStore from '../services/dataStore.js';
3
+ import * as sessionManager from '../services/sessionManager.js';
4
+ import * as serveManager from '../services/serveManager.js';
5
+ import { SSEClient } from '../services/sseClient.js';
6
+ import { formatOutput } from '../utils/messageFormatter.js';
7
+ export async function handleMessageCreate(message) {
8
+ if (message.author.bot)
9
+ return;
10
+ if (message.system)
11
+ return;
12
+ const channel = message.channel;
13
+ if (!channel.isThread())
14
+ return;
15
+ const threadId = channel.id;
16
+ if (!dataStore.isPassthroughEnabled(threadId))
17
+ return;
18
+ const parentChannelId = channel.parentId;
19
+ if (!parentChannelId)
20
+ return;
21
+ const projectPath = dataStore.getChannelProjectPath(parentChannelId);
22
+ if (!projectPath) {
23
+ await message.reply('❌ No project bound to parent channel. Disable passthrough and use `/use` first.');
24
+ return;
25
+ }
26
+ const worktreeMapping = dataStore.getWorktreeMapping(threadId);
27
+ const effectivePath = worktreeMapping?.worktreePath ?? projectPath;
28
+ const existingClient = sessionManager.getSseClient(threadId);
29
+ if (existingClient && existingClient.isConnected()) {
30
+ await message.react('⏳');
31
+ return;
32
+ }
33
+ const prompt = message.content.trim();
34
+ if (!prompt)
35
+ return;
36
+ const buttons = new ActionRowBuilder()
37
+ .addComponents(new ButtonBuilder()
38
+ .setCustomId(`interrupt_${threadId}`)
39
+ .setLabel('⏸️ Interrupt')
40
+ .setStyle(ButtonStyle.Secondary));
41
+ let streamMessage;
42
+ try {
43
+ streamMessage = await channel.send({
44
+ content: `πŸ“Œ **Prompt**: ${prompt}\n\nπŸš€ Starting OpenCode server...`,
45
+ components: [buttons]
46
+ });
47
+ }
48
+ catch {
49
+ return;
50
+ }
51
+ let port;
52
+ let sessionId;
53
+ let updateInterval = null;
54
+ let accumulatedText = '';
55
+ let lastContent = '';
56
+ let tick = 0;
57
+ const spinner = ['β ‹', 'β ™', 'β Ή', 'β Έ', 'β Ό', 'β ΄', 'β ¦', 'β §', 'β ‡', '⠏'];
58
+ const updateStreamMessage = async (content, components) => {
59
+ try {
60
+ await streamMessage.edit({ content, components });
61
+ }
62
+ catch {
63
+ }
64
+ };
65
+ try {
66
+ port = await serveManager.spawnServe(effectivePath);
67
+ await updateStreamMessage(`πŸ“Œ **Prompt**: ${prompt}\n\n⏳ Waiting for OpenCode server...`, [buttons]);
68
+ await serveManager.waitForReady(port);
69
+ const existingSession = sessionManager.getSessionForThread(threadId);
70
+ if (existingSession && existingSession.projectPath === effectivePath) {
71
+ const isValid = await sessionManager.validateSession(port, existingSession.sessionId);
72
+ if (isValid) {
73
+ sessionId = existingSession.sessionId;
74
+ sessionManager.updateSessionLastUsed(threadId);
75
+ }
76
+ else {
77
+ sessionId = await sessionManager.createSession(port);
78
+ sessionManager.setSessionForThread(threadId, sessionId, effectivePath, port);
79
+ }
80
+ }
81
+ else {
82
+ sessionId = await sessionManager.createSession(port);
83
+ sessionManager.setSessionForThread(threadId, sessionId, effectivePath, port);
84
+ }
85
+ const sseClient = new SSEClient();
86
+ sseClient.connect(`http://localhost:${port}`);
87
+ sessionManager.setSseClient(threadId, sseClient);
88
+ sseClient.onPartUpdated((part) => {
89
+ accumulatedText = part.text;
90
+ });
91
+ sseClient.onSessionIdle(() => {
92
+ if (updateInterval) {
93
+ clearInterval(updateInterval);
94
+ updateInterval = null;
95
+ }
96
+ (async () => {
97
+ try {
98
+ const formatted = formatOutput(accumulatedText);
99
+ const disabledButtons = new ActionRowBuilder()
100
+ .addComponents(new ButtonBuilder()
101
+ .setCustomId(`interrupt_${threadId}`)
102
+ .setLabel('⏸️ Interrupt')
103
+ .setStyle(ButtonStyle.Secondary)
104
+ .setDisabled(true));
105
+ await updateStreamMessage(`πŸ“Œ **Prompt**: ${prompt}\n\n\`\`\`\n${formatted}\n\`\`\``, [disabledButtons]);
106
+ await channel.send({ content: 'βœ… Done' });
107
+ sseClient.disconnect();
108
+ sessionManager.clearSseClient(threadId);
109
+ }
110
+ catch {
111
+ }
112
+ })();
113
+ });
114
+ sseClient.onError((error) => {
115
+ if (updateInterval) {
116
+ clearInterval(updateInterval);
117
+ updateInterval = null;
118
+ }
119
+ (async () => {
120
+ try {
121
+ await updateStreamMessage(`πŸ“Œ **Prompt**: ${prompt}\n\n❌ Connection error: ${error.message}`, []);
122
+ }
123
+ catch {
124
+ }
125
+ })();
126
+ });
127
+ updateInterval = setInterval(async () => {
128
+ tick++;
129
+ try {
130
+ const formatted = formatOutput(accumulatedText);
131
+ const spinnerChar = spinner[tick % spinner.length];
132
+ const newContent = formatted || 'Processing...';
133
+ if (newContent !== lastContent || tick % 2 === 0) {
134
+ lastContent = newContent;
135
+ await updateStreamMessage(`πŸ“Œ **Prompt**: ${prompt}\n\n${spinnerChar} **Running...**\n\`\`\`\n${newContent}\n\`\`\``, [buttons]);
136
+ }
137
+ }
138
+ catch {
139
+ }
140
+ }, 1000);
141
+ await updateStreamMessage(`πŸ“Œ **Prompt**: ${prompt}\n\nπŸ“ Sending prompt...`, [buttons]);
142
+ await sessionManager.sendPrompt(port, sessionId, prompt);
143
+ }
144
+ catch (error) {
145
+ if (updateInterval) {
146
+ clearInterval(updateInterval);
147
+ }
148
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
149
+ await updateStreamMessage(`πŸ“Œ **Prompt**: ${prompt}\n\n❌ OpenCode execution failed: ${errorMessage}`, []);
150
+ const client = sessionManager.getSseClient(threadId);
151
+ if (client) {
152
+ client.disconnect();
153
+ sessionManager.clearSseClient(threadId);
154
+ }
155
+ }
156
+ }
@@ -150,3 +150,46 @@ export function getWorktreeMappingsByProject(projectPath) {
150
150
  const data = loadData();
151
151
  return data.worktreeMappings?.filter(m => m.projectPath === projectPath) ?? [];
152
152
  }
153
+ export function setPassthroughMode(threadId, enabled, userId) {
154
+ const data = loadData();
155
+ if (!data.passthroughThreads) {
156
+ data.passthroughThreads = [];
157
+ }
158
+ const existing = data.passthroughThreads.findIndex(p => p.threadId === threadId);
159
+ if (existing >= 0) {
160
+ data.passthroughThreads[existing] = {
161
+ threadId,
162
+ enabled,
163
+ enabledBy: userId,
164
+ enabledAt: Date.now()
165
+ };
166
+ }
167
+ else {
168
+ data.passthroughThreads.push({
169
+ threadId,
170
+ enabled,
171
+ enabledBy: userId,
172
+ enabledAt: Date.now()
173
+ });
174
+ }
175
+ saveData(data);
176
+ }
177
+ export function getPassthroughMode(threadId) {
178
+ const data = loadData();
179
+ return data.passthroughThreads?.find(p => p.threadId === threadId);
180
+ }
181
+ export function isPassthroughEnabled(threadId) {
182
+ const passthrough = getPassthroughMode(threadId);
183
+ return passthrough?.enabled ?? false;
184
+ }
185
+ export function removePassthroughMode(threadId) {
186
+ const data = loadData();
187
+ if (!data.passthroughThreads)
188
+ return false;
189
+ const idx = data.passthroughThreads.findIndex(p => p.threadId === threadId);
190
+ if (idx < 0)
191
+ return false;
192
+ data.passthroughThreads.splice(idx, 1);
193
+ saveData(data);
194
+ return true;
195
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-opencode",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Discord bot for remote OpenCode CLI access",
5
5
  "main": "dist/src/index.js",
6
6
  "bin": {