opencode-pilot 0.14.0 → 0.14.1

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
@@ -94,17 +94,9 @@ opencode-pilot test-mapping MCP # Test field mappings
94
94
 
95
95
  ## Known Issues
96
96
 
97
- ### Sessions attached to global server run in wrong directory
97
+ None currently! Previous issues have been resolved:
98
98
 
99
- When using `server_port` to attach sessions to a global OpenCode server (e.g., OpenCode Desktop with worktree="/"), sessions are created in the server's working directory (typically home) instead of the project directory. This means:
100
-
101
- - File tools resolve paths relative to home, not the project
102
- - The agent sees the wrong `Working directory` in system prompt
103
- - Git operations may target the wrong repository
104
-
105
- **Workaround**: Don't set `server_port` in your config. Sessions will run in the correct directory but won't appear in OpenCode Desktop.
106
-
107
- **Upstream issue**: [anomalyco/opencode#7376](https://github.com/anomalyco/opencode/issues/7376)
99
+ - ~~Sessions attached to global server run in wrong directory~~ - Fixed in v0.14.0 by using the HTTP API with `?directory=` parameter instead of `opencode run --attach`
108
100
 
109
101
  ## Related
110
102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-pilot",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "type": "module",
5
5
  "main": "plugin/index.js",
6
6
  "description": "Automation daemon for OpenCode - polls for work and spawns sessions",
@@ -449,6 +449,106 @@ function runSpawn(args, options = {}) {
449
449
  });
450
450
  }
451
451
 
452
+ /**
453
+ * Create a session via the OpenCode HTTP API
454
+ *
455
+ * This is a workaround for the known issue where `opencode run --attach`
456
+ * doesn't support a --dir flag, causing sessions to run in the wrong directory
457
+ * when attached to a global server.
458
+ *
459
+ * @param {string} serverUrl - Server URL (e.g., "http://localhost:4096")
460
+ * @param {string} directory - Working directory for the session
461
+ * @param {string} prompt - The prompt/message to send
462
+ * @param {object} [options] - Options
463
+ * @param {string} [options.title] - Session title
464
+ * @param {string} [options.agent] - Agent to use
465
+ * @param {string} [options.model] - Model to use
466
+ * @param {function} [options.fetch] - Custom fetch function (for testing)
467
+ * @returns {Promise<object>} Result with sessionId, success, error
468
+ */
469
+ export async function createSessionViaApi(serverUrl, directory, prompt, options = {}) {
470
+ const fetchFn = options.fetch || fetch;
471
+
472
+ try {
473
+ // Step 1: Create a new session with the directory parameter
474
+ const sessionUrl = new URL('/session', serverUrl);
475
+ sessionUrl.searchParams.set('directory', directory);
476
+
477
+ const createResponse = await fetchFn(sessionUrl.toString(), {
478
+ method: 'POST',
479
+ headers: { 'Content-Type': 'application/json' },
480
+ body: JSON.stringify({}),
481
+ });
482
+
483
+ if (!createResponse.ok) {
484
+ const errorText = await createResponse.text();
485
+ throw new Error(`Failed to create session: ${createResponse.status} ${errorText}`);
486
+ }
487
+
488
+ const session = await createResponse.json();
489
+ debug(`createSessionViaApi: created session ${session.id} in ${directory}`);
490
+
491
+ // Step 2: Update session title if provided
492
+ if (options.title) {
493
+ const updateUrl = new URL(`/session/${session.id}`, serverUrl);
494
+ updateUrl.searchParams.set('directory', directory);
495
+ await fetchFn(updateUrl.toString(), {
496
+ method: 'PATCH',
497
+ headers: { 'Content-Type': 'application/json' },
498
+ body: JSON.stringify({ title: options.title }),
499
+ });
500
+ }
501
+
502
+ // Step 3: Send the initial message
503
+ const messageUrl = new URL(`/session/${session.id}/message`, serverUrl);
504
+ messageUrl.searchParams.set('directory', directory);
505
+
506
+ // Build message body
507
+ const messageBody = {
508
+ parts: [{ type: 'text', text: prompt }],
509
+ };
510
+
511
+ // Add agent if specified
512
+ if (options.agent) {
513
+ messageBody.agent = options.agent;
514
+ }
515
+
516
+ // Add model if specified (format: provider/model)
517
+ if (options.model) {
518
+ const [providerID, modelID] = options.model.includes('/')
519
+ ? options.model.split('/', 2)
520
+ : ['anthropic', options.model];
521
+ messageBody.providerID = providerID;
522
+ messageBody.modelID = modelID;
523
+ }
524
+
525
+ const messageResponse = await fetchFn(messageUrl.toString(), {
526
+ method: 'POST',
527
+ headers: { 'Content-Type': 'application/json' },
528
+ body: JSON.stringify(messageBody),
529
+ });
530
+
531
+ if (!messageResponse.ok) {
532
+ const errorText = await messageResponse.text();
533
+ throw new Error(`Failed to send message: ${messageResponse.status} ${errorText}`);
534
+ }
535
+
536
+ debug(`createSessionViaApi: sent message to session ${session.id}`);
537
+
538
+ return {
539
+ success: true,
540
+ sessionId: session.id,
541
+ directory,
542
+ };
543
+ } catch (err) {
544
+ debug(`createSessionViaApi: error - ${err.message}`);
545
+ return {
546
+ success: false,
547
+ error: err.message,
548
+ };
549
+ }
550
+ }
551
+
452
552
  /**
453
553
  * Execute an action
454
554
  * @param {object} item - Item to create session for
@@ -456,6 +556,7 @@ function runSpawn(args, options = {}) {
456
556
  * @param {object} [options] - Execution options
457
557
  * @param {boolean} [options.dryRun] - If true, return command without executing
458
558
  * @param {function} [options.discoverServer] - Custom server discovery function (for testing)
559
+ * @param {function} [options.fetch] - Custom fetch function (for testing)
459
560
  * @returns {Promise<object>} Result with command, stdout, stderr, exitCode
460
561
  */
