opencode-pilot 0.16.2 → 0.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-pilot",
3
- "version": "0.16.2",
3
+ "version": "0.16.4",
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
 
@@ -470,6 +470,8 @@ function runSpawn(args, options = {}) {
470
470
  export async function createSessionViaApi(serverUrl, directory, prompt, options = {}) {
471
471
  const fetchFn = options.fetch || fetch;
472
472
 
473
+ let session = null;
474
+
473
475
  try {
474
476
  // Step 1: Create a new session with the directory parameter
475
477
  const sessionUrl = new URL('/session', serverUrl);
@@ -486,7 +488,7 @@ export async function createSessionViaApi(serverUrl, directory, prompt, options
486
488
  throw new Error(`Failed to create session: ${createResponse.status} ${errorText}`);
487
489
  }
488
490
 
489
- const session = await createResponse.json();
491
+ session = await createResponse.json();
490
492
  debug(`createSessionViaApi: created session ${session.id} in ${directory}`);
491
493
 
492
494
  // Step 2: Update session title if provided
@@ -543,6 +545,17 @@ export async function createSessionViaApi(serverUrl, directory, prompt, options
543
545
  };
544
546
  } catch (err) {
545
547
  debug(`createSessionViaApi: error - ${err.message}`);
548
+ // If session was created but message failed, still return success
549
+ // to prevent re-processing (the session exists, user can send message manually)
550
+ if (session) {
551
+ debug(`createSessionViaApi: session ${session.id} was created, marking as success despite message error`);
552
+ return {
553
+ success: true,
554
+ sessionId: session.id,
555
+ directory,
556
+ warning: err.message,
557
+ };
558
+ }
546
559
  return {
547
560
  success: false,
548
561
  error: err.message,
@@ -578,7 +591,8 @@ export async function executeAction(item, config, options = {}) {
578
591
  // Auto-detect worktree support: if not explicitly configured and server is running,
579
592
  // check if the project has sandboxes (indicating worktree workflow is set up)
580
593
  if (!worktreeMode && serverUrl) {
581
- const projectInfo = await getProjectInfo(serverUrl, { fetch: options.fetch });
594
+ // Look up project info for this specific directory (not just /project/current)
595
+ const projectInfo = await getProjectInfoForDirectory(serverUrl, baseCwd, { fetch: options.fetch });
582
596
  if (projectInfo?.sandboxes?.length > 0) {
583
597
  debug(`executeAction: auto-detected worktree support (${projectInfo.sandboxes.length} sandboxes)`);
584
598
  worktreeMode = 'new';
@@ -227,9 +227,13 @@ export async function pollOnce(options = {}) {
227
227
  itemUpdatedAt: item.updated_at || null,
228
228
  });
229
229
  }
230
- console.log(`[poll] Started session for ${item.id}`);
230
+ if (result.warning) {
231
+ console.log(`[poll] Started session for ${item.id} (warning: ${result.warning})`);
232
+ } else {
233
+ console.log(`[poll] Started session for ${item.id}`);
234
+ }
231
235
  } else {
232
- console.error(`[poll] Failed to start session: ${result.stderr}`);
236
+ console.error(`[poll] Failed to start session: ${result.error || result.stderr || 'unknown error'}`);
233
237
  }
234
238
  } catch (err) {
235
239
  console.error(`[poll] Error executing action: ${err.message}`);
@@ -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');
@@ -922,5 +930,47 @@ describe('actions.js', () => {
922
930
  assert.strictEqual(messageBody.providerID, 'anthropic', 'Should parse provider from model');
923
931
  assert.strictEqual(messageBody.modelID, 'claude-sonnet-4-20250514', 'Should parse model ID');
924
932
  });
933
+
934
+ test('returns success with warning when session created but message fails', async () => {
935
+ const { createSessionViaApi } = await import('../../service/actions.js');
936
+
937
+ const mockSessionId = 'ses_partial123';
938
+
939
+ const mockFetch = async (url, opts) => {
940
+ const urlObj = new URL(url);
941
+
942
+ // Session creation succeeds
943
+ if (urlObj.pathname === '/session' && opts?.method === 'POST') {
944
+ return {
945
+ ok: true,
946
+ json: async () => ({ id: mockSessionId }),
947
+ };
948
+ }
949
+
950
+ // Message send fails
951
+ if (urlObj.pathname.includes('/message') && opts?.method === 'POST') {
952
+ return {
953
+ ok: false,
954
+ status: 500,
955
+ text: async () => 'Message send failed',
956
+ };
957
+ }
958
+
959
+ return { ok: true, json: async () => ({}) };
960
+ };
961
+
962
+ const result = await createSessionViaApi(
963
+ 'http://localhost:4096',
964
+ '/path/to/project',
965
+ 'Fix the bug',
966
+ { fetch: mockFetch }
967
+ );
968
+
969
+ // Should return success because session was created
970
+ assert.ok(result.success, 'Should return success when session was created');
971
+ assert.strictEqual(result.sessionId, mockSessionId, 'Should return session ID');
972
+ assert.ok(result.warning, 'Should include warning about message failure');
973
+ assert.ok(result.warning.includes('Failed to send message'), 'Warning should mention message failure');
974
+ });
925
975
  });
926
976
  });
@@ -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,