ha-opencode 0.1.1 → 0.2.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.
@@ -0,0 +1,394 @@
1
+ import WebSocket from "ws";
2
+ const RECONNECT_DELAY_MS = 5000;
3
+ const AUTH_TIMEOUT_MS = 10000;
4
+ const PING_INTERVAL_MS = 30000;
5
+ export function createHAWebSocketClient(config) {
6
+ let ws = null;
7
+ let connected = false;
8
+ let authenticated = false;
9
+ let messageId = 1;
10
+ let pendingRequests = new Map();
11
+ let commandHandler = null;
12
+ let stateRequestHandler = null;
13
+ let disconnectHandler = null;
14
+ let pingInterval = null;
15
+ let reconnecting = false;
16
+ // Auth flow handlers
17
+ let authResolve = null;
18
+ let authReject = null;
19
+ function nextId() {
20
+ return messageId++;
21
+ }
22
+ function send(message) {
23
+ return new Promise((resolve, reject) => {
24
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
25
+ reject(new Error("WebSocket not connected"));
26
+ return;
27
+ }
28
+ if (!authenticated && message.type !== "auth") {
29
+ reject(new Error("Not authenticated"));
30
+ return;
31
+ }
32
+ const id = nextId();
33
+ message.id = id;
34
+ const timeout = setTimeout(() => {
35
+ pendingRequests.delete(id);
36
+ reject(new Error(`Request ${id} timed out`));
37
+ }, AUTH_TIMEOUT_MS);
38
+ pendingRequests.set(id, { resolve, reject, timeout });
39
+ ws.send(JSON.stringify(message));
40
+ });
41
+ }
42
+ function sendNoResponse(message) {
43
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
44
+ return;
45
+ }
46
+ if (!authenticated && message.type !== "auth") {
47
+ return;
48
+ }
49
+ const id = nextId();
50
+ message.id = id;
51
+ ws.send(JSON.stringify(message));
52
+ }
53
+ function sendRaw(message) {
54
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
55
+ return;
56
+ }
57
+ ws.send(JSON.stringify(message));
58
+ }
59
+ function handleMessage(data) {
60
+ try {
61
+ const message = JSON.parse(data);
62
+ // Handle auth flow
63
+ if (message.type === "auth_required") {
64
+ // HA is asking for authentication
65
+ if (config.accessToken) {
66
+ sendRaw({ type: "auth", access_token: config.accessToken });
67
+ }
68
+ else {
69
+ if (authReject) {
70
+ authReject(new Error("No access token provided for authentication"));
71
+ }
72
+ }
73
+ return;
74
+ }
75
+ if (message.type === "auth_ok") {
76
+ authenticated = true;
77
+ if (authResolve) {
78
+ authResolve();
79
+ authResolve = null;
80
+ authReject = null;
81
+ }
82
+ return;
83
+ }
84
+ if (message.type === "auth_invalid") {
85
+ const errorMsg = message.message || "Authentication failed";
86
+ if (authReject) {
87
+ authReject(new Error(errorMsg));
88
+ authResolve = null;
89
+ authReject = null;
90
+ }
91
+ return;
92
+ }
93
+ // Handle responses to our requests
94
+ if (message.id && pendingRequests.has(message.id)) {
95
+ const pending = pendingRequests.get(message.id);
96
+ clearTimeout(pending.timeout);
97
+ pendingRequests.delete(message.id);
98
+ pending.resolve(message);
99
+ return;
100
+ }
101
+ // Handle server-initiated messages
102
+ if (message.type === "opencode/command" && commandHandler) {
103
+ const command = message.command;
104
+ const sessionId = message.session_id;
105
+ const msgData = message.data || {};
106
+ commandHandler(command, sessionId, msgData);
107
+ return;
108
+ }
109
+ if (message.type === "opencode/request_state" && stateRequestHandler) {
110
+ stateRequestHandler();
111
+ return;
112
+ }
113
+ // Handle pong (keep-alive response)
114
+ if (message.type === "pong") {
115
+ return;
116
+ }
117
+ }
118
+ catch {
119
+ // Silent failure - ignore malformed messages
120
+ }
121
+ }
122
+ function startPing() {
123
+ if (pingInterval) {
124
+ clearInterval(pingInterval);
125
+ }
126
+ pingInterval = setInterval(() => {
127
+ if (ws && ws.readyState === WebSocket.OPEN && authenticated) {
128
+ sendNoResponse({ type: "ping" });
129
+ }
130
+ }, PING_INTERVAL_MS);
131
+ }
132
+ function stopPing() {
133
+ if (pingInterval) {
134
+ clearInterval(pingInterval);
135
+ pingInterval = null;
136
+ }
137
+ }
138
+ function cleanup() {
139
+ stopPing();
140
+ connected = false;
141
+ authenticated = false;
142
+ // Reject all pending requests
143
+ for (const [id, pending] of pendingRequests) {
144
+ clearTimeout(pending.timeout);
145
+ pending.reject(new Error("Connection closed"));
146
+ }
147
+ pendingRequests.clear();
148
+ // Clean up auth handlers
149
+ if (authReject) {
150
+ authReject(new Error("Connection closed"));
151
+ authResolve = null;
152
+ authReject = null;
153
+ }
154
+ }
155
+ return {
156
+ async connect() {
157
+ if (ws && ws.readyState === WebSocket.OPEN) {
158
+ return;
159
+ }
160
+ return new Promise((resolve, reject) => {
161
+ ws = new WebSocket(config.url);
162
+ const connectTimeout = setTimeout(() => {
163
+ if (ws) {
164
+ ws.close();
165
+ }
166
+ reject(new Error("Connection timeout"));
167
+ }, AUTH_TIMEOUT_MS);
168
+ // Set up auth flow handlers
169
+ authResolve = () => {
170
+ clearTimeout(connectTimeout);
171
+ connected = true;
172
+ startPing();
173
+ resolve();
174
+ };
175
+ authReject = (err) => {
176
+ clearTimeout(connectTimeout);
177
+ reject(err);
178
+ };
179
+ ws.on("open", () => {
180
+ // Don't resolve yet - wait for auth_ok
181
+ });
182
+ ws.on("message", (data) => {
183
+ handleMessage(data.toString());
184
+ });
185
+ ws.on("close", () => {
186
+ cleanup();
187
+ if (disconnectHandler && !reconnecting) {
188
+ disconnectHandler();
189
+ }
190
+ });
191
+ ws.on("error", (err) => {
192
+ // Only log if we're not in a reconnect loop
193
+ if (!connected) {
194
+ clearTimeout(connectTimeout);
195
+ reject(err);
196
+ }
197
+ });
198
+ });
199
+ },
200
+ async disconnect() {
201
+ cleanup();
202
+ if (ws) {
203
+ ws.close();
204
+ ws = null;
205
+ }
206
+ },
207
+ isConnected() {
208
+ return connected && authenticated && ws !== null && ws.readyState === WebSocket.OPEN;
209
+ },
210
+ async pair(code, hostname) {
211
+ if (!this.isConnected()) {
212
+ return { success: false, error: "Not connected" };
213
+ }
214
+ try {
215
+ const response = await send({
216
+ type: "opencode/pair",
217
+ code,
218
+ hostname,
219
+ });
220
+ if (response.type === "result" && response.success) {
221
+ const result = response.result;
222
+ return {
223
+ success: true,
224
+ instanceId: result.instance_id,
225
+ instanceToken: result.instance_token,
226
+ };
227
+ }
228
+ else {
229
+ return {
230
+ success: false,
231
+ error: response.error?.message || "Pairing failed",
232
+ };
233
+ }
234
+ }
235
+ catch (err) {
236
+ return {
237
+ success: false,
238
+ error: err instanceof Error ? err.message : "Unknown error",
239
+ };
240
+ }
241
+ },
242
+ async reconnect(instanceToken, hostname) {
243
+ if (!this.isConnected()) {
244
+ return { success: false, error: "Not connected" };
245
+ }
246
+ try {
247
+ const response = await send({
248
+ type: "opencode/connect",
249
+ instance_token: instanceToken,
250
+ hostname,
251
+ });
252
+ if (response.type === "result" && response.success) {
253
+ const result = response.result;
254
+ return {
255
+ success: true,
256
+ instanceId: result.instance_id,
257
+ };
258
+ }
259
+ else {
260
+ return {
261
+ success: false,
262
+ error: response.error?.message || "Reconnection failed",
263
+ };
264
+ }
265
+ }
266
+ catch (err) {
267
+ return {
268
+ success: false,
269
+ error: err instanceof Error ? err.message : "Unknown error",
270
+ };
271
+ }
272
+ },
273
+ async sendSessionUpdate(instanceToken, session) {
274
+ if (!this.isConnected()) {
275
+ throw new Error("Not connected");
276
+ }
277
+ await send({
278
+ type: "opencode/session_update",
279
+ instance_token: instanceToken,
280
+ session,
281
+ });
282
+ },
283
+ async sendSessionRemoved(instanceToken, sessionId) {
284
+ if (!this.isConnected()) {
285
+ throw new Error("Not connected");
286
+ }
287
+ await send({
288
+ type: "opencode/session_removed",
289
+ instance_token: instanceToken,
290
+ session_id: sessionId,
291
+ });
292
+ },
293
+ async sendStateResponse(instanceToken, sessions) {
294
+ if (!this.isConnected()) {
295
+ throw new Error("Not connected");
296
+ }
297
+ await send({
298
+ type: "opencode/state_response",
299
+ instance_token: instanceToken,
300
+ sessions,
301
+ });
302
+ },
303
+ async sendHistoryResponse(instanceToken, data) {
304
+ if (!this.isConnected()) {
305
+ throw new Error("Not connected");
306
+ }
307
+ await send({
308
+ type: "opencode/history_response",
309
+ instance_token: instanceToken,
310
+ ...data,
311
+ });
312
+ },
313
+ async sendAgentsResponse(instanceToken, data) {
314
+ if (!this.isConnected()) {
315
+ throw new Error("Not connected");
316
+ }
317
+ await send({
318
+ type: "opencode/agents_response",
319
+ instance_token: instanceToken,
320
+ ...data,
321
+ });
322
+ },
323
+ onCommand(handler) {
324
+ commandHandler = handler;
325
+ },
326
+ onStateRequest(handler) {
327
+ stateRequestHandler = handler;
328
+ },
329
+ onDisconnect(handler) {
330
+ disconnectHandler = handler;
331
+ },
332
+ };
333
+ }
334
+ /**
335
+ * Create a WebSocket client with automatic reconnection.
336
+ */
337
+ export function createReconnectingClient(config, instanceToken, hostname, callbacks) {
338
+ // Support both old function signature and new callbacks object
339
+ const cb = typeof callbacks === 'function'
340
+ ? { onReconnected: callbacks }
341
+ : callbacks;
342
+ const client = createHAWebSocketClient(config);
343
+ let shouldReconnect = true;
344
+ let reconnectTimeout = null;
345
+ let hasNotifiedDisconnect = false;
346
+ async function attemptReconnect() {
347
+ if (!shouldReconnect)
348
+ return;
349
+ try {
350
+ await client.connect();
351
+ const result = await client.reconnect(instanceToken, hostname);
352
+ if (result.success) {
353
+ hasNotifiedDisconnect = false; // Reset for next disconnect
354
+ cb.onReconnected();
355
+ }
356
+ else {
357
+ scheduleReconnect();
358
+ }
359
+ }
360
+ catch {
361
+ scheduleReconnect();
362
+ }
363
+ }
364
+ function scheduleReconnect() {
365
+ if (!shouldReconnect)
366
+ return;
367
+ if (reconnectTimeout) {
368
+ clearTimeout(reconnectTimeout);
369
+ }
370
+ reconnectTimeout = setTimeout(attemptReconnect, RECONNECT_DELAY_MS);
371
+ }
372
+ client.onDisconnect(() => {
373
+ if (shouldReconnect) {
374
+ // Only notify once per disconnect cycle
375
+ if (!hasNotifiedDisconnect && cb.onDisconnected) {
376
+ hasNotifiedDisconnect = true;
377
+ cb.onDisconnected();
378
+ }
379
+ scheduleReconnect();
380
+ }
381
+ });
382
+ // Override disconnect to prevent reconnection
383
+ const originalDisconnect = client.disconnect.bind(client);
384
+ client.disconnect = async () => {
385
+ shouldReconnect = false;
386
+ if (reconnectTimeout) {
387
+ clearTimeout(reconnectTimeout);
388
+ reconnectTimeout = null;
389
+ }
390
+ await originalDisconnect();
391
+ };
392
+ return client;
393
+ }
394
+ //# sourceMappingURL=websocket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket.js","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ha-opencode",
3
- "version": "0.1.1",
4
- "description": "OpenCode plugin for Home Assistant integration via MQTT",
3
+ "version": "0.2.0",
4
+ "description": "OpenCode plugin for Home Assistant integration via native WebSocket API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -30,19 +30,20 @@
30
30
  "keywords": [
31
31
  "opencode",
32
32
  "home-assistant",
33
- "mqtt",
33
+ "websocket",
34
34
  "plugin"
35
35
  ],
