opencode-pilot 0.12.0 → 0.13.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 +0 -14
- package/bin/opencode-pilot +4 -4
- package/package.json +1 -1
- package/plugin/index.js +1 -1
- package/service/poller.js +106 -4
- package/service/repo-config.js +1 -1
- package/test/unit/poller.test.js +54 -0
package/README.md
CHANGED
|
@@ -106,20 +106,6 @@ When using `server_port` to attach sessions to a global OpenCode server (e.g., O
|
|
|
106
106
|
|
|
107
107
|
**Upstream issue**: [anomalyco/opencode#7376](https://github.com/anomalyco/opencode/issues/7376)
|
|
108
108
|
|
|
109
|
-
### Working directory doesn't switch when templates create worktrees/devcontainers
|
|
110
|
-
|
|
111
|
-
When a template instructs the agent to create a git worktree or switch to a devcontainer, OpenCode's internal working directory context (`Instance.directory`) doesn't update. This means:
|
|
112
|
-
|
|
113
|
-
- The "Session changes" panel shows diffs from the original directory
|
|
114
|
-
- File tools may resolve paths relative to the wrong location
|
|
115
|
-
- The agent works in the new directory, but OpenCode doesn't follow
|
|
116
|
-
|
|
117
|
-
**Workaround**: Start OpenCode directly in the target directory, or use separate terminal sessions.
|
|
118
|
-
|
|
119
|
-
**Upstream issue**: [anomalyco/opencode#6697](https://github.com/anomalyco/opencode/issues/6697)
|
|
120
|
-
|
|
121
|
-
**Related**: [opencode-devcontainers#103](https://github.com/athal7/opencode-devcontainers/issues/103)
|
|
122
|
-
|
|
123
109
|
## Related
|
|
124
110
|
|
|
125
111
|
- [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) - Run multiple devcontainer instances for OpenCode
|
package/bin/opencode-pilot
CHANGED
|
@@ -39,8 +39,8 @@ function findServiceDir() {
|
|
|
39
39
|
const serviceDir = findServiceDir();
|
|
40
40
|
|
|
41
41
|
// Paths
|
|
42
|
-
const PILOT_CONFIG_FILE = join(os.homedir(), ".config/opencode
|
|
43
|
-
const PILOT_TEMPLATES_DIR = join(os.homedir(), ".config/opencode
|
|
42
|
+
const PILOT_CONFIG_FILE = join(os.homedir(), ".config/opencode/pilot/config.yaml");
|
|
43
|
+
const PILOT_TEMPLATES_DIR = join(os.homedir(), ".config/opencode/pilot/templates");
|
|
44
44
|
|
|
45
45
|
// Default port
|
|
46
46
|
const DEFAULT_PORT = 4097;
|
|
@@ -231,7 +231,7 @@ async function configCommand() {
|
|
|
231
231
|
console.log(`Config file not found: ${PILOT_CONFIG_FILE}`);
|
|
232
232
|
console.log("");
|
|
233
233
|
console.log("Create one by copying the example:");
|
|
234
|
-
console.log(" cp node_modules/opencode-pilot/examples/config.yaml ~/.config/opencode
|
|
234
|
+
console.log(" cp node_modules/opencode-pilot/examples/config.yaml ~/.config/opencode/pilot/config.yaml");
|
|
235
235
|
process.exit(1);
|
|
236
236
|
}
|
|
237
237
|
|
|
@@ -666,7 +666,7 @@ async function clearCommand(flags) {
|
|
|
666
666
|
// No flags - show current state summary
|
|
667
667
|
console.log("Poll state summary:");
|
|
668
668
|
console.log(` Total entries: ${beforeCount}`);
|
|
669
|
-
console.log(` State file: ~/.config/opencode
|
|
669
|
+
console.log(` State file: ~/.config/opencode/pilot/poll-state.json`);
|
|
670
670
|
console.log("");
|
|
671
671
|
console.log("Usage:");
|
|
672
672
|
console.log(" opencode-pilot clear --all Clear all entries");
|
package/package.json
CHANGED
package/plugin/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { homedir } from 'os'
|
|
|
10
10
|
import YAML from 'yaml'
|
|
11
11
|
|
|
12
12
|
const DEFAULT_PORT = 4097
|
|
13
|
-
const CONFIG_PATH = join(homedir(), '.config', 'opencode
|
|
13
|
+
const CONFIG_PATH = join(homedir(), '.config', 'opencode', 'pilot', 'config.yaml')
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Load port from config file
|
package/service/poller.js
CHANGED
|
@@ -67,15 +67,33 @@ export function applyMappings(item, mappings) {
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Get tool configuration from a source
|
|
70
|
+
* Supports both MCP tools and CLI commands.
|
|
71
|
+
*
|
|
70
72
|
* @param {object} source - Source configuration from config.yaml
|
|
71
|
-
* @returns {object} Tool configuration
|
|
73
|
+
* @returns {object} Tool configuration with type indicator
|
|
72
74
|
*/
|
|
73
75
|
export function getToolConfig(source) {
|
|
74
|
-
if (!source.tool
|
|
75
|
-
throw new Error(`Source '${source.name || 'unknown'}' missing tool configuration
|
|
76
|
+
if (!source.tool) {
|
|
77
|
+
throw new Error(`Source '${source.name || 'unknown'}' missing tool configuration`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// CLI command support
|
|
81
|
+
if (source.tool.command) {
|
|
82
|
+
return {
|
|
83
|
+
type: 'cli',
|
|
84
|
+
command: source.tool.command,
|
|
85
|
+
args: source.args || {},
|
|
86
|
+
idTemplate: source.item?.id || null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// MCP tool support (existing behavior)
|
|
91
|
+
if (!source.tool.mcp || !source.tool.name) {
|
|
92
|
+
throw new Error(`Source '${source.name || 'unknown'}' missing tool configuration (requires tool.mcp and tool.name, or tool.command)`);
|
|
76
93
|
}
|
|
77
94
|
|
|
78
95
|
return {
|
|
96
|
+
type: 'mcp',
|
|
79
97
|
mcpServer: source.tool.mcp,
|
|
80
98
|
toolName: source.tool.name,
|
|
81
99
|
args: source.args || {},
|
|
@@ -214,7 +232,84 @@ function createTimeout(ms, operation) {
|
|
|
214
232
|
}
|
|
215
233
|
|
|
216
234
|
/**
|
|
217
|
-
*
|
|
235
|
+
* Execute a CLI command and return parsed JSON output
|
|
236
|
+
*
|
|
237
|
+
* @param {string|string[]} command - Command to execute (string or array)
|
|
238
|
+
* @param {object} args - Arguments to substitute into command
|
|
239
|
+
* @param {number} timeout - Timeout in ms
|
|
240
|
+
* @returns {Promise<string>} Command output
|
|
241
|
+
*/
|
|
242
|
+
async function executeCliCommand(command, args, timeout) {
|
|
243
|
+
const { exec } = await import('child_process');
|
|
244
|
+
const { promisify } = await import('util');
|
|
245
|
+
const execAsync = promisify(exec);
|
|
246
|
+
|
|
247
|
+
// Build command string
|
|
248
|
+
let cmdStr;
|
|
249
|
+
if (Array.isArray(command)) {
|
|
250
|
+
// Substitute args into command array
|
|
251
|
+
const expandedCmd = command.map(part => {
|
|
252
|
+
if (typeof part === 'string' && part.startsWith('$')) {
|
|
253
|
+
const argName = part.slice(1);
|
|
254
|
+
return args[argName] !== undefined ? String(args[argName]) : part;
|
|
255
|
+
}
|
|
256
|
+
return part;
|
|
257
|
+
});
|
|
258
|
+
// Quote parts with spaces
|
|
259
|
+
cmdStr = expandedCmd.map(p => p.includes(' ') ? `"${p}"` : p).join(' ');
|
|
260
|
+
} else {
|
|
261
|
+
// String command - substitute ${argName} patterns
|
|
262
|
+
cmdStr = command.replace(/\$\{(\w+)\}/g, (_, name) => {
|
|
263
|
+
return args[name] !== undefined ? String(args[name]) : '';
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const { stdout } = await Promise.race([
|
|
268
|
+
execAsync(cmdStr, { env: { ...process.env } }),
|
|
269
|
+
createTimeout(timeout, `CLI command: ${cmdStr.slice(0, 50)}...`),
|
|
270
|
+
]);
|
|
271
|
+
|
|
272
|
+
return stdout;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Poll a source using CLI command
|
|
277
|
+
*
|
|
278
|
+
* @param {object} source - Source configuration from config.yaml
|
|
279
|
+
* @param {object} toolConfig - Tool config from getToolConfig()
|
|
280
|
+
* @param {object} [options] - Additional options
|
|
281
|
+
* @param {number} [options.timeout] - Timeout in ms (default: 30000)
|
|
282
|
+
* @param {object} [options.toolProviderConfig] - Tool provider config (response_key, mappings)
|
|
283
|
+
* @returns {Promise<Array>} Array of items from the source with IDs and mappings applied
|
|
284
|
+
*/
|
|
285
|
+
async function pollCliSource(source, toolConfig, options = {}) {
|
|
286
|
+
const timeout = options.timeout || DEFAULT_MCP_TIMEOUT;
|
|
287
|
+
const toolProviderConfig = options.toolProviderConfig || {};
|
|
288
|
+
const responseKey = toolProviderConfig.response_key;
|
|
289
|
+
const mappings = toolProviderConfig.mappings || null;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const output = await executeCliCommand(toolConfig.command, toolConfig.args, timeout);
|
|
293
|
+
|
|
294
|
+
if (!output || !output.trim()) return [];
|
|
295
|
+
|
|
296
|
+
const rawItems = parseJsonArray(output, source.name, responseKey);
|
|
297
|
+
|
|
298
|
+
// Apply field mappings before transforming
|
|
299
|
+
const mappedItems = mappings
|
|
300
|
+
? rawItems.map(item => applyMappings(item, mappings))
|
|
301
|
+
: rawItems;
|
|
302
|
+
|
|
303
|
+
// Transform items (add IDs)
|
|
304
|
+
return transformItems(mappedItems, toolConfig.idTemplate);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.error(`[poller] CLI command failed for ${source.name}: ${err.message}`);
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Poll a source using MCP tools or CLI commands
|
|
218
313
|
*
|
|
219
314
|
* @param {object} source - Source configuration from config.yaml
|
|
220
315
|
* @param {object} [options] - Additional options
|
|
@@ -225,6 +320,13 @@ function createTimeout(ms, operation) {
|
|
|
225
320
|
*/
|
|
226
321
|
export async function pollGenericSource(source, options = {}) {
|
|
227
322
|
const toolConfig = getToolConfig(source);
|
|
323
|
+
|
|
324
|
+
// Route to CLI handler if command-based
|
|
325
|
+
if (toolConfig.type === 'cli') {
|
|
326
|
+
return pollCliSource(source, toolConfig, options);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// MCP-based polling (existing behavior)
|
|
228
330
|
const timeout = options.timeout || DEFAULT_MCP_TIMEOUT;
|
|
229
331
|
const toolProviderConfig = options.toolProviderConfig || {};
|
|
230
332
|
const responseKey = toolProviderConfig.response_key;
|
package/service/repo-config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* repo-config.js - Configuration management
|
|
3
3
|
*
|
|
4
|
-
* Manages configuration stored in ~/.config/opencode
|
|
4
|
+
* Manages configuration stored in ~/.config/opencode/pilot/config.yaml
|
|
5
5
|
* Supports:
|
|
6
6
|
* - defaults: default values applied to all sources
|
|
7
7
|
* - repos: per-repository settings (use YAML anchors for sharing)
|
package/test/unit/poller.test.js
CHANGED
|
@@ -57,6 +57,60 @@ describe('poller.js', () => {
|
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
describe('getToolConfig', () => {
|
|
61
|
+
test('returns MCP config for mcp-based sources', async () => {
|
|
62
|
+
const { getToolConfig } = await import('../../service/poller.js');
|
|
63
|
+
|
|
64
|
+
const source = {
|
|
65
|
+
name: 'test-source',
|
|
66
|
+
tool: { mcp: 'github', name: 'search_issues' },
|
|
67
|
+
args: { q: 'is:open' },
|
|
68
|
+
item: { id: '{html_url}' }
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const config = getToolConfig(source);
|
|
72
|
+
assert.strictEqual(config.type, 'mcp');
|
|
73
|
+
assert.strictEqual(config.mcpServer, 'github');
|
|
74
|
+
assert.strictEqual(config.toolName, 'search_issues');
|
|
75
|
+
assert.deepStrictEqual(config.args, { q: 'is:open' });
|
|
76
|
+
assert.strictEqual(config.idTemplate, '{html_url}');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('returns CLI config for command-based sources', async () => {
|
|
80
|
+
const { getToolConfig } = await import('../../service/poller.js');
|
|
81
|
+
|
|
82
|
+
const source = {
|
|
83
|
+
name: 'test-source',
|
|
84
|
+
tool: { command: ['granola-cli', 'meetings', 'list', '20'] },
|
|
85
|
+
item: { id: 'meeting:{id}' }
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const config = getToolConfig(source);
|
|
89
|
+
assert.strictEqual(config.type, 'cli');
|
|
90
|
+
assert.deepStrictEqual(config.command, ['granola-cli', 'meetings', 'list', '20']);
|
|
91
|
+
assert.strictEqual(config.idTemplate, 'meeting:{id}');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('throws for sources missing tool config', async () => {
|
|
95
|
+
const { getToolConfig } = await import('../../service/poller.js');
|
|
96
|
+
|
|
97
|
+
const source = { name: 'bad-source' };
|
|
98
|
+
|
|
99
|
+
assert.throws(() => getToolConfig(source), /missing tool configuration/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('throws for mcp sources missing name', async () => {
|
|
103
|
+
const { getToolConfig } = await import('../../service/poller.js');
|
|
104
|
+
|
|
105
|
+
const source = {
|
|
106
|
+
name: 'bad-source',
|
|
107
|
+
tool: { mcp: 'github' } // missing name
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
assert.throws(() => getToolConfig(source), /missing tool configuration/);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
60
114
|
describe('createPoller', () => {
|
|
61
115
|
test('creates poller with state tracking', async () => {
|
|
62
116
|
const { createPoller } = await import('../../service/poller.js');
|