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 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
@@ -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-pilot/config.yaml");
43
- const PILOT_TEMPLATES_DIR = join(os.homedir(), ".config/opencode-pilot/templates");
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-pilot/config.yaml");
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-pilot/poll-state.json`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-pilot",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "type": "module",
5
5
  "main": "plugin/index.js",
6
6
  "description": "Automation daemon for OpenCode - polls for work and spawns sessions",
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-pilot', 'config.yaml')
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 || !source.tool.mcp || !source.tool.name) {
75
- throw new Error(`Source '${source.name || 'unknown'}' missing tool configuration (requires tool.mcp and tool.name)`);
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
- * Poll a source using MCP tools
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;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * repo-config.js - Configuration management
3
3
  *
4
- * Manages configuration stored in ~/.config/opencode-pilot/config.yaml
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)
@@ -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');