opencode-pilot 0.1.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.
Files changed (50) hide show
  1. package/.devcontainer/devcontainer.json +16 -0
  2. package/.github/workflows/ci.yml +67 -0
  3. package/.releaserc.cjs +28 -0
  4. package/AGENTS.md +71 -0
  5. package/CONTRIBUTING.md +102 -0
  6. package/LICENSE +21 -0
  7. package/README.md +72 -0
  8. package/bin/opencode-pilot +809 -0
  9. package/dist/opencode-ntfy.tar.gz +0 -0
  10. package/examples/config.yaml +73 -0
  11. package/examples/templates/default.md +7 -0
  12. package/examples/templates/devcontainer.md +7 -0
  13. package/examples/templates/review-feedback.md +7 -0
  14. package/examples/templates/review.md +15 -0
  15. package/install.sh +246 -0
  16. package/package.json +40 -0
  17. package/plugin/config.js +76 -0
  18. package/plugin/index.js +260 -0
  19. package/plugin/logger.js +125 -0
  20. package/plugin/notifier.js +110 -0
  21. package/service/actions.js +334 -0
  22. package/service/io.opencode.ntfy.plist +29 -0
  23. package/service/logger.js +82 -0
  24. package/service/poll-service.js +246 -0
  25. package/service/poller.js +339 -0
  26. package/service/readiness.js +234 -0
  27. package/service/repo-config.js +222 -0
  28. package/service/server.js +1523 -0
  29. package/service/utils.js +21 -0
  30. package/test/run_tests.bash +34 -0
  31. package/test/test_actions.bash +263 -0
  32. package/test/test_cli.bash +161 -0
  33. package/test/test_config.bash +438 -0
  34. package/test/test_helper.bash +140 -0
  35. package/test/test_logger.bash +401 -0
  36. package/test/test_notifier.bash +310 -0
  37. package/test/test_plist.bash +125 -0
  38. package/test/test_plugin.bash +952 -0
  39. package/test/test_poll_service.bash +179 -0
  40. package/test/test_poller.bash +120 -0
  41. package/test/test_readiness.bash +313 -0
  42. package/test/test_repo_config.bash +406 -0
  43. package/test/test_service.bash +1342 -0
  44. package/test/unit/actions.test.js +235 -0
  45. package/test/unit/config.test.js +86 -0
  46. package/test/unit/paths.test.js +77 -0
  47. package/test/unit/poll-service.test.js +142 -0
  48. package/test/unit/poller.test.js +347 -0
  49. package/test/unit/repo-config.test.js +441 -0
  50. package/test/unit/utils.test.js +53 -0
