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 +46 -8
- package/dist/src/cli.js +10 -1
- package/dist/src/commands/autowork.js +41 -0
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/opencode.js +41 -2
- package/dist/src/handlers/messageHandler.js +42 -2
- package/dist/src/services/dataStore.js +13 -0
- package/package.json +5 -2
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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",
|