@ericsanchezok/meta-synergy 1.1.7 → 1.1.9

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@ericsanchezok/meta-synergy",
4
- "version": "1.1.7",
4
+ "version": "1.1.9",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -12,13 +12,19 @@ const HOLOS_WS_URL = `wss://${HOLOS_HOST}`
12
12
  export class MetaSynergyHolosClient {
13
13
  #ws: WebSocket | null = null
14
14
  #heartbeat: ReturnType<typeof setInterval> | null = null
15
+ #disconnecting = false
15
16
 
16
17
  constructor(
17
18
  readonly auth: { agentID: string; agentSecret: string },
18
19
  readonly inbound: MetaSynergyInboundHandler,
20
+ readonly hooks?: {
21
+ onOpen?: () => void | Promise<void>
22
+ onClose?: (input: { opened: boolean; intentional: boolean }) => void | Promise<void>
23
+ },
19
24
  ) {}
20
25
 
21
26
  async connect() {
27
+ this.#disconnecting = false
22
28
  const token = await fetchWsToken(this.auth.agentSecret)
23
29
  const endpoint = `${HOLOS_WS_URL}/api/v1/holos/agent_tunnel/ws?token=${token}`
24
30
  MetaSynergyLog.info("holos.connect.begin", {
@@ -35,6 +41,7 @@ export class MetaSynergyHolosClient {
35
41
  MetaSynergyLog.info("holos.connect.open", {
36
42
  agentID: this.auth.agentID,
37
43
  })
44
+ void this.hooks?.onOpen?.()
38
45
  resolve()
39
46
  })
40
47
  ws.addEventListener("error", (error) => {
@@ -58,10 +65,14 @@ export class MetaSynergyHolosClient {
58
65
  ws.addEventListener("close", () => {
59
66
  MetaSynergyLog.warn("holos.socket.closed", {
60
67
  agentID: this.auth.agentID,
68
+ intentional: this.#disconnecting,
61
69
  })
62
70
  if (this.#heartbeat) clearInterval(this.#heartbeat)
63
71
  this.#heartbeat = null
64
72
  this.#ws = null
73
+ const intentional = this.#disconnecting
74
+ this.#disconnecting = false
75
+ void this.hooks?.onClose?.({ opened: true, intentional })
65
76
  })
66
77
 
67
78
  this.#heartbeat = setInterval(() => {
@@ -78,6 +89,7 @@ export class MetaSynergyHolosClient {
78
89
  })
79
90
  if (this.#heartbeat) clearInterval(this.#heartbeat)
80
91
  this.#heartbeat = null
92
+ this.#disconnecting = true
81
93
  this.#ws?.close()
82
94
  this.#ws = null
83
95
  }
@@ -22,7 +22,7 @@ export namespace MetaSynergyHolosLogin {
22
22
  })
23
23
  const body = MetaSynergyHolosProtocol.WsTokenResponse.safeParse(await response.json())
24
24
  if (!body.success || !response.ok || body.data.code !== 0) {
25
- return { valid: false, reason: body.success ? body.data.message ?? "Invalid response" : "Invalid response" }
25
+ return { valid: false, reason: body.success ? (body.data.message ?? "Invalid response") : "Invalid response" }
26
26
  }
27
27
  return { valid: true }
28
28
  }
@@ -47,12 +47,28 @@ export namespace MetaSynergyHolosLogin {
47
47
  if (!params.code || !params.state) {
48
48
  reject(new Error("Missing login callback parameters."))
49
49
  response.statusCode = 400
50
- response.end("Login failed.")
50
+ response.setHeader("Content-Type", "text/html; charset=utf-8")
51
+ response.end(
52
+ htmlPage({
53
+ title: "MetaSynergy Login",
54
+ status: "failed",
55
+ heading: "Login failed",
56
+ message: "Missing callback parameters. Please return to MetaSynergy and try again.",
57
+ }),
58
+ )
51
59
  return
52
60
  }
53
61
 
54
62
  resolve({ code: params.code, state: params.state })
55
- response.end("Login successful. You can close this window.")
63
+ response.setHeader("Content-Type", "text/html; charset=utf-8")
64
+ response.end(
65
+ htmlPage({
66
+ title: "MetaSynergy Login",
67
+ status: "success",
68
+ heading: "MetaSynergy is ready",
69
+ message: "Login successful. You can close this page and return to the app.",
70
+ }),
71
+ )
56
72
  })
57
73
  server.listen(port, "127.0.0.1")
58
74
 
@@ -126,3 +142,163 @@ async function launchBrowser(url: string): Promise<void> {
126
142
  const child = spawn(command[0], command.slice(1), { stdio: "ignore", detached: true })
127
143
  child.unref()
128
144
  }
145
+
146
+ function htmlPage(input: { title: string; status: "success" | "failed"; heading: string; message: string }): string {
147
+ const accent = input.status === "success" ? "#86efac" : "#fca5a5"
148
+ const badge = input.status === "success" ? "Connected" : "Error"
149
+ const symbol = input.status === "success" ? "✓" : "!"
150
+
151
+ return `<!DOCTYPE html>
152
+ <html lang="en">
153
+ <head>
154
+ <meta charset="utf-8" />
155
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
156
+ <title>${escapeHtml(input.title)}</title>
157
+ <style>
158
+ :root {
159
+ color-scheme: dark;
160
+ --bg: #050816;
161
+ --panel: rgba(12, 18, 38, 0.82);
162
+ --panel-border: rgba(255, 255, 255, 0.12);
163
+ --text: #f8fafc;
164
+ --text-dim: rgba(226, 232, 240, 0.72);
165
+ --accent: ${accent};
166
+ }
167
+
168
+ * { box-sizing: border-box; }
169
+
170
+ body {
171
+ margin: 0;
172
+ min-height: 100vh;
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+ padding: 24px;
177
+ font-family:
178
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
179
+ sans-serif;
180
+ color: var(--text);
181
+ background:
182
+ radial-gradient(circle at top, rgba(79, 70, 229, 0.28), transparent 34%),
183
+ radial-gradient(circle at bottom right, rgba(34, 197, 94, 0.18), transparent 30%),
184
+ linear-gradient(180deg, #030712 0%, var(--bg) 100%);
185
+ }
186
+
187
+ .shell {
188
+ width: min(100%, 560px);
189
+ border-radius: 24px;
190
+ border: 1px solid var(--panel-border);
191
+ background: var(--panel);
192
+ backdrop-filter: blur(22px);
193
+ box-shadow:
194
+ 0 24px 80px rgba(0, 0, 0, 0.42),
195
+ inset 0 1px 0 rgba(255, 255, 255, 0.06);
196
+ overflow: hidden;
197
+ }
198
+
199
+ .masthead {
200
+ padding: 20px 24px 0;
201
+ display: flex;
202
+ justify-content: space-between;
203
+ align-items: center;
204
+ }
205
+
206
+ .brand {
207
+ font-size: 13px;
208
+ letter-spacing: 0.14em;
209
+ text-transform: uppercase;
210
+ color: var(--text-dim);
211
+ }
212
+
213
+ .badge {
214
+ display: inline-flex;
215
+ align-items: center;
216
+ gap: 8px;
217
+ padding: 7px 12px;
218
+ border-radius: 999px;
219
+ background: rgba(255, 255, 255, 0.05);
220
+ border: 1px solid rgba(255, 255, 255, 0.08);
221
+ color: var(--text);
222
+ font-size: 12px;
223
+ }
224
+
225
+ .badge::before {
226
+ content: "";
227
+ width: 8px;
228
+ height: 8px;
229
+ border-radius: 999px;
230
+ background: var(--accent);
231
+ box-shadow: 0 0 16px var(--accent);
232
+ }
233
+
234
+ .content {
235
+ padding: 28px 24px 30px;
236
+ text-align: center;
237
+ }
238
+
239
+ .mark {
240
+ width: 72px;
241
+ height: 72px;
242
+ margin: 0 auto 18px;
243
+ border-radius: 22px;
244
+ display: grid;
245
+ place-items: center;
246
+ font-size: 34px;
247
+ font-weight: 700;
248
+ color: #04111f;
249
+ background: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(255,255,255,0.8));
250
+ box-shadow:
251
+ 0 12px 30px rgba(0, 0, 0, 0.24),
252
+ 0 0 0 6px rgba(255, 255, 255, 0.03);
253
+ }
254
+
255
+ h1 {
256
+ margin: 0 0 10px;
257
+ font-size: clamp(28px, 6vw, 36px);
258
+ line-height: 1.05;
259
+ }
260
+
261
+ p {
262
+ margin: 0 auto;
263
+ max-width: 34ch;
264
+ color: var(--text-dim);
265
+ font-size: 15px;
266
+ line-height: 1.6;
267
+ }
268
+
269
+ .footer {
270
+ margin-top: 24px;
271
+ padding-top: 18px;
272
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
273
+ color: rgba(226, 232, 240, 0.58);
274
+ font-size: 12px;
275
+ letter-spacing: 0.06em;
276
+ text-transform: uppercase;
277
+ }
278
+ </style>
279
+ </head>
280
+ <body>
281
+ <main class="shell">
282
+ <div class="masthead">
283
+ <div class="brand">MetaSynergy</div>
284
+ <div class="badge">${badge}</div>
285
+ </div>
286
+ <section class="content">
287
+ <div class="mark">${symbol}</div>
288
+ <h1>${escapeHtml(input.heading)}</h1>
289
+ <p>${escapeHtml(input.message)}</p>
290
+ <div class="footer">Holos agent tunnel</div>
291
+ </section>
292
+ </main>
293
+ </body>
294
+ </html>`
295
+ }
296
+
297
+ function escapeHtml(value: string): string {
298
+ return value
299
+ .replaceAll("&", "&amp;")
300
+ .replaceAll("<", "&lt;")
301
+ .replaceAll(">", "&gt;")
302
+ .replaceAll('"', "&quot;")
303
+ .replaceAll("'", "&#39;")
304
+ }
@@ -85,11 +85,7 @@ export class MetaSynergyInboundHandler {
85
85
  callerAgentID: input.caller.agentID,
86
86
  error: error instanceof Error ? error.message : String(error),
87
87
  })
88
- return errorResult(
89
- undefined,
90
- "host_internal_error",
91
- error instanceof Error ? error.message : String(error),
92
- )
88
+ return errorResult(undefined, "host_internal_error", error instanceof Error ? error.message : String(error))
93
89
  }
94
90
  }