@@ -0,0 +1,809 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * opencode-pilot - CLI for opencode-pilot automation
4
+ *
5
+ * Commands:
6
+ * setup Copy plugin files and configure OpenCode
7
+ * status Show installation and service status
8
+ * help Show help
9
+ */
10
+
11
+ import { fileURLToPath } from "url";
12
+ import { dirname, join } from "path";
13
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, rmSync } from "fs";
14
+ import { execSync } from "child_process";
15
+ import os from "os";
16
+
17
+ // Get script directory for relative imports
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = dirname(__filename);
20
+
21
+ // Find service directory - check multiple locations for homebrew compatibility
22
+ function findServiceDir() {
23
+ const candidates = [
24
+ join(__dirname, "..", "service"), // Development: bin/../service
25
+ join(__dirname, "..", "libexec"), // Homebrew: bin/../libexec
26
+ ];
27
+ for (const dir of candidates) {
28
+ if (existsSync(join(dir, "server.js"))) {
29
+ return dir;
30
+ }
31
+ }
32
+ return candidates[0];
33
+ }
34
+
35
+ // Find plugin directory
36
+ function findPluginDir() {
37
+ const candidates = [
38
+ join(__dirname, "..", "plugin"), // Development
39
+ join(__dirname, "..", "lib", "opencode-pilot", "plugin"), // Homebrew
40
+ ];
41
+ for (const dir of candidates) {
42
+ if (existsSync(join(dir, "index.js"))) {
43
+ return dir;
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+
49
+ const serviceDir = findServiceDir();
50
+
51
+ // Paths
52
+ const OPENCODE_CONFIG_DIR = join(os.homedir(), ".config/opencode");
53
+ const PLUGIN_NAME = "opencode-pilot";
54
+ const PLUGIN_DEST = join(OPENCODE_CONFIG_DIR, "plugins", PLUGIN_NAME);
55
+ const OPENCODE_CONFIG_FILE = join(OPENCODE_CONFIG_DIR, "opencode.json");
56
+ const PILOT_CONFIG_FILE = join(os.homedir(), ".config/opencode-pilot/config.yaml");
57
+ const PILOT_TEMPLATES_DIR = join(os.homedir(), ".config/opencode-pilot/templates");
58
+
59
+ // Parse command line arguments
60
+ function parseArgs(args) {
61
+ const result = {
62
+ command: null,
63
+ subcommand: null,
64
+ flags: {},
65
+ positional: [],
66
+ };
67
+
68
+ let i = 0;
69
+ while (i < args.length) {
70
+ const arg = args[i];
71
+
72
+ if (arg.startsWith("--")) {
73
+ const key = arg.slice(2);
74
+ if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
75
+ result.flags[key] = args[i + 1];
76
+ i += 2;
77
+ } else {
78
+ result.flags[key] = true;
79
+ i += 1;
80
+ }
81
+ } else if (arg.startsWith("-")) {
82
+ const key = arg.slice(1);
83
+ result.flags[key] = true;
84
+ i += 1;
85
+ } else if (!result.command) {
86
+ result.command = arg;
87
+ i += 1;
88
+ } else if (!result.subcommand) {
89
+ result.subcommand = arg;
90
+ i += 1;
91
+ } else {
92
+ result.positional.push(arg);
93
+ i += 1;
94
+ }
95
+ }
96
+
97
+ return result;
98
+ }
99
+
100
+ // Help text
101
+ function showHelp() {
102
+ console.log(`opencode-pilot - Automation layer for OpenCode
103
+
104
+ Usage:
105
+ opencode-pilot <command> [options]
106
+
107
+ Commands:
108
+ start Start the callback service (foreground)
109
+ setup Copy plugin files and configure OpenCode
110
+ status Show installation and service status
111
+ config Validate and show configuration
112
+ test-source NAME Test a source by fetching items and showing mappings
113
+ test-mapping MCP Test field mappings with sample JSON input
114
+ help Show this help message
115
+
116
+ The service handles:
117
+ - Notification callbacks from ntfy
118
+ - Polling for GitHub/Linear issues to work on
119
+
120
+ Examples:
121
+ opencode-pilot start # Start service (foreground)
122
+ opencode-pilot setup # Initial setup
123
+ opencode-pilot status # Check status
124
+ opencode-pilot config # Validate and show config
125
+ opencode-pilot test-source my-issues # Test a source
126
+ echo '{"url":"https://linear.app/team/issue/PROJ-123/title"}' | opencode-pilot test-mapping linear
127
+ `);
128
+ }
129
+
130
+ // ============================================================================
131
+ // Start Command
132
+ // ============================================================================
133
+
134
+ async function startCommand() {
135
+ // Import and run the service directly
136
+ const serverPath = join(serviceDir, "server.js");
137
+ if (!existsSync(serverPath)) {
138
+ console.error("ERROR: Could not find server.js");
139
+ console.error(`Expected at: ${serverPath}`);
140
+ process.exit(1);
141
+ }
142
+
143
+ console.log("[opencode-pilot] Starting callback service...");
144
+
145
+ // Dynamic import of the service module
146
+ const { startService, stopService } = await import(serverPath);
147
+
148
+ const config = {
149
+ httpPort: parseInt(process.env.NTFY_CALLBACK_PORT || "4097", 10),
150
+ socketPath: process.env.NTFY_SOCKET_PATH || "/tmp/opencode-pilot.sock",
151
+ };
152
+
153
+ const service = await startService(config);
154
+
155
+ // Handle graceful shutdown
156
+ process.on("SIGTERM", async () => {
157
+ console.log("[opencode-pilot] Received SIGTERM, shutting down...");
158
+ await stopService(service);
159
+ process.exit(0);
160
+ });
161
+
162
+ process.on("SIGINT", async () => {
163
+ console.log("[opencode-pilot] Received SIGINT, shutting down...");
164
+ await stopService(service);
165
+ process.exit(0);
166
+ });
167
+
168
+ // Keep running until signal received
169
+ await new Promise(() => {});
170
+ }
171
+
172
+ // ============================================================================
173
+ // Setup Command
174
+ // ============================================================================
175
+
176
+ async function setupCommand() {
177
+ console.log("Setting up opencode-pilot...");
178
+ console.log("");
179
+
180
+ const pluginSource = findPluginDir();
181
+ if (!pluginSource) {
182
+ console.error("ERROR: Could not find plugin source files");
183
+ console.error("Install via: brew install athal7/tap/opencode-pilot");
184
+ process.exit(1);
185
+ }
186
+
187
+ console.log(`Source: ${pluginSource}`);
188
+ console.log(`Destination: ${PLUGIN_DEST}`);
189
+ console.log("");
190
+
191
+ // Backup existing config
192
+ if (existsSync(OPENCODE_CONFIG_FILE)) {
193
+ const backupFile = `${OPENCODE_CONFIG_FILE}.backup.${Date.now()}`;
194
+ try {
195
+ cpSync(OPENCODE_CONFIG_FILE, backupFile);
196
+ console.log(`Backup created: ${backupFile}`);
197
+ } catch (err) {
198
+ console.error("ERROR: Failed to create backup of opencode.json");
199
+ process.exit(1);
200
+ }
201
+ }
202
+
203
+ // Copy plugin files
204
+ mkdirSync(join(OPENCODE_CONFIG_DIR, "plugins"), { recursive: true });
205
+ if (existsSync(PLUGIN_DEST)) {
206
+ rmSync(PLUGIN_DEST, { recursive: true });
207
+ }
208
+ cpSync(pluginSource, PLUGIN_DEST, { recursive: true });
209
+ console.log("Plugin files copied.");
210
+
211
+ // Update opencode.json
212
+ let config = {};
213
+ if (existsSync(OPENCODE_CONFIG_FILE)) {
214
+ try {
215
+ config = JSON.parse(readFileSync(OPENCODE_CONFIG_FILE, "utf8"));
216
+ } catch {
217
+ config = {};
218
+ }
219
+ }
220
+
221
+ config.plugin = config.plugin || [];
222
+ const alreadyRegistered = config.plugin.some(p => p.includes(`plugins/${PLUGIN_NAME}`));
223
+
224
+ if (alreadyRegistered) {
225
+ console.log("Plugin already in opencode.json");
226
+ } else {
227
+ config.plugin.push(PLUGIN_DEST);
228
+ const tempFile = `${OPENCODE_CONFIG_FILE}.tmp`;
229
+ writeFileSync(tempFile, JSON.stringify(config, null, 2) + "\n");
230
+ const { renameSync } = await import("fs");
231
+ renameSync(tempFile, OPENCODE_CONFIG_FILE);
232
+ console.log("Added plugin to opencode.json");
233
+ }
234
+
235
+ // Try to restart service
236
+ try {
237
+ execSync("brew services restart opencode-pilot", { stdio: "pipe" });
238
+ console.log("");
239
+ console.log("Service restarted.");
240
+ } catch {
241
+ // Not installed via brew or service not available
242
+ }
243
+
244
+ console.log("");
245
+ console.log("Setup complete!");
246
+ console.log("");
247
+ console.log("Next steps:");
248
+ console.log(" 1. Edit ~/.config/opencode-pilot/config.yaml");
249
+ console.log(" 2. Run: opencode-pilot start");
250
+ }
251
+
252
+ // ============================================================================
253
+ // Status Command
254
+ // ============================================================================
255
+
256
+ function getConfigValue(envVar, configKey, defaultValue = "") {
257
+ // Check env var first
258
+ if (process.env[envVar]) {
259
+ return process.env[envVar];
260
+ }
261
+
262
+ // Check config file
263
+ if (existsSync(PILOT_CONFIG_FILE)) {
264
+ try {
265
+ const config = JSON.parse(readFileSync(PILOT_CONFIG_FILE, "utf8"));
266
+ if (config[configKey] !== undefined && config[configKey] !== "") {
267
+ return config[configKey];
268
+ }
269
+ } catch {
270
+ // Ignore
271
+ }
272
+ }
273
+
274
+ return defaultValue;
275
+ }
276
+
277
+ function statusCommand() {
278
+ console.log("opencode-pilot status");
279
+ console.log("=====================");
280
+ console.log("");
281
+
282
+ // Plugin installed?
283
+ if (existsSync(PLUGIN_DEST)) {
284
+ console.log(`Plugin: installed at ${PLUGIN_DEST}`);
285
+ } else {
286
+ console.log("Plugin: NOT INSTALLED (run: opencode-pilot setup)");
287
+ }
288
+
289
+ // Config?
290
+ if (existsSync(OPENCODE_CONFIG_FILE)) {
291
+ try {
292
+ const config = JSON.parse(readFileSync(OPENCODE_CONFIG_FILE, "utf8"));
293
+ const registered = (config.plugin || []).some(p => p.includes(`plugins/${PLUGIN_NAME}`));
294
+ if (registered) {
295
+ console.log("Config: plugin registered in opencode.json");
296
+ } else {
297
+ console.log("Config: plugin NOT in opencode.json");
298
+ }
299
+ } catch {
300
+ console.log("Config: could not parse opencode.json");
301
+ }
302
+ } else {
303
+ console.log("Config: opencode.json not found");
304
+ }
305
+
306
+ // Service running? Check if socket exists and HTTP responds
307
+ const socketPath = "/tmp/opencode-pilot.sock";
308
+ const servicePort = getConfigValue("NTFY_CALLBACK_PORT", "callbackPort", "4097");
309
+
310
+ if (existsSync(socketPath)) {
311
+ // Socket exists, try health check
312
+ try {
313
+ const res = execSync(`curl -s -o /dev/null -w "%{http_code}" http://localhost:${servicePort}/health`, { encoding: "utf8", timeout: 2000 });
314
+ if (res.trim() === "200") {
315
+ console.log("Service: running");
316
+ } else {
317
+ console.log("Service: socket exists but not responding (run: opencode-pilot start)");
318
+ }
319
+ } catch {
320
+ console.log("Service: socket exists but health check failed");
321
+ }
322
+ } else {
323
+ console.log("Service: not running (run: opencode-pilot start)");
324
+ }
325
+
326
+ // Notification configuration
327
+ console.log("");
328
+ console.log("Notification Configuration:");
329
+
330
+ const topic = getConfigValue("NTFY_TOPIC", "topic", "");
331
+ const server = getConfigValue("NTFY_SERVER", "server", "https://ntfy.sh");
332
+ const callbackHost = getConfigValue("NTFY_CALLBACK_HOST", "callbackHost", "");
333
+ const callbackPort = getConfigValue("NTFY_CALLBACK_PORT", "callbackPort", "4097");
334
+
335
+ console.log(` topic: ${topic || "<not set>"}`);
336
+ console.log(` server: ${server}`);
337
+ console.log(` callbackHost: ${callbackHost || "<not set>"}`);
338
+ console.log(` callbackPort: ${callbackPort}`);
339
+
340
+ // Polling configuration
341
+ console.log("");
342
+ console.log("Polling Configuration:");
343
+ if (existsSync(PILOT_CONFIG_FILE)) {
344
+ console.log(` config.yaml: ${PILOT_CONFIG_FILE}`);
345
+ console.log(" polling: enabled (handled by service)");
346
+ } else {
347
+ console.log(` config.yaml: not found at ${PILOT_CONFIG_FILE}`);
348
+ console.log(" polling: disabled");
349
+ }
350
+ }
351
+
352
+ // ============================================================================
353
+ // Config Command
354
+ // ============================================================================
355
+
356
+ async function configCommand() {
357
+ console.log("opencode-pilot configuration");
358
+ console.log("============================");
359
+ console.log("");
360
+
361
+ // Check config file exists
362
+ if (!existsSync(PILOT_CONFIG_FILE)) {
363
+ console.log(`Config file not found: ${PILOT_CONFIG_FILE}`);
364
+ console.log("");
365
+ console.log("Create one by copying the example:");
366
+ console.log(" cp node_modules/opencode-pilot/examples/config.yaml ~/.config/opencode-pilot/config.yaml");
367
+ process.exit(1);
368
+ }
369
+
370
+ // Load and parse config
371
+ let config;
372
+ try {
373
+ const { default: YAML } = await import("yaml");
374
+ const content = readFileSync(PILOT_CONFIG_FILE, "utf8");
375
+ config = YAML.parse(content);
376
+ console.log("✓ Config file parsed successfully");
377
+ } catch (err) {
378
+ console.log(`✗ Failed to parse config: ${err.message}`);
379
+ process.exit(1);
380
+ }
381
+
382
+ // Validate notifications section
383
+ console.log("");
384
+ console.log("Notifications:");
385
+ const notifications = config.notifications || {};
386
+ if (notifications.topic) {
387
+ console.log(` ✓ topic: ${notifications.topic.substring(0, 8)}...`);
388
+ } else {
389
+ console.log(" ✗ topic: not set (required for notifications)");
390
+ }
391
+ console.log(` server: ${notifications.server || "https://ntfy.sh"}`);
392
+ if (notifications.callback_host) {
393
+ console.log(` ✓ callback_host: ${notifications.callback_host}`);
394
+ } else {
395
+ console.log(" ⚠ callback_host: not set (interactive notifications disabled)");
396
+ }
397
+
398
+ // Validate defaults section
399
+ console.log("");
400
+ console.log("Defaults:");
401
+ const defaults = config.defaults || {};
402
+ console.log(` working_dir: ${defaults.working_dir || "~"}`);
403
+ console.log(` prompt: ${defaults.prompt || "default"}`);
404
+ if (defaults.agent) console.log(` agent: ${defaults.agent}`);
405
+ if (defaults.model) console.log(` model: ${defaults.model}`);
406
+
407
+ // Validate repos section
408
+ console.log("");
409
+ console.log("Repos:");
410
+ const repos = config.repos || {};
411
+ const repoKeys = Object.keys(repos);
412
+ if (repoKeys.length === 0) {
413
+ console.log(" (none configured)");
414
+ } else {
415
+ for (const key of repoKeys) {
416
+ const repo = repos[key];
417
+ const isPrefix = key.endsWith("/");
418
+ const path = repo.path || repo.repo_path;
419
+ const expandedPath = path ? path.replace("~", os.homedir()).replace("{repo}", "<repo>") : "<not set>";
420
+ const pathExists = path && !path.includes("{repo}") ? existsSync(path.replace("~", os.homedir())) : null;
421
+
422
+ if (pathExists === false) {
423
+ console.log(` ✗ ${key}: ${expandedPath} (path not found)`);
424
+ } else if (isPrefix) {
425
+ console.log(` ${key}: ${expandedPath} (prefix)`);
426
+ } else {
427
+ console.log(` ✓ ${key}: ${expandedPath}`);
428
+ }
429
+ }
430
+ }
431
+
432
+ // Validate sources section
433
+ console.log("");
434
+ console.log("Sources:");
435
+ const sources = config.sources || [];
436
+ if (sources.length === 0) {
437
+ console.log(" (none configured)");
438
+ } else {
439
+ for (const source of sources) {
440
+ const name = source.name || "<unnamed>";
441
+ const tool = source.tool;
442
+
443
+ if (!tool || !tool.mcp || !tool.name) {
444
+ console.log(` ✗ ${name}: missing tool.mcp or tool.name`);
445
+ continue;
446
+ }
447
+
448
+ const itemId = source.item?.id;
449
+ if (!itemId) {
450
+ console.log(` ⚠ ${name}: ${tool.mcp}/${tool.name} (no item.id template)`);
451
+ } else {
452
+ console.log(` ✓ ${name}: ${tool.mcp}/${tool.name}`);
453
+ }
454
+
455
+ // Show repo resolution
456
+ if (source.repo) {
457
+ console.log(` repo: ${source.repo}`);
458
+ } else if (source.repos) {
459
+ console.log(` repos: ${Array.isArray(source.repos) ? source.repos.join(", ") : JSON.stringify(source.repos)}`);
460
+ } else if (source.working_dir) {
461
+ console.log(` working_dir: ${source.working_dir}`);
462
+ } else {
463
+ console.log(` (uses defaults.working_dir)`);
464
+ }
465
+ }
466
+ }
467
+
468
+ // Validate templates
469
+ console.log("");
470
+ console.log("Templates:");
471
+ if (!existsSync(PILOT_TEMPLATES_DIR)) {
472
+ console.log(` ⚠ Templates directory not found: ${PILOT_TEMPLATES_DIR}`);
473
+ } else {
474
+ const { readdirSync } = await import("fs");
475
+ const templates = readdirSync(PILOT_TEMPLATES_DIR).filter(f => f.endsWith(".md"));
476
+ if (templates.length === 0) {
477
+ console.log(" (no templates found)");
478
+ } else {
479
+ for (const t of templates) {
480
+ console.log(` ✓ ${t.replace(".md", "")}`);
481
+ }
482
+ }
483
+
484
+ // Check if referenced templates exist
485
+ const referencedTemplates = new Set();
486
+ if (defaults.prompt) referencedTemplates.add(defaults.prompt);
487
+ for (const repo of Object.values(repos)) {
488
+ if (repo.prompt) referencedTemplates.add(repo.prompt);
489
+ }
490
+ for (const source of sources) {
491
+ if (source.prompt) referencedTemplates.add(source.prompt);
492
+ }
493
+
494
+ const existingTemplates = new Set(templates.map(t => t.replace(".md", "")));
495
+ for (const ref of referencedTemplates) {
496
+ if (!existingTemplates.has(ref)) {
497
+ console.log(` ✗ ${ref} (referenced but not found)`);
498
+ }
499
+ }
500
+ }
501
+
502
+ console.log("");
503
+ console.log("Configuration valid!");
504
+ }
505
+
506
+ // ============================================================================
507
+ // Test Source Command
508
+ // ============================================================================
509
+
510
+ async function testSourceCommand(sourceName) {
511
+ if (!sourceName) {
512
+ console.error("Usage: opencode-pilot test-source <source-name>");
513
+ console.error("");
514
+ console.error("Available sources:");
515
+
516
+ // Load and show available sources
517
+ if (existsSync(PILOT_CONFIG_FILE)) {
518
+ try {
519
+ const { default: YAML } = await import("yaml");
520
+ const content = readFileSync(PILOT_CONFIG_FILE, "utf8");
521
+ const config = YAML.parse(content);
522
+ const sources = config.sources || [];
523
+ if (sources.length === 0) {
524
+ console.error(" (no sources configured)");
525
+ } else {
526
+ for (const s of sources) {
527
+ console.error(` ${s.name}`);
528
+ }
529
+ }
530
+ } catch (err) {
531
+ console.error(` Error loading config: ${err.message}`);
532
+ }
533
+ } else {
534
+ console.error(" (config file not found)");
535
+ }
536
+ process.exit(1);
537
+ }
538
+
539
+ console.log(`Testing source: ${sourceName}`);
540
+ console.log("=".repeat(40));
541
+ console.log("");
542
+
543
+ // Load config
544
+ if (!existsSync(PILOT_CONFIG_FILE)) {
545
+ console.error(`Config file not found: ${PILOT_CONFIG_FILE}`);
546
+ process.exit(1);
547
+ }
548
+
549
+ const { default: YAML } = await import("yaml");
550
+ const content = readFileSync(PILOT_CONFIG_FILE, "utf8");
551
+ const config = YAML.parse(content);
552
+
553
+ // Find source
554
+ const sources = config.sources || [];
555
+ const source = sources.find(s => s.name === sourceName);
556
+
557
+ if (!source) {
558
+ console.error(`Source not found: ${sourceName}`);
559
+ console.error("");
560
+ console.error("Available sources:");
561
+ for (const s of sources) {
562
+ console.error(` ${s.name}`);
563
+ }
564
+ process.exit(1);
565
+ }
566
+
567
+ // Show source config
568
+ console.log("Source Configuration:");
569
+ console.log(` name: ${source.name}`);
570
+ console.log(` tool.mcp: ${source.tool?.mcp || "(not set)"}`);
571
+ console.log(` tool.name: ${source.tool?.name || "(not set)"}`);
572
+ console.log(` args: ${JSON.stringify(source.args || {})}`);
573
+ console.log(` item.id: ${source.item?.id || "(not set)"}`);
574
+ if (source.repo) console.log(` repo: ${source.repo}`);
575
+ if (source.working_dir) console.log(` working_dir: ${source.working_dir}`);
576
+ if (source.prompt) console.log(` prompt: ${source.prompt}`);
577
+ if (source.agent) console.log(` agent: ${source.agent}`);
578
+
579
+ // Show mappings
580
+ const tools = config.tools || {};
581
+ const provider = source.tool?.mcp;
582
+ const mappings = tools[provider]?.mappings;
583
+
584
+ console.log("");
585
+ console.log("Field Mappings:");
586
+ if (mappings) {
587
+ for (const [target, sourcePath] of Object.entries(mappings)) {
588
+ console.log(` ${target} ← ${sourcePath}`);
589
+ }
590
+ } else {
591
+ console.log(` (no mappings configured for provider '${provider}')`);
592
+ }
593
+
594
+ // Fetch items
595
+ console.log("");
596
+ console.log("Fetching items...");
597
+
598
+ try {
599
+ // Import poller
600
+ const pollerPath = join(serviceDir, "poller.js");
601
+ const { pollGenericSource, applyMappings } = await import(pollerPath);
602
+ const { getToolMappings } = await import(join(serviceDir, "repo-config.js"));
603
+
604
+ // Fetch items with mappings applied
605
+ const items = await pollGenericSource(source, { mappings });
606
+
607
+ console.log(`Found ${items.length} item(s)`);
608
+ console.log("");
609
+
610
+ // Show first few items
611
+ const maxItems = 3;
612
+ for (let i = 0; i < Math.min(items.length, maxItems); i++) {
613
+ const item = items[i];
614
+ console.log(`Item ${i + 1}:`);
615
+ console.log(" Raw fields:");
616
+ for (const [key, value] of Object.entries(item)) {
617
+ const displayValue = typeof value === "object"
618
+ ? JSON.stringify(value)
619
+ : String(value);
620
+ const truncated = displayValue.length > 60
621
+ ? displayValue.substring(0, 57) + "..."
622
+ : displayValue;
623
+ console.log(` ${key}: ${truncated}`);
624
+ }
625
+
626
+ // Apply mappings
627
+ if (mappings) {
628
+ const mapped = applyMappings(item, mappings);
629
+ console.log(" After mappings:");
630
+ // Show mapped fields that are different
631
+ for (const [target] of Object.entries(mappings)) {
632
+ if (mapped[target] !== undefined) {
633
+ const displayValue = typeof mapped[target] === "object"
634
+ ? JSON.stringify(mapped[target])
635
+ : String(mapped[target]);
636
+ const truncated = displayValue.length > 60
637
+ ? displayValue.substring(0, 57) + "..."
638
+ : displayValue;
639
+ console.log(` ${target}: ${truncated}`);
640
+ }
641
+ }
642
+ }
643
+ console.log("");
644
+ }
645
+
646
+ if (items.length > maxItems) {
647
+ console.log(`... and ${items.length - maxItems} more item(s)`);
648
+ }
649
+
650
+ } catch (err) {
651
+ console.error(`Error fetching items: ${err.message}`);
652
+ if (err.stack) {
653
+ console.error("");
654
+ console.error("Stack trace:");
655
+ console.error(err.stack);
656
+ }
657
+ process.exit(1);
658
+ }
659
+ }
660
+
661
+ // ============================================================================
662
+ // Test Mapping Command
663
+ // ============================================================================
664
+
665
+ async function testMappingCommand(mcpName) {
666
+ if (!mcpName) {
667
+ console.error("Usage: echo '<json>' | opencode-pilot test-mapping <mcp-name>");
668
+ console.error("");
669
+ console.error("Test field mappings by piping JSON and specifying the MCP server name.");
670
+ console.error("");
671
+ console.error("Example:");
672
+ console.error(' echo \'{"url":"https://linear.app/team/issue/PROJ-123/title","title":"Fix bug"}\' | opencode-pilot test-mapping linear');
673
+ console.error("");
674
+
675
+ // Show available tools with mappings
676
+ const { loadRepoConfig, getToolMappings } = await import(join(serviceDir, "repo-config.js"));
677
+ loadRepoConfig(PILOT_CONFIG_FILE);
678
+
679
+ console.error("Configured MCP servers with mappings:");
680
+ for (const name of ["github", "linear"]) {
681
+ const mappings = getToolMappings(name);
682
+ if (mappings && Object.keys(mappings).length > 0) {
683
+ console.error(` ${name}:`);
684
+ for (const [target, source] of Object.entries(mappings)) {
685
+ console.error(` ${target} ← ${source}`);
686
+ }
687
+ }
688
+ }
689
+ process.exit(1);
690
+ }
691
+
692
+ // Read JSON from stdin
693
+ const chunks = [];
694
+ for await (const chunk of process.stdin) {
695
+ chunks.push(chunk);
696
+ }
697
+ const input = Buffer.concat(chunks).toString().trim();
698
+
699
+ if (!input) {
700
+ console.error("Error: No JSON input provided. Pipe JSON to stdin.");
701
+ console.error("");
702
+ console.error("Example:");
703
+ console.error(' echo \'{"url":"https://linear.app/team/issue/PROJ-123/title"}\' | opencode-pilot test-mapping linear');
704
+ process.exit(1);
705
+ }
706
+
707
+ let item;
708
+ try {
709
+ item = JSON.parse(input);
710
+ } catch (err) {
711
+ console.error(`Error parsing JSON: ${err.message}`);
712
+ process.exit(1);
713
+ }
714
+
715
+ // Load mappings
716
+ const { loadRepoConfig, getToolMappings } = await import(join(serviceDir, "repo-config.js"));
717
+ const { applyMappings } = await import(join(serviceDir, "poller.js"));
718
+
719
+ loadRepoConfig(PILOT_CONFIG_FILE);
720
+ const mappings = getToolMappings(mcpName);
721
+
722
+ if (!mappings || Object.keys(mappings).length === 0) {
723
+ console.error(`No mappings configured for MCP server: ${mcpName}`);
724
+ console.error("");
725
+ console.error("Add mappings to your config.yaml:");
726
+ console.error(" tools:");
727
+ console.error(` ${mcpName}:`);
728
+ console.error(" mappings:");
729
+ console.error(' number: "url:/([A-Z0-9]+-[0-9]+)/"');
730
+ process.exit(1);
731
+ }
732
+
733
+ console.log("Input:");
734
+ for (const [key, value] of Object.entries(item)) {
735
+ const displayValue = typeof value === "object" ? JSON.stringify(value) : String(value);
736
+ const truncated = displayValue.length > 60 ? displayValue.substring(0, 57) + "..." : displayValue;
737
+ console.log(` ${key}: ${truncated}`);
738
+ }
739
+ console.log("");
740
+
741
+ console.log(`Mappings (${mcpName}):`);
742
+ for (const [target, source] of Object.entries(mappings)) {
743
+ console.log(` ${target} ← ${source}`);
744
+ }
745
+ console.log("");
746
+
747
+ const mapped = applyMappings(item, mappings);
748
+
749
+ console.log("Result:");
750
+ for (const [target] of Object.entries(mappings)) {
751
+ const value = mapped[target];
752
+ if (value !== undefined) {
753
+ console.log(` ${target}: ${value}`);
754
+ } else {
755
+ console.log(` ${target}: (undefined - no match)`);
756
+ }
757
+ }
758
+ }
759
+
760
+ // ============================================================================
761
+ // Main
762
+ // ============================================================================
763
+
764
+ async function main() {
765
+ const args = process.argv.slice(2);
766
+ const { command, subcommand, flags } = parseArgs(args);
767
+
768
+ if (!command || command === "help") {
769
+ showHelp();
770
+ return;
771
+ }
772
+
773
+ switch (command) {
774
+ case "start":
775
+ await startCommand();
776
+ break;
777
+
778
+ case "setup":
779
+ await setupCommand();
780
+ break;
781
+
782
+ case "status":
783
+ statusCommand();
784
+ break;
785
+
786
+ case "config":
787
+ await configCommand();
788
+ break;
789
+
790
+ case "test-source":
791
+ await testSourceCommand(subcommand);
792
+ break;
793
+
794
+ case "test-mapping":
795
+ await testMappingCommand(subcommand);
796
+ break;
797
+
798
+ default:
799
+ console.error(`Unknown command: ${command}`);
800
+ console.error("");
801
+ showHelp();
802
+ process.exit(1);
803
+ }
804
+ }
805
+
806
+ main().catch((err) => {
807
+ console.error("Error:", err.message);
808
+ process.exit(1);
809
+ });