opencode-pilot 0.1.0 → 0.2.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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Tests for consistent path naming across the codebase.
3
3
  *
4
- * These tests ensure all config/socket paths use "opencode-pilot"
4
+ * These tests ensure all config paths use "opencode-pilot"
5
5
  * and not the old "opencode-ntfy" name.
6
6
  */
7
7
 
@@ -14,64 +14,22 @@ import { fileURLToPath } from 'url';
14
14
  const __filename = fileURLToPath(import.meta.url);
15
15
  const __dirname = dirname(__filename);
16
16
  const ROOT_DIR = join(__dirname, '..', '..');
17
- const PLUGIN_DIR = join(ROOT_DIR, 'plugin');
18
17
  const SERVICE_DIR = join(ROOT_DIR, 'service');
19
18
 
20
19
  describe('Path naming consistency', () => {
21
20
 
22
- describe('config.js', () => {
23
- const configPath = join(PLUGIN_DIR, 'config.js');
24
- const content = readFileSync(configPath, 'utf8');
25
-
26
- test('uses opencode-pilot config path', () => {
27
- assert.match(content, /opencode-pilot.*config\.yaml/,
28
- 'config.js should reference opencode-pilot config path');
29
- });
30
-
31
- test('does not reference old opencode-ntfy path', () => {
32
- assert.doesNotMatch(content, /opencode-ntfy/,
33
- 'config.js should not reference old opencode-ntfy name');
34
- });
35
- });
36
-
37
- describe('logger.js', () => {
38
- const loggerPath = join(PLUGIN_DIR, 'logger.js');
39
- const content = readFileSync(loggerPath, 'utf8');
40
-
41
- test('uses opencode-pilot debug log path', () => {
42
- assert.match(content, /opencode-pilot.*debug\.log/,
43
- 'logger.js should reference opencode-pilot debug log path');
44
- });
45
-
46
- test('does not reference old opencode-ntfy path', () => {
47
- assert.doesNotMatch(content, /opencode-ntfy/,
48
- 'logger.js should not reference old opencode-ntfy name');
49
- });
50
- });
51
-
52
21
  describe('server.js', () => {
53
22
  const serverPath = join(SERVICE_DIR, 'server.js');
54
23
  const content = readFileSync(serverPath, 'utf8');
55
24
 
56
- test('uses opencode-pilot socket path', () => {
57
- assert.match(content, /opencode-pilot\.sock/,
58
- 'server.js should reference opencode-pilot socket path');
59
- });
60
-
61
25
  test('uses opencode-pilot config path', () => {
62
- assert.match(content, /opencode-pilot.*config\.json|opencode-pilot/,
26
+ assert.match(content, /opencode-pilot.*config\.yaml|opencode-pilot/,
63
27
  'server.js should reference opencode-pilot paths');
64
28
  });
65
- });
66
-
67
- describe('index.js (plugin entry)', () => {
68
- const indexPath = join(PLUGIN_DIR, 'index.js');
69
- const content = readFileSync(indexPath, 'utf8');
70
29
 
71
- test('does not reference old opencode-ntfy name in comments', () => {
72
- // Allow "ntfy" alone (the service name) but not "opencode-ntfy"
30
+ test('does not reference old opencode-ntfy name', () => {
73
31
  assert.doesNotMatch(content, /opencode-ntfy/,
74
- 'index.js should not reference old opencode-ntfy name');
32
+ 'server.js should not reference old opencode-ntfy name');
75
33
  });
76
34
  });
77
35
  });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Tests for plugin/index.js - Auto-start plugin for OpenCode
3
+ */
4
+
5
+ import { describe, it, mock } from "node:test";
6
+ import assert from "node:assert";
7
+
8
+ describe("plugin/index.js", () => {
9
+ describe("exports", () => {
10
+ it("exports PilotPlugin as named export", async () => {
11
+ const plugin = await import("../../plugin/index.js");
12
+ assert.strictEqual(typeof plugin.PilotPlugin, "function");
13
+ });
14
+
15
+ it("exports PilotPlugin as default export", async () => {
16
+ const plugin = await import("../../plugin/index.js");
17
+ assert.strictEqual(plugin.default, plugin.PilotPlugin);
18
+ });
19
+ });
20
+
21
+ describe("PilotPlugin", () => {
22
+ it("returns empty hooks object", async () => {
23
+ const plugin = await import("../../plugin/index.js");
24
+
25
+ // Mock context with $ shell function
26
+ const mockShell = mock.fn(() => Promise.resolve());
27
+ mockShell.quiet = mock.fn(() => Promise.resolve());
28
+ const ctx = { $: mockShell };
29
+
30
+ // Call plugin - it will try to fetch health endpoint which will fail
31
+ // in test environment, so it will try to start daemon
32
+ const hooks = await plugin.PilotPlugin(ctx);
33
+
34
+ // Should return empty hooks object (no event handlers)
35
+ assert.deepStrictEqual(hooks, {});
36
+ });
37
+
38
+ it("is an async function", async () => {
39
+ const plugin = await import("../../plugin/index.js");
40
+ assert.strictEqual(
41
+ plugin.PilotPlugin.constructor.name,
42
+ "AsyncFunction"
43
+ );
44
+ });
45
+ });
46
+ });
Binary file
package/plugin/config.js DELETED
@@ -1,76 +0,0 @@
1
- // Configuration management for opencode-pilot
2
- // Reads from ~/.config/opencode-pilot/config.yaml
3
- //
4
- // Example config file (~/.config/opencode-pilot/config.yaml):
5
- // notifications:
6
- // topic: my-secret-topic
7
- // server: https://ntfy.sh
8
- // idle_delay_ms: 300000
9
- // debug: true
10
-
11
- import { readFileSync, existsSync } from 'fs'
12
- import { join } from 'path'
13
- import { homedir } from 'os'
14
- import YAML from 'yaml'
15
-
16
- const DEFAULT_CONFIG_PATH = join(homedir(), '.config', 'opencode-pilot', 'config.yaml')
17
-
18
- /**
19
- * Load configuration from config file
20
- * @param {string} [configPath] - Optional path to config file (for testing)
21
- */
22
- export function loadConfig(configPath) {
23
- const actualPath = configPath || DEFAULT_CONFIG_PATH
24
-
25
- // Load config.yaml if it exists
26
- let fileConfig = {}
27
- if (existsSync(actualPath)) {
28
- try {
29
- const content = readFileSync(actualPath, 'utf8')
30
- const parsed = YAML.parse(content)
31
- // Extract notifications section
32
- fileConfig = parsed?.notifications || {}
33
- } catch (err) {
34
- // Silently ignore parse errors
35
- }
36
- }
37
-
38
- // Helper to get value with default
39
- const get = (key, defaultValue) => {
40
- if (fileConfig[key] !== undefined && fileConfig[key] !== '') {
41
- return fileConfig[key]
42
- }
43
- return defaultValue
44
- }
45
-
46
- // Helper to parse boolean
47
- const getBool = (key, defaultValue) => {
48
- const value = get(key, undefined)
49
- if (value === undefined) return defaultValue
50
- if (typeof value === 'boolean') return value
51
- return String(value).toLowerCase() !== 'false' && String(value) !== '0'
52
- }
53
-
54
- // Helper to parse int
55
- const getInt = (key, defaultValue) => {
56
- const value = get(key, undefined)
57
- if (value === undefined) return defaultValue
58
- if (typeof value === 'number') return value
59
- const parsed = parseInt(String(value), 10)
60
- return isNaN(parsed) ? defaultValue : parsed
61
- }
62
-
63
- return {
64
- topic: get('topic', null),
65
- server: get('server', 'https://ntfy.sh'),
66
- authToken: get('token', null),
67
- idleDelayMs: getInt('idle_delay_ms', 300000),
68
- errorNotify: getBool('error_notify', true),
69
- errorDebounceMs: getInt('error_debounce_ms', 60000),
70
- retryNotifyFirst: getBool('retry_notify_first', true),
71
- retryNotifyAfter: getInt('retry_notify_after', 3),
72
- idleNotify: getBool('idle_notify', true),
73
- debug: getBool('debug', false),
74
- debugPath: get('debug_path', null),
75
- }
76
- }
package/plugin/logger.js DELETED
@@ -1,125 +0,0 @@
1
- // Debug logging module for opencode-pilot plugin
2
- // Writes to ~/.config/opencode-pilot/debug.log when enabled via NTFY_DEBUG=true or config.debug
3
- //
4
- // Usage:
5
- // import { initLogger, debug } from './logger.js'
6
- // initLogger({ debug: true, debugPath: '/custom/path.log' })
7
- // debug('Event received', { type: 'session.status', status: 'idle' })
8
-
9
- import { appendFileSync, existsSync, mkdirSync, statSync, unlinkSync } from 'fs'
10
- import { join, dirname } from 'path'
11
- import { homedir } from 'os'
12
-
13
- // Maximum log file size before rotation (1MB)
14
- export const MAX_LOG_SIZE = 1024 * 1024
15
-
16
- // Default log path
17
- const DEFAULT_LOG_PATH = join(homedir(), '.config', 'opencode-pilot', 'debug.log')
18
-
19
- // Module state
20
- let enabled = false
21
- let logPath = DEFAULT_LOG_PATH
22
-
23
- /**
24
- * Initialize the logger with configuration
25
- * @param {Object} options
26
- * @param {boolean} [options.debug] - Enable debug logging
27
- * @param {string} [options.debugPath] - Custom log file path
28
- */
29
- export function initLogger(options = {}) {
30
- // Check environment variables first, then options
31
- const envDebug = process.env.NTFY_DEBUG
32
- const envDebugPath = process.env.NTFY_DEBUG_PATH
33
-
34
- // Enable if NTFY_DEBUG is set to any truthy value (not 'false' or '0')
35
- if (envDebug !== undefined && envDebug !== '' && envDebug !== 'false' && envDebug !== '0') {
36
- enabled = true
37
- } else if (options.debug !== undefined) {
38
- enabled = Boolean(options.debug)
39
- } else {
40
- enabled = false
41
- }
42
-
43
- // Set log path (env var takes precedence)
44
- if (envDebugPath) {
45
- logPath = envDebugPath
46
- } else if (options.debugPath) {
47
- logPath = options.debugPath
48
- } else {
49
- logPath = DEFAULT_LOG_PATH
50
- }
51
-
52
- // Create directory if it doesn't exist
53
- if (enabled) {
54
- try {
55
- const dir = dirname(logPath)
56
- if (!existsSync(dir)) {
57
- mkdirSync(dir, { recursive: true })
58
- }
59
- } catch {
60
- // Silently ignore directory creation errors
61
- }
62
- }
63
- }
64
-
65
- /**
66
- * Write a debug log entry
67
- * @param {string} message - Log message
68
- * @param {Object} [data] - Optional data to include
69
- */
70
- export function debug(message, data) {
71
- if (!enabled) {
72
- return
73
- }
74
-
75
- try {
76
- // Check file size and rotate if needed
77
- rotateIfNeeded()
78
-
79
- // Format log entry with ISO 8601 timestamp
80
- const timestamp = new Date().toISOString()
81
- let entry = `[${timestamp}] ${message}`
82
-
83
- // Append data if provided
84
- if (data !== undefined) {
85
- if (typeof data === 'object') {
86
- entry += ' ' + JSON.stringify(data)
87
- } else {
88
- entry += ' ' + String(data)
89
- }
90
- }
91
-
92
- entry += '\n'
93
-
94
- // Ensure directory exists
95
- const dir = dirname(logPath)
96
- if (!existsSync(dir)) {
97
- mkdirSync(dir, { recursive: true })
98
- }
99
-
100
- // Append to log file
101
- appendFileSync(logPath, entry)
102
- } catch {
103
- // Silently ignore write errors to avoid affecting the plugin
104
- }
105
- }
106
-
107
- /**
108
- * Rotate log file if it exceeds MAX_LOG_SIZE
109
- */
110
- function rotateIfNeeded() {
111
- try {
112
- if (!existsSync(logPath)) {
113
- return
114
- }
115
-
116
- const stats = statSync(logPath)
117
- if (stats.size > MAX_LOG_SIZE) {
118
- // Simple rotation: just truncate the file
119
- // For more sophisticated rotation, could rename to .old first
120
- unlinkSync(logPath)
121
- }
122
- } catch {
123
- // Silently ignore rotation errors
124
- }
125
- }
@@ -1,110 +0,0 @@
1
- // ntfy HTTP client for sending notifications
2
-
3
- import { debug } from './logger.js'
4
-
5
- // Deduplication cache: track recently sent notifications to prevent duplicates
6
- // Key: hash of notification content, Value: timestamp
7
- const recentNotifications = new Map()
8
- const DEDUPE_WINDOW_MS = 5000 // 5 seconds
9
-
10
- /**
11
- * Generate a simple hash for deduplication
12
- * @param {string} str - String to hash
13
- * @returns {string} Simple hash
14
- */
15
- function simpleHash(str) {
16
- let hash = 0
17
- for (let i = 0; i < str.length; i++) {
18
- const char = str.charCodeAt(i)
19
- hash = ((hash << 5) - hash) + char
20
- hash = hash & hash // Convert to 32bit integer
21
- }
22
- return hash.toString(36)
23
- }
24
-
25
- /**
26
- * Check if notification was recently sent (for deduplication)
27
- * @param {string} key - Deduplication key
28
- * @returns {boolean} True if duplicate
29
- */
30
- function isDuplicate(key) {
31
- const now = Date.now()
32
-
33
- // Clean up old entries
34
- for (const [k, timestamp] of recentNotifications) {
35
- if (now - timestamp > DEDUPE_WINDOW_MS) {
36
- recentNotifications.delete(k)
37
- }
38
- }
39
-
40
- if (recentNotifications.has(key)) {
41
- return true
42
- }
43
-
44
- recentNotifications.set(key, now)
45
- return false
46
- }
47
-
48
- /**
49
- * Build headers for ntfy requests
50
- * @param {string} [authToken] - Optional ntfy access token for Bearer auth
51
- * @returns {Object} Headers object
52
- */
53
- function buildHeaders(authToken) {
54
- const headers = {
55
- 'Content-Type': 'application/json',
56
- }
57
-
58
- if (authToken) {
59
- headers['Authorization'] = `Bearer ${authToken}`
60
- }
61
-
62
- return headers
63
- }
64
-
65
- /**
66
- * Send a basic notification to ntfy
67
- * @param {Object} options
68
- * @param {string} options.server - ntfy server URL
69
- * @param {string} options.topic - ntfy topic name
70
- * @param {string} options.title - Notification title
71
- * @param {string} options.message - Notification message
72
- * @param {number} [options.priority] - Priority (1-5, default 3)
73
- * @param {string[]} [options.tags] - Emoji tags
74
- * @param {string} [options.authToken] - Optional ntfy access token for protected topics
75
- */
76
- export async function sendNotification({ server, topic, title, message, priority, tags, authToken }) {
77
- // Deduplicate: skip if same notification sent recently
78
- const dedupeKey = simpleHash(`${topic}:${title}:${message}`)
79
- if (isDuplicate(dedupeKey)) {
80
- debug(`Notification skipped (duplicate): ${title}`)
81
- return
82
- }
83
-
84
- const body = {
85
- topic,
86
- title,
87
- message,
88
- }
89
-
90
- // Add optional fields only if provided
91
- if (priority !== undefined) {
92
- body.priority = priority
93
- }
94
- if (tags && tags.length > 0) {
95
- body.tags = tags
96
- }
97
-
98
- try {
99
- debug(`Notification sending: ${title}`)
100
- const response = await fetch(server, {
101
- method: 'POST',
102
- headers: buildHeaders(authToken),
103
- body: JSON.stringify(body),
104
- })
105
- debug(`Notification sent: ${title} (status=${response.status})`)
106
- } catch (error) {
107
- debug(`Notification failed: ${title} (error=${error.message})`)
108
- // Silently ignore - errors here shouldn't affect the user
109
- }
110
- }
@@ -1,29 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
- <plist version="1.0">
4
- <dict>
5
- <key>Label</key>
6
- <string>io.opencode.ntfy</string>
7
-
8
- <key>ProgramArguments</key>
9
- <array>
10
- <string>/usr/local/bin/node</string>
11
- <string>/usr/local/opt/opencode-ntfy/libexec/server.js</string>
12
- </array>
13
-
14
- <key>RunAtLoad</key>
15
- <true/>
16
-
17
- <key>KeepAlive</key>
18
- <true/>
19
-
20
- <key>StandardOutPath</key>
21
- <string>/usr/local/var/log/opencode-ntfy.log</string>
22
-
23
- <key>StandardErrorPath</key>
24
- <string>/usr/local/var/log/opencode-ntfy.log</string>
25
-
26
- <key>WorkingDirectory</key>
27
- <string>/usr/local/opt/opencode-ntfy/libexec</string>
28
- </dict>
29
- </plist>
@@ -1,34 +0,0 @@
1
- #!/usr/bin/env bash
2
- #
3
- # Run all tests for opencode-ntfy
4
- #
5
-
6
- set -euo pipefail
7
-
8
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
-
10
- echo "========================================"
11
- echo " opencode-ntfy test suite"
12
- echo "========================================"
13
- echo ""
14
-
15
- FAILED=0
16
-
17
- for test_file in "$SCRIPT_DIR"/test_*.bash; do
18
- if [[ -f "$test_file" ]] && [[ "$test_file" != *"test_helper.bash"* ]]; then
19
- echo "----------------------------------------"
20
- if ! bash "$test_file"; then
21
- FAILED=1
22
- fi
23
- echo ""
24
- fi
25
- done
26
-
27
- echo "========================================"
28
- if [[ $FAILED -eq 0 ]]; then
29
- echo " All test suites passed!"
30
- else
31
- echo " Some tests failed!"
32
- exit 1
33
- fi
34
- echo "========================================"