36
36
  "author": "Stephen Golub",
37
37
  "license": "MIT",
38
38
  "dependencies": {
39
- "mqtt": "^5.10.0",
40
- "source-map-support": "^0.5.21"
39
+ "source-map-support": "^0.5.21",
40
+ "ws": "^8.18.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@opencode-ai/plugin": "^1.0.23",
44
44
  "@types/node": "^22.0.0",
45
45
  "@types/source-map-support": "^0.5.10",
46
+ "@types/ws": "^8.5.13",
46
47
  "@vitest/coverage-v8": "^4.0.16",
47
48
  "typescript": "^5.7.0",
48
49
  "vitest": "^4.0.16"
@@ -1,81 +0,0 @@
1
- blueprint:
2
- name: OpenCode Permission Response Handler
3
- description: >
4
- Handles approve/reject actions from OpenCode permission notifications.
5
-
6
- This blueprint listens for notification actions (button taps) and sends
7
- the appropriate permission response via MQTT.
8
-
9
- Works with both iOS and Android via the Home Assistant Companion app.
10
-
11
- Use this together with the OpenCode State Notifications blueprint.
12
- domain: automation
13
- author: ha-opencode
14
- source_url: https://github.com/your-repo/ha-opencode/blob/main/blueprints/opencode_permission_response.yaml
15
- input: {}
16
-
17
- mode: parallel
18
- max_exceeded: silent
19
-
20
- trigger:
21
- - platform: event
22
- event_type: mobile_app_notification_action
23
-
24
- variables:
25
- # Get the action string
26
- action: "{{ trigger.event.data.action | default('') }}"
27
-
28
- # Check if this is an OpenCode action
29
- is_opencode_action: >
30
- {{ action.startswith('OPENCODE_APPROVE_') or action.startswith('OPENCODE_REJECT_') }}
31
-
32
- is_approve: "{{ action.startswith('OPENCODE_APPROVE_') }}"
33
- response: "{{ 'once' if is_approve else 'reject' }}"
34
-
35
- # Get data passed from the notification
36
- # iOS uses action_data, Android might use data directly
37
- action_data: >
38
- {% if trigger.event.data.action_data is defined %}
39
- {{ trigger.event.data.action_data }}
40
- {% elif trigger.event.data.data is defined %}
41
- {{ trigger.event.data.data }}
42
- {% else %}
43
- {}
44
- {% endif %}
45
-
46
- command_topic: "{{ action_data.command_topic | default('') }}"
47
- permission_id: "{{ action_data.permission_id | default('') }}"
48
- device_id: "{{ action_data.device_id | default('') }}"
49
-
50
- condition:
51
- - condition: template
52
- value_template: "{{ is_opencode_action }}"
53
-
54
- action:
55
- # Log for debugging
56
- - service: system_log.write
57
- data:
58
- message: >
59
- OpenCode permission response: action={{ action }}, is_approve={{ is_approve }},
60
- response={{ response }}, command_topic={{ command_topic }},
61
- permission_id={{ permission_id }}, device_id={{ device_id }},
62
- action_data={{ action_data }}
63
- level: info
64
- logger: ha-opencode.permission
65
-
66
- # Check we have required data before publishing
67
- - condition: template
68
- value_template: "{{ command_topic != '' and permission_id != '' }}"
69
-
70
- - service: mqtt.publish
71
- data:
72
- topic: "{{ command_topic }}"
73
- payload: >
74
- {"command": "permission_response", "permission_id": "{{ permission_id }}", "response": "{{ response }}"}
75
-
76
- # Log success
77
- - service: system_log.write
78
- data:
79
- message: "OpenCode permission {{ response }} sent for {{ permission_id }}"
80
- level: info
81
- logger: ha-opencode.permission