opencode-pilot 0.13.1 → 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/service/poller.js +3 -2
- package/service/presets/github.yaml +19 -28
- package/test/unit/actions.test.js +128 -3
- package/test/unit/poll-service.test.js +7 -7
- package/test/unit/repo-config.test.js +24 -19
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
|
|
package/service/poller.js
CHANGED
|
@@ -255,8 +255,9 @@ async function executeCliCommand(command, args, timeout) {
|
|
|
255
255
|
}
|
|
256
256
|
return part;
|
|
257
257
|
});
|
|
258
|
-
// Quote parts with spaces
|
|
259
|
-
|
|
258
|
+
// Quote parts with spaces or shell special characters
|
|
259
|
+
const shellSpecialChars = /[ <>|&;$`"'\\!*?#~=\[\]{}()]/;
|
|
260
|
+
cmdStr = expandedCmd.map(p => shellSpecialChars.test(p) ? `"${p.replace(/"/g, '\\"')}"` : p).join(' ');
|
|
260
261
|
} else {
|
|
261
262
|
// String command - substitute ${argName} patterns
|
|
262
263
|
cmdStr = command.replace(/\$\{(\w+)\}/g, (_, name) => {
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
# GitHub source presets
|
|
1
|
+
# GitHub source presets (using gh CLI)
|
|
2
|
+
#
|
|
3
|
+
# These presets use the `gh` CLI instead of requiring a GitHub MCP server.
|
|
4
|
+
# The gh CLI must be installed and authenticated (gh auth login).
|
|
2
5
|
|
|
3
6
|
# Provider-level config (applies to all GitHub presets)
|
|
4
7
|
_provider:
|
|
5
|
-
response_key: items
|
|
6
8
|
mappings:
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
+
# Map gh CLI fields to standard fields
|
|
10
|
+
html_url: url
|
|
11
|
+
repository_full_name: repository.nameWithOwner
|
|
12
|
+
updated_at: updatedAt
|
|
9
13
|
# Reprocess items when state changes (e.g., reopened issues)
|
|
10
14
|
# Note: updated_at is NOT included because our own changes would trigger reprocessing
|
|
11
15
|
reprocess_on:
|
|
@@ -15,48 +19,35 @@ _provider:
|
|
|
15
19
|
my-issues:
|
|
16
20
|
name: my-issues
|
|
17
21
|
tool:
|
|
18
|
-
|
|
19
|
-
name: search_issues
|
|
20
|
-
args:
|
|
21
|
-
q: "is:issue assignee:@me state:open"
|
|
22
|
+
command: ["gh", "search", "issues", "--assignee=@me", "--state=open", "--json", "number,title,url,repository,state,body,updatedAt"]
|
|
22
23
|
item:
|
|
23
|
-
id: "{
|
|
24
|
-
repo: "{
|
|
24
|
+
id: "{url}"
|
|
25
|
+
repo: "{repository.nameWithOwner}"
|
|
25
26
|
session:
|
|
26
27
|
name: "{title}"
|
|
27
28
|
|
|
28
29
|
review-requests:
|
|
29
30
|
name: review-requests
|
|
30
31
|
tool:
|
|
31
|
-
|
|
32
|
-
name: search_issues
|
|
33
|
-
args:
|
|
34
|
-
q: "is:pr review-requested:@me state:open"
|
|
32
|
+
command: ["gh", "search", "prs", "--review-requested=@me", "--state=open", "--json", "number,title,url,repository,state,body,updatedAt"]
|
|
35
33
|
item:
|
|
36
|
-
id: "{
|
|
37
|
-
repo: "{
|
|
34
|
+
id: "{url}"
|
|
35
|
+
repo: "{repository.nameWithOwner}"
|
|
38
36
|
session:
|
|
39
37
|
name: "Review: {title}"
|
|
40
38
|
|
|
41
39
|
my-prs-feedback:
|
|
42
40
|
name: my-prs-feedback
|
|
43
41
|
tool:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
args:
|
|
47
|
-
# Catches PRs with any review activity (comments, reviews, or changes requested)
|
|
48
|
-
# Note: comments:>0 includes both review comments and issue comments
|
|
49
|
-
q: "is:pr author:@me state:open comments:>0"
|
|
42
|
+
# comments:>0 filter ensures only PRs with feedback are returned
|
|
43
|
+
command: ["gh", "search", "prs", "--author=@me", "--state=open", "comments:>0", "--json", "number,title,url,repository,state,body,updatedAt"]
|
|
50
44
|
item:
|
|
51
|
-
id: "{
|
|
52
|
-
repo: "{
|
|
45
|
+
id: "{url}"
|
|
46
|
+
repo: "{repository.nameWithOwner}"
|
|
53
47
|
session:
|
|
54
48
|
name: "Feedback: {title}"
|
|
55
49
|
# Reprocess when PR is updated (new commits pushed, new comments, etc.)
|
|
56
50
|
# This ensures we re-trigger after addressing review feedback
|
|
57
51
|
reprocess_on:
|
|
58
52
|
- state
|
|
59
|
-
-
|
|
60
|
-
# Filter out PRs where all comments are from bots or the PR author
|
|
61
|
-
# Fetches comments via API and enriches items with _comments for readiness check
|
|
62
|
-
filter_bot_comments: true
|
|
53
|
+
- updatedAt
|
|
@@ -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
|
});
|
|
@@ -159,15 +159,15 @@ sources:
|
|
|
159
159
|
loadRepoConfig(configPath);
|
|
160
160
|
|
|
161
161
|
const source = getSources()[0];
|
|
162
|
-
// Item with
|
|
162
|
+
// Item with repository.nameWithOwner (gh CLI output format)
|
|
163
163
|
const item = {
|
|
164
|
-
|
|
164
|
+
repository: { nameWithOwner: 'myorg/backend' },
|
|
165
165
|
number: 123,
|
|
166
|
-
|
|
166
|
+
url: 'https://github.com/myorg/backend/issues/123'
|
|
167
167
|
};
|
|
168
168
|
|
|
169
|
-
// Source should have repo field from preset (uses
|
|
170
|
-
assert.strictEqual(source.repo, '{
|
|
169
|
+
// Source should have repo field from preset (uses gh CLI field)
|
|
170
|
+
assert.strictEqual(source.repo, '{repository.nameWithOwner}');
|
|
171
171
|
|
|
172
172
|
// resolveRepoForItem should extract repo key from item
|
|
173
173
|
const repoKeys = resolveRepoForItem(source, item);
|
|
@@ -195,9 +195,9 @@ sources:
|
|
|
195
195
|
loadRepoConfig(configPath);
|
|
196
196
|
|
|
197
197
|
const source = getSources()[0];
|
|
198
|
-
// Item with
|
|
198
|
+
// Item with repository.nameWithOwner (gh CLI output format)
|
|
199
199
|
const item = {
|
|
200
|
-
|
|
200
|
+
repository: { nameWithOwner: 'unknown/repo' },
|
|
201
201
|
number: 456
|
|
202
202
|
};
|
|
203
203
|
|
|
@@ -340,7 +340,7 @@ sources: []
|
|
|
340
340
|
tools:
|
|
341
341
|
github:
|
|
342
342
|
mappings:
|
|
343
|
-
|
|
343
|
+
custom_field: some_source
|
|
344
344
|
|
|
345
345
|
sources: []
|
|
346
346
|
`);
|
|
@@ -350,10 +350,11 @@ sources: []
|
|
|
350
350
|
|
|
351
351
|
const toolConfig = getToolProviderConfig('github');
|
|
352
352
|
|
|
353
|
-
// GitHub preset
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
353
|
+
// GitHub preset now uses gh CLI and doesn't need response_key
|
|
354
|
+
// User config custom_field should be merged with preset mappings
|
|
355
|
+
assert.strictEqual(toolConfig.mappings.custom_field, 'some_source');
|
|
356
|
+
// GitHub provider has mappings for gh CLI field normalization
|
|
357
|
+
assert.ok(toolConfig.mappings.html_url, 'Should have html_url mapping');
|
|
357
358
|
assert.ok(toolConfig.mappings.repository_full_name, 'Should have repository_full_name mapping');
|
|
358
359
|
});
|
|
359
360
|
|
|
@@ -534,9 +535,10 @@ sources:
|
|
|
534
535
|
|
|
535
536
|
assert.strictEqual(sources.length, 1);
|
|
536
537
|
assert.strictEqual(sources[0].name, 'my-issues');
|
|
537
|
-
|
|
538
|
-
assert.
|
|
539
|
-
assert.
|
|
538
|
+
// GitHub presets now use gh CLI instead of MCP
|
|
539
|
+
assert.ok(Array.isArray(sources[0].tool.command), 'tool.command should be an array');
|
|
540
|
+
assert.ok(sources[0].tool.command.includes('gh'), 'command should use gh CLI');
|
|
541
|
+
assert.strictEqual(sources[0].item.id, '{url}');
|
|
540
542
|
assert.strictEqual(sources[0].prompt, 'worktree');
|
|
541
543
|
});
|
|
542
544
|
|
|
@@ -551,7 +553,8 @@ sources:
|
|
|
551
553
|
const sources = getSources();
|
|
552
554
|
|
|
553
555
|
assert.strictEqual(sources[0].name, 'review-requests');
|
|
554
|
-
|
|
556
|
+
// GitHub presets now use gh CLI instead of MCP
|
|
557
|
+
assert.ok(sources[0].tool.command.includes('--review-requested=@me'), 'command should include review-requested filter');
|
|
555
558
|
});
|
|
556
559
|
|
|
557
560
|
test('expands github/my-prs-feedback preset', async () => {
|
|
@@ -565,9 +568,11 @@ sources:
|
|
|
565
568
|
const sources = getSources();
|
|
566
569
|
|
|
567
570
|
assert.strictEqual(sources[0].name, 'my-prs-feedback');
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
assert.
|
|
571
|
+
// GitHub presets now use gh CLI instead of MCP
|
|
572
|
+
assert.ok(sources[0].tool.command.includes('--author=@me'), 'command should include author filter');
|
|
573
|
+
assert.ok(sources[0].tool.command.includes('comments:>0'), 'command should filter for PRs with comments');
|
|
574
|
+
// This preset includes updatedAt in reprocess_on to catch new commits
|
|
575
|
+
assert.deepStrictEqual(sources[0].reprocess_on, ['state', 'updatedAt']);
|
|
571
576
|
});
|
|
572
577
|
|
|
573
578
|
test('expands linear/my-issues preset with required args', async () => {
|
|
@@ -632,12 +637,12 @@ sources:
|
|
|
632
637
|
loadRepoConfig(configPath);
|
|
633
638
|
const sources = getSources();
|
|
634
639
|
|
|
635
|
-
// All GitHub presets should have repo field that references
|
|
636
|
-
// (
|
|
637
|
-
const mockItem = {
|
|
640
|
+
// All GitHub presets should have repo field that references repository.nameWithOwner
|
|
641
|
+
// (gh CLI returns this field directly)
|
|
642
|
+
const mockItem = { repository: { nameWithOwner: 'myorg/backend' } };
|
|
638
643
|
|
|
639
644
|
for (const source of sources) {
|
|
640
|
-
assert.strictEqual(source.repo, '{
|
|
645
|
+
assert.strictEqual(source.repo, '{repository.nameWithOwner}', `Preset ${source.name} should have repo field`);
|
|
641
646
|
const repos = resolveRepoForItem(source, mockItem);
|
|
642
647
|
assert.deepStrictEqual(repos, ['myorg/backend'], `Preset ${source.name} should resolve repo from item`);
|
|
643
648
|
}
|
|
@@ -656,12 +661,12 @@ sources:
|
|
|
656
661
|
loadRepoConfig(configPath);
|
|
657
662
|
const source = getSources()[0];
|
|
658
663
|
|
|
659
|
-
// Item from allowed repo should resolve (
|
|
660
|
-
const allowedItem = {
|
|
664
|
+
// Item from allowed repo should resolve (gh CLI returns repository.nameWithOwner)
|
|
665
|
+
const allowedItem = { repository: { nameWithOwner: 'myorg/backend' } };
|
|
661
666
|
assert.deepStrictEqual(resolveRepoForItem(source, allowedItem), ['myorg/backend']);
|
|
662
667
|
|
|
663
668
|
// Item from non-allowed repo should return empty (filtered out)
|
|
664
|
-
const filteredItem = {
|
|
669
|
+
const filteredItem = { repository: { nameWithOwner: 'other/repo' } };
|
|
665
670
|
assert.deepStrictEqual(resolveRepoForItem(source, filteredItem), []);
|
|
666
671
|
});
|
|
667
672
|
|