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 +2 -10
- package/package.json +1 -1
- package/service/actions.js +145 -2
- package/test/unit/actions.test.js +128 -3
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
|
-
|
|
97
|
+
None currently! Previous issues have been resolved:
|
|
98
98
|
|
|
99
|
-
|
|
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
package/service/actions.js
CHANGED
|
@@ -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
|
-
//
|
|
473
|
-
|
|
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('
|
|
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.
|
|
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('
|
|
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
|
});
|