opencode-pilot 0.16.1 → 0.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-pilot",
3
- "version": "0.16.1",
3
+ "version": "0.16.3",
4
4
  "type": "module",
5
5
  "main": "plugin/index.js",
6
6
  "description": "Automation daemon for OpenCode - polls for work and spawns sessions",
@@ -10,7 +10,7 @@ import { readFileSync, existsSync } from "fs";
10
10
  import { debug } from "./logger.js";
11
11
  import { getNestedValue } from "./utils.js";
12
12
  import { getServerPort } from "./repo-config.js";
13
- import { resolveWorktreeDirectory, getProjectInfo } from "./worktree.js";
13
+ import { resolveWorktreeDirectory, getProjectInfo, getProjectInfoForDirectory } from "./worktree.js";
14
14
  import path from "path";
15
15
  import os from "os";
16
16
 
@@ -578,7 +578,8 @@ export async function executeAction(item, config, options = {}) {
578
578
  // Auto-detect worktree support: if not explicitly configured and server is running,
579
579
  // check if the project has sandboxes (indicating worktree workflow is set up)
580
580
  if (!worktreeMode && serverUrl) {
581
- const projectInfo = await getProjectInfo(serverUrl, { fetch: options.fetch });
581
+ // Look up project info for this specific directory (not just /project/current)
582
+ const projectInfo = await getProjectInfoForDirectory(serverUrl, baseCwd, { fetch: options.fetch });
582
583
  if (projectInfo?.sandboxes?.length > 0) {
583
584
  debug(`executeAction: auto-detected worktree support (${projectInfo.sandboxes.length} sandboxes)`);
584
585
  worktreeMode = 'new';
package/service/utils.js CHANGED
@@ -61,6 +61,11 @@ export function isBot(username, type) {
61
61
  // Check for [bot] suffix in username
62
62
  if (username.toLowerCase().endsWith("[bot]")) return true;
63
63
 
64
+ // Known bot usernames without [bot] suffix
65
+ // Note: 'Copilot' is intentionally NOT included - Copilot review feedback is actionable
66
+ const knownBots = ['linear'];
67
+ if (knownBots.includes(username.toLowerCase())) return true;
68
+
64
69
  return false;
65
70
  }
66
71
 
@@ -40,6 +40,7 @@ export async function listWorktrees(serverUrl, options = {}) {
40
40
  *
41
41
  * @param {string} serverUrl - OpenCode server URL (e.g., "http://localhost:4096")
42
42
  * @param {object} [options] - Options
43
+ * @param {string} [options.directory] - Project directory (required for global server)
43
44
  * @param {string} [options.name] - Optional name for the worktree
44
45
  * @param {string} [options.startCommand] - Optional startup script to run after creation
45
46
  * @param {function} [options.fetch] - Custom fetch function (for testing)
@@ -53,7 +54,14 @@ export async function createWorktree(serverUrl, options = {}) {
53
54
  if (options.name) body.name = options.name;
54
55
  if (options.startCommand) body.startCommand = options.startCommand;
55
56
 
56
- const response = await fetchFn(`${serverUrl}/experimental/worktree`, {
57
+ // Build URL with directory parameter if provided
58
+ // This tells the global server which project to create the worktree for
59
+ let url = `${serverUrl}/experimental/worktree`;
60
+ if (options.directory) {
61
+ url += `?directory=${encodeURIComponent(options.directory)}`;
62
+ }
63
+
64
+ const response = await fetchFn(url, {
57
65
  method: 'POST',
58
66
  headers: { 'Content-Type': 'application/json' },
59
67
  body: JSON.stringify(body),
@@ -112,6 +120,48 @@ export async function getProjectInfo(serverUrl, options = {}) {
112
120
  }
113
121
  }
114
122
 
123
+ /**
124
+ * Get project info for a specific directory by querying all projects
125
+ *
126
+ * @param {string} serverUrl - OpenCode server URL
127
+ * @param {string} directory - Directory path to find project for
128
+ * @param {object} [options] - Options
129
+ * @param {function} [options.fetch] - Custom fetch function (for testing)
130
+ * @returns {Promise<object|null>} Project info or null if not found
131
+ */
132
+ export async function getProjectInfoForDirectory(serverUrl, directory, options = {}) {
133
+ const fetchFn = options.fetch || fetch;
134
+
135
+ try {
136
+ const response = await fetchFn(`${serverUrl}/project`);
137
+
138
+ if (!response.ok) {
139
+ debug(`getProjectInfoForDirectory: ${serverUrl} returned ${response.status}`);
140
+ return null;
141
+ }
142
+
143
+ const projects = await response.json();
144
+
145
+ // Find project matching this directory, preferring ones with sandboxes
146
+ const matches = projects.filter(p => p.worktree === directory);
147
+
148
+ if (matches.length === 0) {
149
+ debug(`getProjectInfoForDirectory: no project found for ${directory}`);
150
+ return null;
151
+ }
152
+
153
+ // Prefer the project with sandboxes (if multiple exist for same worktree)
154
+ const withSandboxes = matches.find(p => p.sandboxes?.length > 0);
155
+ const project = withSandboxes || matches[0];
156
+
157
+ debug(`getProjectInfoForDirectory: found project ${project.id} for ${directory} with ${project.sandboxes?.length || 0} sandboxes`);
158
+ return project;
159
+ } catch (err) {
160
+ debug(`getProjectInfoForDirectory: error - ${err.message}`);
161
+ return null;
162
+ }
163
+ }
164
+
115
165
  /**
116
166
  * Resolve the working directory based on worktree configuration
117
167
  *
@@ -147,6 +197,7 @@ export async function resolveWorktreeDirectory(serverUrl, baseDir, worktreeConfi
147
197
  // "new" - create a fresh worktree via OpenCode API
148
198
  if (worktreeValue === "new") {
149
199
  const result = await createWorktree(serverUrl, {
200
+ directory: baseDir,
150
201
  name: worktreeConfig.worktreeName,
151
202
  fetch: options.fetch,
152
203
  });
@@ -588,8 +588,12 @@ describe('actions.js', () => {
588
588
 
589
589
  // Mock worktree creation via fetch
590
590
  const mockFetch = async (url, opts) => {
591
- // Worktree creation endpoint
592
- if (url === 'http://localhost:4096/experimental/worktree' && opts?.method === 'POST') {
591
+ // Worktree creation endpoint - now includes directory query param
592
+ if (url.startsWith('http://localhost:4096/experimental/worktree') && opts?.method === 'POST') {
593
+ // Verify directory parameter is passed
594
+ const urlObj = new URL(url);
595
+ assert.strictEqual(urlObj.searchParams.get('directory'), tempDir,
596
+ 'Should pass directory as query param');
593
597
  const body = JSON.parse(opts.body);
594
598
  assert.strictEqual(body.name, 'feature-branch', 'Should pass worktree name');
595
599
  return {
@@ -708,25 +712,27 @@ describe('actions.js', () => {
708
712
  const mockDiscoverServer = async () => 'http://localhost:4096';
709
713
 
710
714
  // Track API calls
711
- let projectInfoCalled = false;
715
+ let projectListCalled = false;
712
716
  let worktreeCreateCalled = false;
713
717
 
714
718
  const mockFetch = async (url, opts) => {
715
- // Project info endpoint - returns sandboxes indicating worktree workflow
716
- if (url === 'http://localhost:4096/project/current') {
717
- projectInfoCalled = true;
719
+ // Project list endpoint - returns projects including one with sandboxes
720
+ if (url === 'http://localhost:4096/project') {
721
+ projectListCalled = true;
718
722
  return {
719
723
  ok: true,
720
- json: async () => ({
721
- id: 'proj-123',
722
- worktree: tempDir,
723
- sandboxes: ['/data/worktree/proj-123/sandbox-1'],
724
- time: { created: 1 }
725
- })
724
+ json: async () => ([
725
+ {
726
+ id: 'proj-123',
727
+ worktree: tempDir,
728
+ sandboxes: ['/data/worktree/proj-123/sandbox-1'],
729
+ time: { created: 1 }
730
+ }
731
+ ])
726
732
  };
727
733
  }
728
- // Worktree creation endpoint
729
- if (url === 'http://localhost:4096/experimental/worktree' && opts?.method === 'POST') {
734
+ // Worktree creation endpoint - now includes directory query param
735
+ if (url.startsWith('http://localhost:4096/experimental/worktree') && opts?.method === 'POST') {
730
736
  worktreeCreateCalled = true;
731
737
  return {
732
738
  ok: true,
@@ -747,7 +753,7 @@ describe('actions.js', () => {
747
753
  });
748
754
 
749
755
  assert.ok(result.dryRun);
750
- assert.ok(projectInfoCalled, 'Should call project/current to check for sandboxes');
756
+ assert.ok(projectListCalled, 'Should call /project to find project by directory');
751
757
  assert.ok(worktreeCreateCalled, 'Should auto-create worktree when sandboxes detected');
752
758
  assert.ok(result.command.includes('/data/worktree/proj-123/new-sandbox'),
753
759
  'Should use newly created worktree directory');
@@ -766,21 +772,23 @@ describe('actions.js', () => {
766
772
  // Mock server discovery
767
773
  const mockDiscoverServer = async () => 'http://localhost:4096';
768
774
 
769
- let projectInfoCalled = false;
775
+ let projectListCalled = false;
770
776
  let worktreeCreateCalled = false;
771
777
 
772
778
  const mockFetch = async (url, opts) => {
773
- // Project info endpoint - returns empty sandboxes (no worktree workflow)
774
- if (url === 'http://localhost:4096/project/current') {
775
- projectInfoCalled = true;
779
+ // Project list endpoint - returns project with empty sandboxes
780
+ if (url === 'http://localhost:4096/project') {
781
+ projectListCalled = true;
776
782
  return {
777
783
  ok: true,
778
- json: async () => ({
779
- id: 'proj-456',
780
- worktree: tempDir,
781
- sandboxes: [],
782
- time: { created: 1 }
783
- })
784
+ json: async () => ([
785
+ {
786
+ id: 'proj-456',
787
+ worktree: tempDir,
788
+ sandboxes: [],
789
+ time: { created: 1 }
790
+ }
791
+ ])
784
792
  };
785
793
  }
786
794
  if (url === 'http://localhost:4096/experimental/worktree' && opts?.method === 'POST') {
@@ -796,7 +804,7 @@ describe('actions.js', () => {
796
804
  });
797
805
 
798
806
  assert.ok(result.dryRun);
799
- assert.ok(projectInfoCalled, 'Should call project/current to check for sandboxes');
807
+ assert.ok(projectListCalled, 'Should call /project to find project by directory');
800
808
  assert.ok(!worktreeCreateCalled, 'Should NOT create worktree when no sandboxes');
801
809
  assert.ok(result.command.includes(tempDir),
802
810
  'Should use base directory when no worktree workflow detected');
@@ -82,6 +82,26 @@ describe("worktree", () => {
82
82
  assert.strictEqual(body.name, "my-feature");
83
83
  });
84
84
 
85
+ it("passes directory as query parameter", async () => {
86
+ const mockFetch = mock.fn(async (url, options) => ({
87
+ ok: true,
88
+ json: async () => ({
89
+ name: "test-worktree",
90
+ branch: "opencode/test-worktree",
91
+ directory: "/data/worktree/abc123/test-worktree",
92
+ }),
93
+ }));
94
+
95
+ await createWorktree("http://localhost:4096", {
96
+ directory: "/Users/test/code/my-project",
97
+ fetch: mockFetch,
98
+ });
99
+
100
+ const calledUrl = mockFetch.mock.calls[0].arguments[0];
101
+ assert.ok(calledUrl.includes("directory="), "URL should include directory parameter");
102
+ assert.ok(calledUrl.includes(encodeURIComponent("/Users/test/code/my-project")), "URL should include encoded directory path");
103
+ });
104
+
85
105
  it("returns error on failure", async () => {
86
106
  const mockFetch = mock.fn(async () => ({
87
107
  ok: false,