opencode-pilot 0.2.2 → 0.4.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/examples/config.yaml +16 -4
- package/examples/templates/devcontainer.md +7 -2
- package/examples/templates/worktree.md +12 -0
- package/package.json +1 -1
- package/service/poll-service.js +3 -3
- package/service/poller.js +23 -7
- package/service/repo-config.js +17 -0
- package/test/unit/poller.test.js +59 -0
- package/test/unit/repo-config.test.js +39 -0
package/examples/config.yaml
CHANGED
|
@@ -10,21 +10,33 @@
|
|
|
10
10
|
|
|
11
11
|
# =============================================================================
|
|
12
12
|
# TOOLS - Field mappings for MCP servers (normalize different APIs)
|
|
13
|
+
#
|
|
14
|
+
# Each tool provider can specify:
|
|
15
|
+
# response_key: Key to extract array from response (e.g., "reminders", "nodes")
|
|
16
|
+
# mappings: Field name translations (e.g., { body: "description" })
|
|
13
17
|
# =============================================================================
|
|
14
18
|
tools:
|
|
15
19
|
github:
|
|
20
|
+
response_key: items # GitHub search returns { items: [...] }
|
|
16
21
|
mappings: {} # GitHub already uses standard field names
|
|
17
22
|
|
|
18
23
|
linear:
|
|
24
|
+
response_key: nodes # Linear returns { nodes: [...] }
|
|
19
25
|
mappings:
|
|
20
26
|
body: title # Use title as body
|
|
21
27
|
number: "url:/([A-Z0-9]+-[0-9]+)/" # Extract PROJ-123 from URL
|
|
22
28
|
|
|
29
|
+
apple-reminders:
|
|
30
|
+
response_key: reminders # Apple Reminders returns { reminders: [...] }
|
|
31
|
+
mappings:
|
|
32
|
+
title: name # Reminders uses 'name' not 'title'
|
|
33
|
+
body: notes # Reminders uses 'notes' for details
|
|
34
|
+
|
|
23
35
|
# =============================================================================
|
|
24
36
|
# SOURCES - What to poll (generic MCP tool references)
|
|
25
37
|
# =============================================================================
|
|
26
38
|
sources:
|
|
27
|
-
# GitHub issues assigned to me
|
|
39
|
+
# GitHub issues assigned to me
|
|
28
40
|
- name: my-issues
|
|
29
41
|
tool:
|
|
30
42
|
mcp: github
|
|
@@ -33,7 +45,7 @@ sources:
|
|
|
33
45
|
q: "is:issue assignee:@me state:open"
|
|
34
46
|
item:
|
|
35
47
|
id: "{html_url}"
|
|
36
|
-
prompt:
|
|
48
|
+
prompt: worktree
|
|
37
49
|
agent: plan
|
|
38
50
|
|
|
39
51
|
# GitHub PRs needing review
|
|
@@ -48,7 +60,7 @@ sources:
|
|
|
48
60
|
prompt: review
|
|
49
61
|
agent: plan
|
|
50
62
|
|
|
51
|
-
# Linear issues
|
|
63
|
+
# Linear issues
|
|
52
64
|
# NOTE: Replace teamId and assigneeId with your actual Linear UUIDs
|
|
53
65
|
# Find these via: linear_list_teams and check user IDs in team members
|
|
54
66
|
- name: linear-work
|
|
@@ -62,5 +74,5 @@ sources:
|
|
|
62
74
|
item:
|
|
63
75
|
id: "linear:{id}"
|
|
64
76
|
working_dir: ~/code/myproject
|
|
65
|
-
prompt:
|
|
77
|
+
prompt: worktree
|
|
66
78
|
agent: plan
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
Work on this issue in an isolated devcontainer:
|
|
2
2
|
|
|
3
3
|
{title}
|
|
4
4
|
|
|
5
5
|
{body}
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
First, set up an isolated workspace:
|
|
8
|
+
1. Create a shallow clone in a temp directory for branch `issue-{number}`
|
|
9
|
+
2. Open the devcontainer in that clone
|
|
10
|
+
3. Work from inside the devcontainer
|
|
11
|
+
|
|
12
|
+
Complete the implementation and create a PR when ready.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Work on this issue:
|
|
2
|
+
|
|
3
|
+
{title}
|
|
4
|
+
|
|
5
|
+
{body}
|
|
6
|
+
|
|
7
|
+
First, create a git worktree for this work:
|
|
8
|
+
1. Create branch `issue-{number}` if it doesn't exist
|
|
9
|
+
2. Create a worktree at `../$(basename $PWD)-issue-{number}`
|
|
10
|
+
3. Switch to that worktree directory
|
|
11
|
+
|
|
12
|
+
Follow TDD: write failing tests first, then implement. Create a PR when complete.
|
package/package.json
CHANGED
package/service/poll-service.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 5. Track processed items to avoid duplicates
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { loadRepoConfig, getRepoConfig, getAllSources,
|
|
12
|
+
import { loadRepoConfig, getRepoConfig, getAllSources, getToolProviderConfig } from "./repo-config.js";
|
|
13
13
|
import { createPoller, pollGenericSource } from "./poller.js";
|
|
14
14
|
import { evaluateReadiness, sortByPriority } from "./readiness.js";
|
|
15
15
|
import { executeAction, buildCommand } from "./actions.js";
|
|
@@ -100,8 +100,8 @@ export async function pollOnce(options = {}) {
|
|
|
100
100
|
// Fetch items from source
|
|
101
101
|
if (!skipMcp) {
|
|
102
102
|
try {
|
|
103
|
-
const
|
|
104
|
-
items = await pollGenericSource(source, {
|
|
103
|
+
const toolProviderConfig = getToolProviderConfig(source.tool.mcp);
|
|
104
|
+
items = await pollGenericSource(source, { toolProviderConfig });
|
|
105
105
|
debug(`Fetched ${items.length} items from ${sourceName}`);
|
|
106
106
|
} catch (err) {
|
|
107
107
|
console.error(`[poll] Error fetching from ${sourceName}: ${err.message}`);
|
package/service/poller.js
CHANGED
|
@@ -107,14 +107,28 @@ export function transformItems(items, idTemplate) {
|
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
109
|
* Parse JSON text as an array with error handling
|
|
110
|
+
* @param {string} text - JSON text to parse
|
|
111
|
+
* @param {string} sourceName - Source name for error logging
|
|
112
|
+
* @param {string} [responseKey] - Key to extract array from response object
|
|
113
|
+
* @returns {Array} Parsed array of items
|
|
110
114
|
*/
|
|
111
|
-
function parseJsonArray(text, sourceName) {
|
|
115
|
+
export function parseJsonArray(text, sourceName, responseKey) {
|
|
112
116
|
try {
|
|
113
117
|
const data = JSON.parse(text);
|
|
118
|
+
|
|
119
|
+
// If already an array, return it
|
|
114
120
|
if (Array.isArray(data)) return data;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
121
|
+
|
|
122
|
+
// If response_key is configured, use it to extract the array
|
|
123
|
+
if (responseKey) {
|
|
124
|
+
const items = data[responseKey];
|
|
125
|
+
if (Array.isArray(items)) return items;
|
|
126
|
+
// response_key was specified but not found or not an array
|
|
127
|
+
console.error(`[poller] Response key '${responseKey}' not found or not an array in ${sourceName} response`);
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// No response_key - wrap single object as array
|
|
118
132
|
return [data];
|
|
119
133
|
} catch (err) {
|
|
120
134
|
console.error(`[poller] Failed to parse ${sourceName} response:`, err.message);
|
|
@@ -206,13 +220,15 @@ function createTimeout(ms, operation) {
|
|
|
206
220
|
* @param {object} [options] - Additional options
|
|
207
221
|
* @param {number} [options.timeout] - Timeout in ms (default: 30000)
|
|
208
222
|
* @param {string} [options.opencodeConfigPath] - Path to opencode.json for MCP config
|
|
209
|
-
* @param {object} [options.
|
|
223
|
+
* @param {object} [options.toolProviderConfig] - Tool provider config (response_key, mappings)
|
|
210
224
|
* @returns {Promise<Array>} Array of items from the source with IDs and mappings applied
|
|
211
225
|
*/
|
|
212
226
|
export async function pollGenericSource(source, options = {}) {
|
|
213
227
|
const toolConfig = getToolConfig(source);
|
|
214
228
|
const timeout = options.timeout || DEFAULT_MCP_TIMEOUT;
|
|
215
|
-
const
|
|
229
|
+
const toolProviderConfig = options.toolProviderConfig || {};
|
|
230
|
+
const responseKey = toolProviderConfig.response_key;
|
|
231
|
+
const mappings = toolProviderConfig.mappings || null;
|
|
216
232
|
const mcpConfig = getMcpConfig(toolConfig.mcpServer, options.opencodeConfigPath);
|
|
217
233
|
const client = new Client({ name: "opencode-pilot", version: "1.0.0" });
|
|
218
234
|
|
|
@@ -235,7 +251,7 @@ export async function pollGenericSource(source, options = {}) {
|
|
|
235
251
|
const text = result.content?.[0]?.text;
|
|
236
252
|
if (!text) return [];
|
|
237
253
|
|
|
238
|
-
const rawItems = parseJsonArray(text, source.name);
|
|
254
|
+
const rawItems = parseJsonArray(text, source.name, responseKey);
|
|
239
255
|
|
|
240
256
|
// Apply field mappings before transforming
|
|
241
257
|
const mappedItems = mappings
|
package/service/repo-config.js
CHANGED
|
@@ -134,6 +134,23 @@ export function getToolMappings(provider) {
|
|
|
134
134
|
return toolConfig.mappings;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Get full tool provider configuration (response_key, mappings, etc.)
|
|
139
|
+
* @param {string} provider - Tool provider name (e.g., "github", "linear", "apple-reminders")
|
|
140
|
+
* @returns {object|null} Tool config including response_key and mappings, or null if not configured
|
|
141
|
+
*/
|
|
142
|
+
export function getToolProviderConfig(provider) {
|
|
143
|
+
const config = getRawConfig();
|
|
144
|
+
const tools = config.tools || {};
|
|
145
|
+
const toolConfig = tools[provider];
|
|
146
|
+
|
|
147
|
+
if (!toolConfig) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return toolConfig;
|
|
152
|
+
}
|
|
153
|
+
|
|
137
154
|
/**
|
|
138
155
|
* Load a template from the templates directory
|
|
139
156
|
* @param {string} templateName - Template name (without .md extension)
|
package/test/unit/poller.test.js
CHANGED
|
@@ -312,6 +312,65 @@ describe('poller.js', () => {
|
|
|
312
312
|
});
|
|
313
313
|
});
|
|
314
314
|
|
|
315
|
+
describe('parseJsonArray', () => {
|
|
316
|
+
test('parses direct array response', async () => {
|
|
317
|
+
const { parseJsonArray } = await import('../../service/poller.js');
|
|
318
|
+
|
|
319
|
+
const text = JSON.stringify([{ id: '1' }, { id: '2' }]);
|
|
320
|
+
const result = parseJsonArray(text, 'test');
|
|
321
|
+
|
|
322
|
+
assert.strictEqual(result.length, 2);
|
|
323
|
+
assert.strictEqual(result[0].id, '1');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('extracts array using response_key', async () => {
|
|
327
|
+
const { parseJsonArray } = await import('../../service/poller.js');
|
|
328
|
+
|
|
329
|
+
const text = JSON.stringify({
|
|
330
|
+
reminders: [
|
|
331
|
+
{ id: 'reminder-1', name: 'Task 1', completed: false },
|
|
332
|
+
{ id: 'reminder-2', name: 'Task 2', completed: false },
|
|
333
|
+
{ id: 'reminder-3', name: 'Task 3', completed: false }
|
|
334
|
+
],
|
|
335
|
+
count: 3
|
|
336
|
+
});
|
|
337
|
+
const result = parseJsonArray(text, 'agent-tasks', 'reminders');
|
|
338
|
+
|
|
339
|
+
assert.strictEqual(result.length, 3);
|
|
340
|
+
assert.strictEqual(result[0].id, 'reminder-1');
|
|
341
|
+
assert.strictEqual(result[0].name, 'Task 1');
|
|
342
|
+
assert.strictEqual(result[1].id, 'reminder-2');
|
|
343
|
+
assert.strictEqual(result[2].id, 'reminder-3');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('wraps single object as array when no response_key', async () => {
|
|
347
|
+
const { parseJsonArray } = await import('../../service/poller.js');
|
|
348
|
+
|
|
349
|
+
const text = JSON.stringify({ id: '1', title: 'Single item' });
|
|
350
|
+
const result = parseJsonArray(text, 'test');
|
|
351
|
+
|
|
352
|
+
assert.strictEqual(result.length, 1);
|
|
353
|
+
assert.strictEqual(result[0].id, '1');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('returns empty array for invalid JSON', async () => {
|
|
357
|
+
const { parseJsonArray } = await import('../../service/poller.js');
|
|
358
|
+
|
|
359
|
+
const result = parseJsonArray('not valid json', 'test');
|
|
360
|
+
|
|
361
|
+
assert.strictEqual(result.length, 0);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('returns empty array when response_key not found', async () => {
|
|
365
|
+
const { parseJsonArray } = await import('../../service/poller.js');
|
|
366
|
+
|
|
367
|
+
const text = JSON.stringify({ items: [{ id: '1' }] });
|
|
368
|
+
const result = parseJsonArray(text, 'test', 'reminders');
|
|
369
|
+
|
|
370
|
+
assert.strictEqual(result.length, 0);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
315
374
|
describe('transformItems with mappings', () => {
|
|
316
375
|
test('applies mappings to all items', async () => {
|
|
317
376
|
const { transformItems, applyMappings } = await import('../../service/poller.js');
|
|
@@ -314,6 +314,45 @@ sources: []
|
|
|
314
314
|
|
|
315
315
|
assert.strictEqual(mappings, null);
|
|
316
316
|
});
|
|
317
|
+
|
|
318
|
+
test('getToolProviderConfig returns full tool config with response_key', async () => {
|
|
319
|
+
writeFileSync(configPath, `
|
|
320
|
+
tools:
|
|
321
|
+
apple-reminders:
|
|
322
|
+
response_key: reminders
|
|
323
|
+
mappings:
|
|
324
|
+
body: notes
|
|
325
|
+
|
|
326
|
+
sources: []
|
|
327
|
+
`);
|
|
328
|
+
|
|
329
|
+
const { loadRepoConfig, getToolProviderConfig } = await import('../../service/repo-config.js');
|
|
330
|
+
loadRepoConfig(configPath);
|
|
331
|
+
|
|
332
|
+
const toolConfig = getToolProviderConfig('apple-reminders');
|
|
333
|
+
|
|
334
|
+
assert.strictEqual(toolConfig.response_key, 'reminders');
|
|
335
|
+
assert.deepStrictEqual(toolConfig.mappings, { body: 'notes' });
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('getToolProviderConfig returns config without response_key', async () => {
|
|
339
|
+
writeFileSync(configPath, `
|
|
340
|
+
tools:
|
|
341
|
+
github:
|
|
342
|
+
mappings:
|
|
343
|
+
url: html_url
|
|
344
|
+
|
|
345
|
+
sources: []
|
|
346
|
+
`);
|
|
347
|
+
|
|
348
|
+
const { loadRepoConfig, getToolProviderConfig } = await import('../../service/repo-config.js');
|
|
349
|
+
loadRepoConfig(configPath);
|
|
350
|
+
|
|
351
|
+
const toolConfig = getToolProviderConfig('github');
|
|
352
|
+
|
|
353
|
+
assert.strictEqual(toolConfig.response_key, undefined);
|
|
354
|
+
assert.deepStrictEqual(toolConfig.mappings, { url: 'html_url' });
|
|
355
|
+
});
|
|
317
356
|
});
|
|
318
357
|
|
|
319
358
|
describe('repo resolution for sources', () => {
|