95
91
 
@@ -192,9 +188,7 @@ function errorResult(
192
188
  }
193
189
  }
194
190
 
195
- function isEnvelopeError(
196
- error: unknown,
197
- ): error is {
191
+ function isEnvelopeError(error: unknown): error is {
198
192
  requestID?: string
199
193
  tool?: MetaProtocolEnvelope.Tool
200
194
  action?: string
@@ -136,9 +136,7 @@ function errorResult(
136
136
  }
137
137
  }
138
138
 
139
- function isEnvelopeError(
140
- error: unknown,
141
- ): error is {
139
+ function isEnvelopeError(error: unknown): error is {
142
140
  requestID?: string
143
141
  tool?: MetaProtocolEnvelope.Tool
144
142
  action?: string
package/src/rpc/schema.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import z from "zod"
2
- import { MetaProtocolBash, MetaProtocolEnvelope, MetaProtocolProcess, MetaProtocolSession } from "@ericsanchezok/meta-protocol"
2
+ import {
3
+ MetaProtocolBash,
4
+ MetaProtocolEnvelope,
5
+ MetaProtocolProcess,
6
+ MetaProtocolSession,
7
+ } from "@ericsanchezok/meta-protocol"
3
8
 
4
9
  export const RPCRequestSchema = z.discriminatedUnion("tool", [
5
10
  MetaProtocolBash.ExecuteRequest,
package/src/runtime.ts CHANGED
@@ -19,6 +19,10 @@ export class MetaSynergyRuntime {
19
19
  #client: MetaSynergyHolosClient | null = null
20
20
  #timers: Array<ReturnType<typeof setInterval>> = []
21
21
  #lastControlRequestID?: string
22
+ #reconnectTimer: ReturnType<typeof setTimeout> | null = null
23
+ #reconnectAttempt = 0
24
+ #stopping = false
25
+ #manualReconnectInFlight = false
22
26
 
23
27
  private constructor(
24
28
  host: MetaSynergyHost,
@@ -82,6 +86,7 @@ export class MetaSynergyRuntime {
82
86
 
83
87
  async start() {
84
88
  const state = requireState(this)
89
+ this.#stopping = false
85
90
  state.connectionStatus = "connecting"
86
91
  await MetaSynergyStore.saveState(state)
87
92
 
@@ -96,15 +101,26 @@ export class MetaSynergyRuntime {
96
101
  throw new Error("Meta Synergy could not load credentials after login.")
97
102
  }
98
103
 
99
- this.#client = new MetaSynergyHolosClient(nextAuth, this.inbound)
100
- await this.#connectClient()
101
104
  this.#startLoops()
105
+ this.#ensureClient(nextAuth)
106
+ try {
107
+ await this.#connectClient()
108
+ } catch (error) {
109
+ MetaSynergyLog.error("runtime.connection.initial_connect_failed", {
110
+ error: error instanceof Error ? error.message : String(error),
111
+ })
112
+ await this.#setConnectionStatus("disconnected")
113
+ this.#scheduleReconnect("initial_connect_failed")
114
+ }
102
115
 
103
- console.log(`Connected to Holos as ${nextAuth.agentID}`)
116
+ console.log(`MetaSynergy running as ${nextAuth.agentID}`)
104
117
  console.log(`envID: ${this.host.envID}`)
118
+ console.log(`Holos: ${this.state?.connectionStatus ?? "disconnected"}`)
105
119
  console.log(`Status: ${this.sessions.current() ? "busy" : "idle"}`)
106
120
 
107
121
  const stop = async () => {
122
+ this.#stopping = true
123
+ this.#clearReconnectTimer()
108
124
  this.#stopLoops()
109
125
  if (this.state) {
110
126
  this.state.connectionStatus = "disconnected"
@@ -132,6 +148,8 @@ export class MetaSynergyRuntime {
132
148
  }
133
149
 
134
150
  async logout() {
151
+ this.#stopping = true
152
+ this.#clearReconnectTimer()
135
153
  this.#stopLoops()
136
154
  if (this.state) {
137
155
  this.state.connectionStatus = "disconnected"
@@ -226,7 +244,10 @@ export class MetaSynergyRuntime {
226
244
  this.sessions.kickCurrent(disk.controlRequest.block ?? false)
227
245
  }
228
246
  if (disk.controlRequest.kind === "reconnect") {
229
- await this.#reconnectClient()
247
+ const succeeded = await this.#reconnectClient()
248
+ if (!succeeded) {
249
+ this.#scheduleReconnect("manual_reconnect_failed")
250
+ }
230
251
  }
231
252
  this.state.controlRequest = undefined
232
253
  await MetaSynergyStore.saveState(this.state)
@@ -242,35 +263,112 @@ export class MetaSynergyRuntime {
242
263
  throw new Error("Meta Synergy could not load credentials.")
243
264
  }
244
265
 
245
- if (!this.#client) {
246
- this.#client = new MetaSynergyHolosClient(auth, this.inbound)
266
+ this.#ensureClient(auth)
267
+ const client = this.#client
268
+ if (!client) {
269
+ throw new Error("Meta Synergy could not create Holos client.")
247
270
  }
248
271
 
249
- this.state.connectionStatus = "connecting"
250
- await MetaSynergyStore.saveState(this.state)
272
+ await this.#setConnectionStatus("connecting")
251
273
  try {
252
- await this.#client.connect()
253
- this.state.connectionStatus = "connected"
254
- await MetaSynergyStore.saveState(this.state)
274
+ await client.connect()
275
+ this.#reconnectAttempt = 0
276
+ this.#clearReconnectTimer()
277
+ await this.#setConnectionStatus("connected")
255
278
  } catch (error) {
256
- this.state.connectionStatus = "disconnected"
257
- await MetaSynergyStore.saveState(this.state)
279
+ await this.#setConnectionStatus("disconnected")
258
280
  throw error
259
281
  }
260
282
  }
261
283
 
262
284
  async #reconnectClient() {
263
285
  MetaSynergyLog.info("runtime.connection.reconnect.begin")
286
+ this.#manualReconnectInFlight = true
287
+ this.#clearReconnectTimer()
264
288
  await this.#client?.disconnect().catch(() => undefined)
265
289
  this.#client = null
290
+ let succeeded = false
266
291
  try {
267
292
  await this.#connectClient()
293
+ succeeded = true
268
294
  MetaSynergyLog.info("runtime.connection.reconnect.completed")
269
295
  } catch (error) {
270
296
  MetaSynergyLog.error("runtime.connection.reconnect.failed", {
271
297
  error: error instanceof Error ? error.message : String(error),
272
298
  })
299
+ } finally {
300
+ this.#manualReconnectInFlight = false
273
301
  }
302
+ return succeeded
303
+ }
304
+
305
+ #ensureClient(auth: { agentID: string; agentSecret: string }) {
306
+ if (this.#client?.auth.agentID === auth.agentID && this.#client?.auth.agentSecret === auth.agentSecret) {
307
+ return
308
+ }
309
+ let client!: MetaSynergyHolosClient
310
+ client = new MetaSynergyHolosClient(auth, this.inbound, {
311
+ onClose: ({ intentional }) => {
312
+ if (this.#client !== client) return
313
+ void this.#handleClientClosed({ intentional })
314
+ },
315
+ })
316
+ this.#client = client
317
+ }
318
+
319
+ async #handleClientClosed(input: { intentional: boolean }) {
320
+ await this.#setConnectionStatus("disconnected")
321
+ if (input.intentional || this.#stopping || this.#manualReconnectInFlight) {
322
+ return
323
+ }
324
+ this.#scheduleReconnect("socket_closed")
325
+ }
326
+
327
+ #scheduleReconnect(reason: string) {
328
+ if (this.#stopping || this.#manualReconnectInFlight || this.#reconnectTimer) return
329
+
330
+ const attempt = this.#reconnectAttempt + 1
331
+ const delayMs = Math.min(60_000, 2_000 * 2 ** (attempt - 1))
332
+ this.#reconnectAttempt = attempt
333
+ MetaSynergyLog.warn("runtime.connection.reconnect.scheduled", {
334
+ reason,
335
+ attempt,
336
+ delayMs,
337
+ })
338
+
339
+ const timer = setTimeout(() => {
340
+ this.#reconnectTimer = null
341
+ void this.#performScheduledReconnect()
342
+ }, delayMs)
343
+ timer.unref?.()
344
+ this.#reconnectTimer = timer
345
+ }
346
+
347
+ async #performScheduledReconnect() {
348
+ if (this.#stopping || this.#manualReconnectInFlight) return
349
+ try {
350
+ const succeeded = await this.#reconnectClient()
351
+ if (!succeeded) {
352
+ this.#scheduleReconnect("scheduled_reconnect_failed")
353
+ }
354
+ } catch (error) {
355
+ MetaSynergyLog.error("runtime.connection.reconnect.unexpected", {
356
+ error: error instanceof Error ? error.message : String(error),
357
+ })
358
+ this.#scheduleReconnect("scheduled_reconnect_failed")
359
+ }
360
+ }
361
+
362
+ #clearReconnectTimer() {
363
+ if (!this.#reconnectTimer) return
364
+ clearTimeout(this.#reconnectTimer)
365
+ this.#reconnectTimer = null
366
+ }
367
+
368
+ async #setConnectionStatus(status: "disconnected" | "connecting" | "connected") {
369
+ if (!this.state) return
370
+ this.state.connectionStatus = status
371
+ await MetaSynergyStore.saveState(this.state)
274
372
  }
275
373
 
276
374
  async confirmCollaboration(input: { caller: { agentID: string }; label?: string }) {
@@ -314,5 +412,5 @@ function requireState(runtime: MetaSynergyRuntime) {
314
412
  }
315
413
 
316
414
  function escapeAppleScript(input: string) {
317
- return input.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")
415
+ return input.replaceAll("\\", "\\\\").replaceAll('"', '\\"')
318
416
  }
@@ -244,6 +244,9 @@ export class SessionManager {
244
244
  }
245
245
  }
246
246
 
247
- function envelopeError(code: MetaProtocolError.Code, message: string): { code: MetaProtocolError.Code; message: string } {
247
+ function envelopeError(
248
+ code: MetaProtocolError.Code,
249
+ message: string,
250
+ ): { code: MetaProtocolError.Code; message: string } {
248
251
  return { code, message }
249
252
  }