remote-opencode 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -315,6 +315,40 @@ Bot: ❌ Passthrough mode disabled.
315
315
  - ⏳ **Busy indicator** — shows ⏳ reaction if previous task is still running
316
316
  - 🔒 **Safe** — ignores bot messages (no infinite loops)
317
317
 
318
+ ### `/autowork` — Toggle Automatic Worktree Creation
319
+
320
+ Enable automatic worktree creation for a project. When enabled, new `/opencode` sessions will automatically create isolated git worktrees.
321
+
322
+ ```
323
+ /autowork
324
+ ```
325
+
326
+ **How it works:**
327
+ 1. Run `/autowork` in a channel bound to a project
328
+ 2. The setting toggles on/off for that project
329
+ 3. When enabled, new sessions automatically create worktrees with branch names like `auto/abc12345-1738600000000`
330
+
331
+ **Example:**
332
+ ```
333
+ You: /autowork
334
+ Bot: ✅ Auto-worktree enabled for project myapp.
335
+ New sessions will automatically create isolated worktrees.
336
+
337
+ You: /opencode prompt:Add user authentication
338
+ Bot: [Creates thread + auto-worktree]
339
+ 🌳 Auto-Worktree: auto/abc12345-1738600000000
340
+ [Delete] [Create PR]
341
+ 📌 Prompt: Add user authentication
342
+ [streaming response...]
343
+ ```
344
+
345
+ **Features:**
346
+ - 🌳 **Automatic isolation** — each session gets its own branch and worktree
347
+ - 📱 **Mobile-friendly** — no need to type `/work` with branch names
348
+ - 🗑️ **Delete button** — removes worktree when done
349
+ - 🚀 **Create PR button** — easily create pull requests from worktree
350
+ - ⚡ **Per-project setting** — enable/disable independently for each project
351
+
318
352
  ---
319
353
 
320
354
  ## Usage Workflow
@@ -407,17 +441,21 @@ All configuration is stored in `~/.remote-opencode/`:
407
441
 
408
442
  ```json
409
443
  {
410
- "projects": {
411
- "myapp": "/Users/you/projects/my-app"
412
- },
413
- "bindings": {
414
- "channel-id": "myapp"
415
- },
416
- "threadSessions": { ... },
417
- "worktreeMappings": { ... }
444
+ "projects": [
445
+ { "alias": "myapp", "path": "/Users/you/projects/my-app", "autoWorktree": true }
446
+ ],
447
+ "bindings": [
448
+ { "channelId": "channel-id", "projectAlias": "myapp" }
449
+ ],
450
+ "threadSessions": [ ... ],
451
+ "worktreeMappings": [ ... ]
418
452
  }
419
453
  ```
420
454
 
455
+ | Field | Description |
456
+ |-------|-------------|
457
+ | `projects[].autoWorktree` | Optional. When `true`, new sessions auto-create worktrees |
458
+
421
459
  ---
422
460
 
423
461
  ## Troubleshooting
package/dist/src/cli.js CHANGED
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import pc from 'picocolors';
4
+ import { createRequire } from 'module';
4
5
  import { runSetupWizard } from './setup/wizard.js';
5
6
  import { deployCommands } from './setup/deploy.js';
6
7
  import { startBot } from './bot.js';
7
8
  import { hasBotConfig, getConfigDir } from './services/configStore.js';
9
+ const require = createRequire(import.meta.url);
10
+ const pkg = require('../../package.json');
8
11
  const program = new Command();
9
12
  program
10
13
  .name('remote-opencode')
11
14
  .description('Discord bot for remote OpenCode CLI access')
12
- .version('1.0.0');
15
+ .version(pkg.version);
13
16
  program
14
17
  .command('start')
15
18
  .description('Start the Discord bot')
@@ -19,6 +22,12 @@ program
19
22
  console.log(`Run ${pc.cyan('remote-opencode setup')} first to configure your Discord bot.\n`);
20
23
  process.exit(1);
21
24
  }
25
+ try {
26
+ await deployCommands();
27
+ }
28
+ catch {
29
+ console.log(pc.dim('Command deployment skipped (will retry on next start)'));
30
+ }
22
31
  await startBot();
23
32
  });
24
33
  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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-opencode",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
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",