remote-opencode 1.0.1 β†’ 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RoundTable02
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 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
@@ -38,6 +42,12 @@ The bot runs on your development machine alongside OpenCode. When you send a com
38
42
  - [Installation](#installation)
39
43
  - [Quick Start](#quick-start)
40
44
  - [Discord Bot Setup](#discord-bot-setup)
45
+ - [Step 1: Create Discord Application](#step-1-create-discord-application)
46
+ - [Step 2: Create Bot & Get Token](#step-2-create-bot--get-token)
47
+ - [Step 3: Enable Required Intents](#step-3-enable-required-intents)
48
+ - [Step 4: Configure Bot Permissions](#step-4-configure-bot-permissions)
49
+ - [Step 5: Get Your Server (Guild) ID](#step-5-get-your-server-guild-id)
50
+ - [Step 6: Invite Bot to Your Server](#step-6-invite-bot-to-your-server)
41
51
  - [CLI Commands](#cli-commands)
42
52
  - [Discord Slash Commands](#discord-slash-commands)
43
53
  - [Usage Workflow](#usage-workflow)
@@ -69,7 +79,7 @@ npx remote-opencode
69
79
  ### Install from source
70
80
 
71
81
  ```bash
72
- git clone https://github.com/<your-username>/remote-opencode.git
82
+ git clone https://github.com/RoundTable02/remote-opencode.git
73
83
  cd remote-opencode
74
84
  npm install
75
85
  npm run build
@@ -103,9 +113,7 @@ Before using remote-opencode, you need to create a Discord Application and Bot.
103
113
  3. Enter a name (e.g., "Remote OpenCode")
104
114
  4. Copy the **Application ID** β€” you'll need this later
105
115
 
106
- <!-- SCREENSHOT: Discord Developer Portal - New Application button -->
107
- <!-- ![Create Application](./docs/images/01-create-application.png) -->
108
- *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" />
109
117
 
110
118
  ### Step 2: Create Bot & Get Token
111
119
 
@@ -114,9 +122,6 @@ Before using remote-opencode, you need to create a Discord Application and Bot.
114
122
  3. **Copy the token immediately** β€” it's only shown once!
115
123
  4. Keep this token secret β€” never share it publicly
116
124
 
117
- <!-- SCREENSHOT: Bot section showing Reset Token button -->
118
- <!-- ![Get Bot Token](./docs/images/02-bot-token.png) -->
119
- *Screenshot placeholder: Bot settings page with token section highlighted*
120
125
 
121
126
  ### Step 3: Enable Required Intents
122
127
 
@@ -127,36 +132,67 @@ Still in the **"Bot"** section, scroll down to **"Privileged Gateway Intents"**
127
132
 
128
133
  Click **"Save Changes"**
129
134
 
130
- <!-- SCREENSHOT: Privileged Gateway Intents toggles -->
131
- <!-- ![Enable Intents](./docs/images/03-intents.png) -->
132
- *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" />
136
+
137
+ ### Step 4: Configure Bot Permissions
138
+
139
+ The bot needs specific permissions to function properly. You can configure permissions in two ways:
140
+
141
+ #### Option A: Using OAuth2 URL Generator (Recommended)
142
+
143
+ 1. Navigate to the **"OAuth2"** section in the sidebar
144
+ 2. Click on **"URL Generator"**
145
+ 3. In **"Scopes"**, select:
146
+ - βœ… `bot`
147
+ - βœ… `applications.commands`
148
+ 4. In **"Bot Permissions"**, select only these essential permissions:
149
+
150
+ **General Permissions:**
151
+ - βœ… **View Channels** β€” Required to access channels
152
+
153
+ **Text Permissions:**
154
+ - βœ… **Send Messages** β€” Send responses to channels
155
+ - βœ… **Create Public Threads** β€” Create threads for each `/opencode` session
156
+ - βœ… **Send Messages in Threads** β€” Reply within threads
157
+ - βœ… **Embed Links** β€” Send formatted embed messages
158
+ - βœ… **Read Message History** β€” Access context for conversations
159
+ - βœ… **Add Reactions** β€” Add buttons (uses emoji reactions internally)
160
+ - βœ… **Use Slash Commands** β€” Register and use slash commands
161
+
162
+ 5. Copy the generated URL at the bottom β€” this is your bot invite link!
163
+
133
164
 
134
- ### Step 4: Get Your Server (Guild) ID
165
+ #### Option B: Manual Permission Calculation
166
+
167
+ If you're building the URL manually, use this permission value:
168
+
169
+ ```
170
+ https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=311385214016&integration_type=0&scope=bot+applications.commands
171
+ ```
172
+
173
+ **Important:** The URL must include `applications.commands` scope for slash commands to work!
174
+
175
+ ### Step 5: Get Your Server (Guild) ID
135
176
 
136
177
  1. Open Discord and go to **User Settings β†’ Advanced**
137
178
  2. Enable **"Developer Mode"**
138
179
  3. Right-click on your server name in the sidebar
139
180
  4. Click **"Copy Server ID"**
140
181
 
141
- <!-- SCREENSHOT: Discord showing Copy Server ID option -->
142
- <!-- ![Copy Server ID](./docs/images/04-guild-id.png) -->
143
- *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" />
144
183
 
145
- ### Step 5: Invite Bot to Your Server
184
+ ### Step 6: Invite Bot to Your Server
146
185
 
147
- The setup wizard generates an invite URL, or use this format:
186
+ Use the URL generated in Step 4 (OAuth2 URL Generator), or construct it manually:
148
187
 
149
188
  ```
150
- https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=2147534848&scope=bot
189
+ https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=218900185540&scope=bot%20applications.commands
151
190
  ```
152
191
 
153
192
  1. Replace `YOUR_CLIENT_ID` with your Application ID
154
193
  2. Open the URL in your browser
155
194
  3. Select your server and authorize
156
195
 
157
- <!-- SCREENSHOT: Bot authorization page -->
158
- <!-- ![Authorize Bot](./docs/images/05-authorize.png) -->
159
- *Screenshot placeholder: Discord OAuth2 authorization page*
160
196
 
161
197
  ---
162
198
 
@@ -242,6 +278,43 @@ Start isolated work on a new branch with its own worktree.
242
278
 
243
279
  This is perfect for working on multiple features simultaneously without branch switching.
244
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
+
245
318
  ---
246
319
 
247
320
  ## Usage Workflow
@@ -278,6 +351,8 @@ Perfect for when you're away from your desk:
278
351
  4. Watch real-time progress
279
352
  5. Use the **Interrupt** button if needed
280
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
+
281
356
  ### Team Collaboration Workflow
282
357
 
283
358
  Share AI coding sessions with your team:
@@ -413,7 +488,7 @@ The bot maintains persistent sessions. If you encounter issues:
413
488
  ### Run from source
414
489
 
415
490
  ```bash
416
- git clone https://github.com/<your-username>/remote-opencode.git
491
+ git clone https://github.com/RoundTable02/remote-opencode.git
417
492
  cd remote-opencode
418
493
  npm install
419
494
 
@@ -440,13 +515,15 @@ src/
440
515
  β”œβ”€β”€ bot.ts # Discord client initialization
441
516
  β”œβ”€β”€ commands/ # Slash command definitions
442
517
  β”‚ β”œβ”€β”€ opencode.ts # Main AI interaction command
518
+ β”‚ β”œβ”€β”€ code.ts # Passthrough mode toggle
443
519
  β”‚ β”œβ”€β”€ work.ts # Worktree management
444
520
  β”‚ β”œβ”€β”€ setpath.ts # Project registration
445
521
  β”‚ β”œβ”€β”€ projects.ts # List projects
446
522
  β”‚ └── use.ts # Channel binding
447
523
  β”œβ”€β”€ handlers/ # Interaction handlers
448
524
  β”‚ β”œβ”€β”€ interactionHandler.ts
449
- β”‚ └── buttonHandler.ts
525
+ β”‚ β”œβ”€β”€ buttonHandler.ts
526
+ β”‚ └── messageHandler.ts # Passthrough message handling
450
527
  β”œβ”€β”€ services/ # Core business logic
451
528
  β”‚ β”œβ”€β”€ serveManager.ts # OpenCode process management
452
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.1",
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": {