remote-opencode 1.0.3 → 1.0.6

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
@@ -42,12 +42,6 @@ The bot runs on your development machine alongside OpenCode. When you send a com
42
42
  - [Installation](#installation)
43
43
  - [Quick Start](#quick-start)
44
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)
51
45
  - [CLI Commands](#cli-commands)
52
46
  - [Discord Slash Commands](#discord-slash-commands)
53
47
  - [Usage Workflow](#usage-workflow)
@@ -104,95 +98,30 @@ That's it! Now use Discord slash commands to interact with OpenCode.
104
98
 
105
99
  ## Discord Bot Setup
106
100
 
107
- Before using remote-opencode, you need to create a Discord Application and Bot. The setup wizard will guide you, but here's a visual walkthrough:
101
+ The setup wizard (`remote-opencode setup`) guides you through the entire process interactively:
108
102
 
109
- ### Step 1: Create Discord Application
103
+ 1. **Opens Discord Developer Portal** in your browser
104
+ 2. **Walks you through** creating an application, enabling intents, and getting your bot token
105
+ 3. **Generates the invite link** automatically and opens it in your browser
106
+ 4. **Deploys slash commands** to your server
110
107
 
111
- 1. Go to [Discord Developer Portal](https://discord.com/developers/applications)
112
- 2. Click **"New Application"**
113
- 3. Enter a name (e.g., "Remote OpenCode")
114
- 4. Copy the **Application ID** — you'll need this later
108
+ Just run `remote-opencode setup` and follow the prompts — no manual URL copying needed!
115
109
 
116
- <img width="800" alt="image" src="https://github.com/user-attachments/assets/ca2e7ff3-91e7-4d66-93dc-c166189c0107" />
110
+ <details>
111
+ <summary>📖 Manual setup reference (click to expand)</summary>
117
112
 
118
- ### Step 2: Create Bot & Get Token
113
+ If you prefer manual setup or need to troubleshoot:
119
114
 
120
- 1. Navigate to the **"Bot"** section in the sidebar
121
- 2. Click **"Reset Token"** (or "View Token" if available)
122
- 3. **Copy the token immediately** it's only shown once!
123
- 4. Keep this token secret never share it publicly
124
-
125
-
126
- ### Step 3: Enable Required Intents
127
-
128
- Still in the **"Bot"** section, scroll down to **"Privileged Gateway Intents"** and enable:
129
-
130
- - ✅ **SERVER MEMBERS INTENT**
131
- - ✅ **MESSAGE CONTENT INTENT**
132
-
133
- Click **"Save Changes"**
134
-
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
-
164
-
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
176
-
177
- 1. Open Discord and go to **User Settings → Advanced**
178
- 2. Enable **"Developer Mode"**
179
- 3. Right-click on your server name in the sidebar
180
- 4. Click **"Copy Server ID"**
181
-
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" />
183
-
184
- ### Step 6: Invite Bot to Your Server
185
-
186
- Use the URL generated in Step 4 (OAuth2 URL Generator), or construct it manually:
187
-
188
- ```
189
- https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=218900185540&scope=bot%20applications.commands
190
- ```
191
-
192
- 1. Replace `YOUR_CLIENT_ID` with your Application ID
193
- 2. Open the URL in your browser
194
- 3. Select your server and authorize
115
+ 1. **Create Application**: Go to [Discord Developer Portal](https://discord.com/developers/applications), create a new application
116
+ 2. **Enable Intents**: In "Bot" section, enable SERVER MEMBERS INTENT and MESSAGE CONTENT INTENT
117
+ 3. **Get Bot Token**: In "Bot" section, reset/view token and copy it
118
+ 4. **Get Guild ID**: Enable Developer Mode in Discord settings, right-click your server → Copy Server ID
119
+ 5. **Invite Bot**: Use this URL format:
120
+ ```
121
+ https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=2147534848&scope=bot+applications.commands
122
+ ```
195
123
 
124
+ </details>
196
125
 
197
126
  ---
198
127
 
@@ -315,6 +244,40 @@ Bot: ❌ Passthrough mode disabled.
315
244
  - ⏳ **Busy indicator** — shows ⏳ reaction if previous task is still running
316
245
  - 🔒 **Safe** — ignores bot messages (no infinite loops)
317
246
 
247
+ ### `/autowork` — Toggle Automatic Worktree Creation
248
+
249
+ Enable automatic worktree creation for a project. When enabled, new `/opencode` sessions will automatically create isolated git worktrees.
250
+
251
+ ```
252
+ /autowork
253
+ ```
254
+
255
+ **How it works:**
256
+ 1. Run `/autowork` in a channel bound to a project
257
+ 2. The setting toggles on/off for that project
258
+ 3. When enabled, new sessions automatically create worktrees with branch names like `auto/abc12345-1738600000000`
259
+
260
+ **Example:**
261
+ ```
262
+ You: /autowork
263
+ Bot: ✅ Auto-worktree enabled for project myapp.
264
+ New sessions will automatically create isolated worktrees.
265
+
266
+ You: /opencode prompt:Add user authentication
267
+ Bot: [Creates thread + auto-worktree]
268
+ 🌳 Auto-Worktree: auto/abc12345-1738600000000
269
+ [Delete] [Create PR]
270
+ 📌 Prompt: Add user authentication
271
+ [streaming response...]
272
+ ```
273
+
274
+ **Features:**
275
+ - 🌳 **Automatic isolation** — each session gets its own branch and worktree
276
+ - 📱 **Mobile-friendly** — no need to type `/work` with branch names
277
+ - 🗑️ **Delete button** — removes worktree when done
278
+ - 🚀 **Create PR button** — easily create pull requests from worktree
279
+ - ⚡ **Per-project setting** — enable/disable independently for each project
280
+
318
281
  ---
319
282
 
320
283
  ## Usage Workflow
@@ -407,17 +370,21 @@ All configuration is stored in `~/.remote-opencode/`:
407
370
 
408
371
  ```json
409
372
  {
410
- "projects": {
411
- "myapp": "/Users/you/projects/my-app"
412
- },
413
- "bindings": {
414
- "channel-id": "myapp"
415
- },
416
- "threadSessions": { ... },
417
- "worktreeMappings": { ... }
373
+ "projects": [
374
+ { "alias": "myapp", "path": "/Users/you/projects/my-app", "autoWorktree": true }
375
+ ],
376
+ "bindings": [
377
+ { "channelId": "channel-id", "projectAlias": "myapp" }
378
+ ],
379
+ "threadSessions": [ ... ],
380
+ "worktreeMappings": [ ... ]
418
381
  }
419
382
  ```
420
383
 
384
+ | Field | Description |
385
+ |-------|-------------|
386
+ | `projects[].autoWorktree` | Optional. When `true`, new sessions auto-create worktrees |
387
+
421
388
  ---
422
389
 
423
390
  ## Troubleshooting
package/dist/src/cli.js CHANGED
@@ -1,15 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import pc from 'picocolors';
4
+ import { createRequire } from 'module';
5
+ import updateNotifier from 'update-notifier';
4
6
  import { runSetupWizard } from './setup/wizard.js';
5
7
  import { deployCommands } from './setup/deploy.js';
6
8
  import { startBot } from './bot.js';
7
9
  import { hasBotConfig, getConfigDir } from './services/configStore.js';
10
+ const require = createRequire(import.meta.url);
11
+ const pkg = require('../package.json');
12
+ updateNotifier({ pkg }).notify({ isGlobal: true });
8
13
  const program = new Command();
9
14
  program
10
15
  .name('remote-opencode')
11
16
  .description('Discord bot for remote OpenCode CLI access')
12
- .version('1.0.0');
17
+ .version(pkg.version);
13
18
  program
14
19
  .command('start')
15
20
  .description('Start the Discord bot')
@@ -19,6 +24,12 @@ program
19
24
  console.log(`Run ${pc.cyan('remote-opencode setup')} first to configure your Discord bot.\n`);
20
25
  process.exit(1);
21
26
  }
27
+ try {
28
+ await deployCommands();
29
+ }
30
+ catch {
31
+ console.log(pc.dim('Command deployment skipped (will retry on next start)'));
32
+ }
22
33
  await startBot();
23
34
  });
24
35
  program
@@ -0,0 +1,41 @@
1
+ import { SlashCommandBuilder, MessageFlags } from 'discord.js';
2
+ import * as dataStore from '../services/dataStore.js';
3
+ function getParentChannelId(interaction) {
4
+ const channel = interaction.channel;
5
+ if (channel?.isThread()) {
6
+ return channel.parentId ?? interaction.channelId;
7
+ }
8
+ return interaction.channelId;
9
+ }
10
+ export const autowork = {
11
+ data: new SlashCommandBuilder()
12
+ .setName('autowork')
13
+ .setDescription('Toggle automatic worktree creation for this channel\'s project'),
14
+ async execute(interaction) {
15
+ const channelId = getParentChannelId(interaction);
16
+ const projectAlias = dataStore.getChannelBinding(channelId);
17
+ if (!projectAlias) {
18
+ await interaction.reply({
19
+ content: '❌ No project set for this channel. Use `/use <alias>` to bind a project first.',
20
+ flags: MessageFlags.Ephemeral
21
+ });
22
+ return;
23
+ }
24
+ const currentState = dataStore.getProjectAutoWorktree(projectAlias);
25
+ const newState = !currentState;
26
+ const success = dataStore.setProjectAutoWorktree(projectAlias, newState);
27
+ if (!success) {
28
+ await interaction.reply({
29
+ content: `❌ Project "${projectAlias}" not found.`,
30
+ flags: MessageFlags.Ephemeral
31
+ });
32
+ return;
33
+ }
34
+ const emoji = newState ? '✅' : '❌';
35
+ const status = newState ? 'enabled' : 'disabled';
36
+ await interaction.reply({
37
+ content: `${emoji} Auto-worktree **${status}** for project **${projectAlias}**.\n\nNew sessions will ${newState ? 'automatically create' : 'NOT create'} isolated worktrees.`,
38
+ flags: MessageFlags.Ephemeral
39
+ });
40
+ }
41
+ };
@@ -5,6 +5,7 @@ import { use } from './use.js';
5
5
  import { opencode } from './opencode.js';
6
6
  import { work } from './work.js';
7
7
  import { code } from './code.js';
8
+ import { autowork } from './autowork.js';
8
9
  export const commands = new Collection();
9
10
  commands.set(setpath.data.name, setpath);
10
11
  commands.set(projects.data.name, projects);
@@ -12,3 +13,4 @@ commands.set(use.data.name, use);
12
13
  commands.set(opencode.data.name, opencode);
13
14
  commands.set(work.data.name, work);
14
15
  commands.set(code.data.name, code);
16
+ commands.set(autowork.data.name, autowork);
@@ -1,7 +1,8 @@
1
- import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from 'discord.js';
1
+ import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags, EmbedBuilder } from 'discord.js';
2
2
  import * as dataStore from '../services/dataStore.js';
3
3
  import * as sessionManager from '../services/sessionManager.js';
4
4
  import * as serveManager from '../services/serveManager.js';
5
+ import * as worktreeManager from '../services/worktreeManager.js';
5
6
  import { SSEClient } from '../services/sseClient.js';
6
7
  import { getOrCreateThread } from '../utils/threadHelper.js';
7
8
  import { formatOutput } from '../utils/messageFormatter.js';
@@ -48,7 +49,45 @@ export const opencode = {
48
49
  return;
49
50
  }
50
51
  const threadId = thread.id;
51
- const worktreeMapping = dataStore.getWorktreeMapping(threadId);
52
+ // Auto-create worktree if enabled and this is a new thread (not isInThread)
53
+ let worktreeMapping = dataStore.getWorktreeMapping(threadId);
54
+ if (!worktreeMapping && !isInThread) {
55
+ const projectAlias = dataStore.getChannelBinding(channelId);
56
+ if (projectAlias && dataStore.getProjectAutoWorktree(projectAlias)) {
57
+ try {
58
+ const branchName = worktreeManager.sanitizeBranchName(`auto/${threadId.slice(0, 8)}-${Date.now()}`);
59
+ const worktreePath = await worktreeManager.createWorktree(projectPath, branchName);
60
+ const newMapping = {
61
+ threadId,
62
+ branchName,
63
+ worktreePath,
64
+ projectPath,
65
+ description: prompt.slice(0, 50) + (prompt.length > 50 ? '...' : ''),
66
+ createdAt: Date.now()
67
+ };
68
+ dataStore.setWorktreeMapping(newMapping);
69
+ worktreeMapping = newMapping;
70
+ const embed = new EmbedBuilder()
71
+ .setTitle(`🌳 Auto-Worktree: ${branchName}`)
72
+ .setDescription('Automatically created for this session')
73
+ .addFields({ name: 'Branch', value: branchName, inline: true }, { name: 'Path', value: worktreePath, inline: true })
74
+ .setColor(0x2ecc71);
75
+ const worktreeButtons = new ActionRowBuilder()
76
+ .addComponents(new ButtonBuilder()
77
+ .setCustomId(`delete_${threadId}`)
78
+ .setLabel('Delete')
79
+ .setStyle(ButtonStyle.Danger), new ButtonBuilder()
80
+ .setCustomId(`pr_${threadId}`)
81
+ .setLabel('Create PR')
82
+ .setStyle(ButtonStyle.Primary));
83
+ await thread.send({ embeds: [embed], components: [worktreeButtons] });
84
+ }
85
+ catch (error) {
86
+ console.error('Auto-worktree creation failed:', error);
87
+ // Continue with main project path (graceful degradation)
88
+ }
89
+ }
90
+ }
52
91
  const effectivePath = worktreeMapping?.worktreePath ?? projectPath;
53
92
  const existingClient = sessionManager.getSseClient(threadId);
54
93
  if (existingClient && existingClient.isConnected()) {
@@ -1,7 +1,8 @@
1
- import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
1
+ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
2
2
  import * as dataStore from '../services/dataStore.js';
3
3
  import * as sessionManager from '../services/sessionManager.js';
4
4
  import * as serveManager from '../services/serveManager.js';
5
+ import * as worktreeManager from '../services/worktreeManager.js';
5
6
  import { SSEClient } from '../services/sseClient.js';
6
7
  import { formatOutput } from '../utils/messageFormatter.js';
7
8
  export async function handleMessageCreate(message) {
@@ -23,7 +24,46 @@ export async function handleMessageCreate(message) {
23
24
  await message.reply('❌ No project bound to parent channel. Disable passthrough and use `/use` first.');
24
25
  return;
25
26
  }
26
- const worktreeMapping = dataStore.getWorktreeMapping(threadId);
27
+ let worktreeMapping = dataStore.getWorktreeMapping(threadId);
28
+ // Auto-create worktree if enabled and no mapping exists for this thread
29
+ if (!worktreeMapping) {
30
+ const projectAlias = dataStore.getChannelBinding(parentChannelId);
31
+ if (projectAlias && dataStore.getProjectAutoWorktree(projectAlias)) {
32
+ try {
33
+ const branchName = worktreeManager.sanitizeBranchName(`auto/${threadId.slice(0, 8)}-${Date.now()}`);
34
+ const worktreePath = await worktreeManager.createWorktree(projectPath, branchName);
35
+ const prompt = message.content.trim();
36
+ const newMapping = {
37
+ threadId,
38
+ branchName,
39
+ worktreePath,
40
+ projectPath,
41
+ description: prompt.slice(0, 50) + (prompt.length > 50 ? '...' : ''),
42
+ createdAt: Date.now()
43
+ };
44
+ dataStore.setWorktreeMapping(newMapping);
45
+ worktreeMapping = newMapping;
46
+ const embed = new EmbedBuilder()
47
+ .setTitle(`🌳 Auto-Worktree: ${branchName}`)
48
+ .setDescription('Automatically created for this session')
49
+ .addFields({ name: 'Branch', value: branchName, inline: true }, { name: 'Path', value: worktreePath, inline: true })
50
+ .setColor(0x2ecc71);
51
+ const worktreeButtons = new ActionRowBuilder()
52
+ .addComponents(new ButtonBuilder()
53
+ .setCustomId(`delete_${threadId}`)
54
+ .setLabel('Delete')
55
+ .setStyle(ButtonStyle.Danger), new ButtonBuilder()
56
+ .setCustomId(`pr_${threadId}`)
57
+ .setLabel('Create PR')
58
+ .setStyle(ButtonStyle.Primary));
59
+ await channel.send({ embeds: [embed], components: [worktreeButtons] });
60
+ }
61
+ catch (error) {
62
+ console.error('Auto-worktree creation failed:', error);
63
+ // Continue with main project path (graceful degradation)
64
+ }
65
+ }
66
+ }
27
67
  const effectivePath = worktreeMapping?.worktreePath ?? projectPath;
28
68
  const existingClient = sessionManager.getSseClient(threadId);
29
69
  if (existingClient && existingClient.isConnected()) {
@@ -193,3 +193,16 @@ export function removePassthroughMode(threadId) {
193
193
  saveData(data);
194
194
  return true;
195
195
  }
196
+ export function setProjectAutoWorktree(alias, enabled) {
197
+ const data = loadData();
198
+ const project = data.projects.find(p => p.alias === alias);
199
+ if (!project)
200
+ return false;
201
+ project.autoWorktree = enabled;
202
+ saveData(data);
203
+ return true;
204
+ }
205
+ export function getProjectAutoWorktree(alias) {
206
+ const project = getProject(alias);
207
+ return project?.autoWorktree ?? false;
208
+ }
@@ -1,8 +1,11 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import pc from 'picocolors';
3
+ import open from 'open';
3
4
  import { setBotConfig, getBotConfig, hasBotConfig } from '../services/configStore.js';
4
5
  import { deployCommands } from './deploy.js';
5
6
  const DISCORD_DEV_URL = 'https://discord.com/developers/applications';
7
+ const BOT_PERMISSIONS = '2147534848';
8
+ const BOT_SCOPES = 'bot applications.commands';
6
9
  function validateApplicationId(value) {
7
10
  if (!value)
8
11
  return 'Application ID is required';
@@ -24,6 +27,21 @@ function validateGuildId(value) {
24
27
  return 'Invalid format (should be 17-20 digits)';
25
28
  return undefined;
26
29
  }
30
+ function generateInviteUrl(clientId) {
31
+ const url = new URL('https://discord.com/api/oauth2/authorize');
32
+ url.searchParams.set('client_id', clientId);
33
+ url.searchParams.set('permissions', BOT_PERMISSIONS);
34
+ url.searchParams.set('scope', BOT_SCOPES);
35
+ return url.toString();
36
+ }
37
+ async function openUrl(url) {
38
+ try {
39
+ await open(url);
40
+ }
41
+ catch {
42
+ // Silently fail - URL is displayed to user anyway
43
+ }
44
+ }
27
45
  export async function runSetupWizard() {
28
46
  console.clear();
29
47
  p.intro(pc.bgCyan(pc.black(' remote-opencode setup ')));
@@ -38,10 +56,21 @@ export async function runSetupWizard() {
38
56
  return;
39
57
  }
40
58
  }
41
- p.note(`1. Go to ${pc.cyan(DISCORD_DEV_URL)}\n` +
42
- `2. Click ${pc.bold('"New Application"')}\n` +
43
- `3. Give your application a name\n` +
44
- `4. Copy the ${pc.bold('Application ID')} from "General Information"`, 'Step 1: Create Discord Application');
59
+ // Step 1: Open Discord Developer Portal
60
+ p.note(`We'll open the Discord Developer Portal in your browser.\n\n` +
61
+ `1. Click ${pc.bold('"New Application"')}\n` +
62
+ `2. Give your application a name (e.g., "Remote OpenCode")\n` +
63
+ `3. Copy the ${pc.bold('Application ID')} from "General Information"`, 'Step 1: Create Discord Application');
64
+ const openPortal = await p.text({
65
+ message: `Press ${pc.cyan('Enter')} to open Discord Developer Portal...`,
66
+ placeholder: 'Press Enter',
67
+ defaultValue: '',
68
+ });
69
+ if (p.isCancel(openPortal)) {
70
+ p.cancel('Setup cancelled.');
71
+ process.exit(0);
72
+ }
73
+ await openUrl(DISCORD_DEV_URL);
45
74
  const clientId = await p.text({
46
75
  message: 'Enter your Discord Application ID:',
47
76
  placeholder: 'e.g., 1234567890123456789',
@@ -51,19 +80,27 @@ export async function runSetupWizard() {
51
80
  p.cancel('Setup cancelled.');
52
81
  process.exit(0);
53
82
  }
54
- p.note(`1. Go to the ${pc.bold('"Bot"')} section in the left sidebar\n` +
83
+ // Step 2: Enable Intents (guidance only)
84
+ p.note(`In the Discord Developer Portal:\n\n` +
85
+ `1. Go to the ${pc.bold('"Bot"')} section in the left sidebar\n` +
55
86
  `2. Scroll down to ${pc.bold('"Privileged Gateway Intents"')}\n` +
56
87
  `3. Enable these intents:\n` +
57
- ` ${pc.green('SERVER MEMBERS INTENT')}\n` +
58
- ` ${pc.green('MESSAGE CONTENT INTENT')}\n` +
59
- `4. Click "Save Changes"`, 'Step 2: Enable Required Intents');
60
- await p.confirm({
88
+ ` ${pc.green('*')} SERVER MEMBERS INTENT\n` +
89
+ ` ${pc.green('*')} MESSAGE CONTENT INTENT\n` +
90
+ `4. Click ${pc.bold('"Save Changes"')}`, 'Step 2: Enable Required Intents');
91
+ const intentsConfirm = await p.confirm({
61
92
  message: 'Have you enabled the required intents?',
62
93
  initialValue: true,
63
94
  });
64
- p.note(`1. In the ${pc.bold('"Bot"')} section\n` +
95
+ if (p.isCancel(intentsConfirm)) {
96
+ p.cancel('Setup cancelled.');
97
+ process.exit(0);
98
+ }
99
+ // Step 3: Get Bot Token (guidance only)
100
+ p.note(`Still in the Discord Developer Portal:\n\n` +
101
+ `1. In the ${pc.bold('"Bot"')} section\n` +
65
102
  `2. Click ${pc.bold('"Reset Token"')} (or "View Token" if available)\n` +
66
- `3. Copy the token (it's only shown once!)`, 'Step 3: Get Bot Token');
103
+ `3. Copy the token ${pc.dim('(it\'s only shown once!)')}`, 'Step 3: Get Bot Token');
67
104
  const discordToken = await p.password({
68
105
  message: 'Enter your Discord Bot Token:',
69
106
  validate: validateToken,
@@ -72,7 +109,8 @@ export async function runSetupWizard() {
72
109
  p.cancel('Setup cancelled.');
73
110
  process.exit(0);
74
111
  }
75
- p.note(`1. Open Discord and go to User Settings > Advanced\n` +
112
+ // Step 4: Get Guild ID (guidance only)
113
+ p.note(`1. Open Discord and go to ${pc.bold('User Settings > Advanced')}\n` +
76
114
  `2. Enable ${pc.bold('"Developer Mode"')}\n` +
77
115
  `3. Right-click on your server name\n` +
78
116
  `4. Click ${pc.bold('"Copy Server ID"')}`, 'Step 4: Get Guild (Server) ID');
@@ -85,6 +123,7 @@ export async function runSetupWizard() {
85
123
  p.cancel('Setup cancelled.');
86
124
  process.exit(0);
87
125
  }
126
+ // Save configuration
88
127
  const s = p.spinner();
89
128
  s.start('Saving configuration...');
90
129
  setBotConfig({
@@ -93,9 +132,22 @@ export async function runSetupWizard() {
93
132
  guildId: guildId,
94
133
  });
95
134
  s.stop('Configuration saved!');
96
- const inviteUrl = `https://discord.com/api/oauth2/authorize?client_id=${clientId}&permissions=2147534848&scope=bot`;
97
- p.note(`Open this URL in your browser:\n\n${pc.cyan(inviteUrl)}\n\n` +
98
- `Select your server and authorize the bot.`, 'Step 5: Invite Bot to Server');
135
+ // Step 5: Invite Bot to Server
136
+ const inviteUrl = generateInviteUrl(clientId);
137
+ p.note(`We'll open the bot invite page in your browser.\n\n` +
138
+ `1. Select your server\n` +
139
+ `2. Click ${pc.bold('"Authorize"')}\n\n` +
140
+ `${pc.dim('URL: ' + inviteUrl)}`, 'Step 5: Invite Bot to Server');
141
+ const openInvite = await p.text({
142
+ message: `Press ${pc.cyan('Enter')} to open the invite page...`,
143
+ placeholder: 'Press Enter',
144
+ defaultValue: '',
145
+ });
146
+ if (p.isCancel(openInvite)) {
147
+ p.cancel('Setup cancelled.');
148
+ process.exit(0);
149
+ }
150
+ await openUrl(inviteUrl);
99
151
  const invited = await p.confirm({
100
152
  message: 'Have you invited the bot to your server?',
101
153
  initialValue: true,
@@ -104,6 +156,7 @@ export async function runSetupWizard() {
104
156
  p.cancel('Setup cancelled.');
105
157
  process.exit(0);
106
158
  }
159
+ // Step 6: Deploy Commands
107
160
  const shouldDeploy = await p.confirm({
108
161
  message: 'Deploy slash commands now?',
109
162
  initialValue: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-opencode",
3
- "version": "1.0.3",
3
+ "version": "1.0.6",
4
4
  "description": "Discord bot for remote OpenCode CLI access",
5
5
  "main": "dist/src/index.js",
6
6
  "bin": {
@@ -19,7 +19,10 @@
19
19
  "dev": "node --loader ts-node/esm src/cli.ts",
20
20
  "deploy-commands": "npm run build && node dist/src/cli.js deploy",
21
21
  "test": "vitest",
22
- "prepublishOnly": "npm run build"
22
+ "prepublishOnly": "npm run build",
23
+ "release": "npm version patch && npm run build && npm publish",
24
+ "release:minor": "npm version minor && npm run build && npm publish",
25
+ "release:major": "npm version major && npm run build && npm publish"
23
26
  },
24
27
  "keywords": [
25
28
  "discord",
@@ -38,10 +41,13 @@
38
41
  "discord.js": "^14.25.1",
39
42
  "eventsource": "^4.1.0",
40
43
  "node-pty": "^1.1.0",
41
- "picocolors": "^1.1.1"
44
+ "open": "^10.1.0",
45
+ "picocolors": "^1.1.1",
46
+ "update-notifier": "^7.3.1"
42
47
  },
43
48
  "devDependencies": {
44
49
  "@types/node": "^25.1.0",
50
+ "@types/update-notifier": "^6.0.8",
45
51
  "ts-node": "^10.9.2",
46
52
  "typescript": "^5.9.3",
47
53
  "vitest": "^4.0.18"