461
562
  export async function executeAction(item, config, options = {}) {
@@ -469,8 +570,48 @@ export async function executeAction(item, config, options = {}) {
469
570
 
470
571
  debug(`executeAction: discovered server=${serverUrl} for cwd=${cwd}`);
471
572
 
472
- // Build command info with server URL for --attach flag
473
- const cmdInfo = getCommandInfoNew(item, config, undefined, serverUrl);
573
+ // If a server is running, use the HTTP API to create the session
574
+ // This is a workaround for the known issue where --attach doesn't support --dir
575
+ // See: https://github.com/anomalyco/opencode/issues/7376
576
+ if (serverUrl) {
577
+ // Build prompt from template
578
+ const prompt = buildPromptFromTemplate(config.prompt || "default", item);
579
+
580
+ // Build session title
581
+ const sessionTitle = config.session?.name
582
+ ? buildSessionName(config.session.name, item)
583
+ : (item.title || `session-${Date.now()}`);
584
+
585
+ const apiCommand = `[API] POST ${serverUrl}/session?directory=${cwd}`;
586
+ debug(`executeAction: using HTTP API - ${apiCommand}`);
587
+
588
+ if (options.dryRun) {
589
+ return {
590
+ command: apiCommand,
591
+ dryRun: true,
592
+ method: 'api',
593
+ };
594
+ }
595
+
596
+ const result = await createSessionViaApi(serverUrl, cwd, prompt, {
597
+ title: sessionTitle,
598
+ agent: config.agent,
599
+ model: config.model,
600
+ fetch: options.fetch,
601
+ });
602
+
603
+ return {
604
+ command: apiCommand,
605
+ success: result.success,
606
+ sessionId: result.sessionId,
607
+ error: result.error,
608
+ method: 'api',
609
+ };
610
+ }
611
+
612
+ // No server running - fall back to spawning opencode run
613
+ // This works correctly because we set cwd on the spawn
614
+ const cmdInfo = getCommandInfoNew(item, config, undefined, null);
474
615
 
475
616
  // Build command string for display
476
617
  const quoteArgs = (args) => args.map(a =>
@@ -486,6 +627,7 @@ export async function executeAction(item, config, options = {}) {
486
627
  return {
487
628
  command,
488
629
  dryRun: true,
630
+ method: 'spawn',
489
631
  };
490
632
  }
491
633
 
@@ -505,6 +647,7 @@ export async function executeAction(item, config, options = {}) {
505
647
  command,
506
648
  success: true,
507
649
  pid: child.pid,
650
+ method: 'spawn',
508
651
  };
509
652
  }
510
653
 
@@ -525,7 +525,7 @@ describe('actions.js', () => {
525
525
  });
526
526
 
527
527
  describe('executeAction', () => {
528
- test('discovers server and includes --attach in dry run', async () => {
528
+ test('uses HTTP API when server is discovered (dry run)', async () => {
529
529
  const { executeAction } = await import('../../service/actions.js');
530
530
 
531
531
  const item = { number: 123, title: 'Fix bug' };
@@ -543,11 +543,13 @@ describe('actions.js', () => {
543
543
  });
544
544
 
545
545
  assert.ok(result.dryRun);
546
- assert.ok(result.command.includes('--attach'), 'Command should include --attach flag');
546
+ assert.strictEqual(result.method, 'api', 'Should use API method when server found');
547
+ assert.ok(result.command.includes('POST'), 'Command should show POST request');
547
548
  assert.ok(result.command.includes('http://localhost:4096'), 'Command should include server URL');
549
+ assert.ok(result.command.includes('directory='), 'Command should include directory param');
548
550
  });
549
551
 
550
- test('does not include --attach when no server discovered', async () => {
552
+ test('falls back to spawn when no server discovered (dry run)', async () => {
551
553
  const { executeAction } = await import('../../service/actions.js');
552
554
 
553
555
  const item = { number: 123, title: 'Fix bug' };
@@ -565,7 +567,130 @@ describe('actions.js', () => {
565
567
  });
566
568
 
567
569
  assert.ok(result.dryRun);
570
+ assert.strictEqual(result.method, 'spawn', 'Should use spawn method when no server');
568
571
  assert.ok(!result.command.includes('--attach'), 'Command should not include --attach flag');
572
+ assert.ok(result.command.includes('opencode run'), 'Command should include opencode run');
573
+ });
574
+ });
575
+
576
+ describe('createSessionViaApi', () => {
577
+ test('creates session and sends message with directory param', async () => {
578
+ const { createSessionViaApi } = await import('../../service/actions.js');
579
+
580
+ const mockSessionId = 'ses_test123';
581
+ let createCalled = false;
582
+ let messageCalled = false;
583
+ let createUrl = null;
584
+ let messageUrl = null;
585
+
586
+ const mockFetch = async (url, opts) => {
587
+ const urlObj = new URL(url);
588
+
589
+ if (urlObj.pathname === '/session' && opts?.method === 'POST') {
590
+ createCalled = true;
591
+ createUrl = url;
592
+ return {
593
+ ok: true,
594
+ json: async () => ({ id: mockSessionId }),
595
+ };
596
+ }
597
+
598
+ if (urlObj.pathname.includes('/message') && opts?.method === 'POST') {
599
+ messageCalled = true;
600
+ messageUrl = url;
601
+ return {
602
+ ok: true,
603
+ json: async () => ({ success: true }),
604
+ };
605
+ }
606
+
607
+ return { ok: false, text: async () => 'Not found' };
608
+ };
609
+
610
+ const result = await createSessionViaApi(
611
+ 'http://localhost:4096',
612
+ '/path/to/project',
613
+ 'Fix the bug',
614
+ { fetch: mockFetch }
615
+ );
616
+
617
+ assert.ok(result.success, 'Should succeed');
618
+ assert.strictEqual(result.sessionId, mockSessionId, 'Should return session ID');
619
+ assert.ok(createCalled, 'Should call create session endpoint');
620
+ assert.ok(messageCalled, 'Should call message endpoint');
621
+ // URL encodes slashes as %2F
622
+ assert.ok(createUrl.includes('directory='), 'Create URL should include directory param');
623
+ assert.ok(createUrl.includes('%2Fpath%2Fto%2Fproject'), 'Create URL should include encoded directory path');
624
+ assert.ok(messageUrl.includes('directory='), 'Message URL should include directory param');
625
+ assert.ok(messageUrl.includes('%2Fpath%2Fto%2Fproject'), 'Message URL should include encoded directory path');
626
+ });
627
+
628
+ test('handles session creation failure', async () => {
629
+ const { createSessionViaApi } = await import('../../service/actions.js');
630
+
631
+ const mockFetch = async () => ({
632
+ ok: false,
633
+ status: 500,
634
+ text: async () => 'Internal server error',
635
+ });
636
+
637
+ const result = await createSessionViaApi(
638
+ 'http://localhost:4096',
639
+ '/path/to/project',
640
+ 'Fix the bug',
641
+ { fetch: mockFetch }
642
+ );
643
+
644
+ assert.ok(!result.success, 'Should fail');
645
+ assert.ok(result.error.includes('Failed to create session'), 'Should include error message');
646
+ });
647
+
648
+ test('passes agent and model options', async () => {
649
+ const { createSessionViaApi } = await import('../../service/actions.js');
650
+
651
+ let messageBody = null;
652
+
653
+ const mockFetch = async (url, opts) => {
654
+ const urlObj = new URL(url);
655
+
656
+ if (urlObj.pathname === '/session' && opts?.method === 'POST') {
657
+ return {
658
+ ok: true,
659
+ json: async () => ({ id: 'ses_test' }),
660
+ };
661
+ }
662
+
663
+ if (urlObj.pathname.includes('/message')) {
664
+ messageBody = JSON.parse(opts.body);
665
+ return {
666
+ ok: true,
667
+ json: async () => ({ success: true }),
668
+ };
669
+ }
670
+
671
+ // PATCH for title update
672
+ if (opts?.method === 'PATCH') {
673
+ return { ok: true, json: async () => ({}) };
674
+ }
675
+
676
+ return { ok: false, text: async () => 'Not found' };
677
+ };
678
+
679
+ await createSessionViaApi(
680
+ 'http://localhost:4096',
681
+ '/path/to/project',
682
+ 'Fix the bug',
683
+ {
684
+ fetch: mockFetch,
685
+ agent: 'code',
686
+ model: 'anthropic/claude-sonnet-4-20250514',
687
+ title: 'Test Session',
688
+ }
689
+ );
690
+
691
+ assert.strictEqual(messageBody.agent, 'code', 'Should pass agent');
692
+ assert.strictEqual(messageBody.providerID, 'anthropic', 'Should parse provider from model');
693
+ assert.strictEqual(messageBody.modelID, 'claude-sonnet-4-20250514', 'Should parse model ID');
569
694
  });
570
695
  });
571
696
  });