opencode-pilot 0.13.1 → 0.14.0

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.13.1",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "main": "plugin/index.js",
6
6
  "description": "Automation daemon for OpenCode - polls for work and spawns sessions",
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
- cmdStr = expandedCmd.map(p => p.includes(' ') ? `"${p}"` : p).join(' ');
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
- # Extract repo full name from repository_url (e.g., "https://api.github.com/repos/owner/repo")
8
- repository_full_name: "repository_url:/repos\\/([^/]+\\/[^/]+)$/"
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
- mcp: github
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: "{html_url}"
24
- repo: "{repository_full_name}"
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
- mcp: github
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: "{html_url}"
37
- repo: "{repository_full_name}"
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
- mcp: github
45
- name: search_issues
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: "{html_url}"
52
- repo: "{repository_full_name}"
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
- - updated_at
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
@@ -159,15 +159,15 @@ sources:
159
159
  loadRepoConfig(configPath);
160
160
 
161
161
  const source = getSources()[0];
162
- // Item with repository_full_name (mapped from repository_url by GitHub provider)
162
+ // Item with repository.nameWithOwner (gh CLI output format)
163
163
  const item = {
164
- repository_full_name: 'myorg/backend',
164
+ repository: { nameWithOwner: 'myorg/backend' },
165
165
  number: 123,
166
- html_url: 'https://github.com/myorg/backend/issues/123'
166
+ url: 'https://github.com/myorg/backend/issues/123'
167
167
  };
168
168
 
169
- // Source should have repo field from preset (uses mapped field)
170
- assert.strictEqual(source.repo, '{repository_full_name}');
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 repository_full_name (mapped from repository_url by GitHub provider)
198
+ // Item with repository.nameWithOwner (gh CLI output format)
199
199
  const item = {
200
- repository_full_name: 'unknown/repo',
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
- url: html_url
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 has response_key: items, user config doesn't override it
354
- assert.strictEqual(toolConfig.response_key, 'items');
355
- // GitHub provider has mapping for repository_full_name extraction from URL
356
- assert.strictEqual(toolConfig.mappings.url, 'html_url');
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
- assert.deepStrictEqual(sources[0].tool, { mcp: 'github', name: 'search_issues' });
538
- assert.strictEqual(sources[0].args.q, 'is:issue assignee:@me state:open');
539
- assert.strictEqual(sources[0].item.id, '{html_url}');
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
- assert.strictEqual(sources[0].args.q, 'is:pr review-requested:@me state:open');
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
- assert.strictEqual(sources[0].args.q, 'is:pr author:@me state:open comments:>0');
569
- // This preset includes updated_at in reprocess_on to catch new commits
570
- assert.deepStrictEqual(sources[0].reprocess_on, ['state', 'updated_at']);
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 repository_full_name
636
- // (which is mapped from repository_url by the GitHub provider)
637
- const mockItem = { repository_full_name: 'myorg/backend' };
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, '{repository_full_name}', `Preset ${source.name} should have repo field`);
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 (repository_full_name is mapped from repository_url)
660
- const allowedItem = { repository_full_name: 'myorg/backend' };
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 = { repository_full_name: 'other/repo' };
669
+ const filteredItem = { repository: { nameWithOwner: 'other/repo' } };
665
670
  assert.deepStrictEqual(resolveRepoForItem(source, filteredItem), []);
666
671
  });
667
672