@ericsanchezok/meta-synergy 1.1.26 → 1.2.17

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/src/display.ts ADDED
@@ -0,0 +1,70 @@
1
+ export type MetaSynergyHiddenReason = "managed" | "policy"
2
+
3
+ interface IdentifierValueOptions {
4
+ missing?: string
5
+ unknown?: string
6
+ hiddenReason?: MetaSynergyHiddenReason | null
7
+ showStart?: number
8
+ showEnd?: number
9
+ }
10
+
11
+ interface IdentifierListOptions extends IdentifierValueOptions {
12
+ separator?: string
13
+ }
14
+
15
+ export namespace MetaSynergyDisplay {
16
+ export function identifier(value: string | null | undefined, options?: IdentifierValueOptions): string {
17
+ if (value === null || value === undefined || value.length === 0) {
18
+ return options?.missing ?? "none"
19
+ }
20
+
21
+ if (options?.hiddenReason) {
22
+ return maskIdentifier(value, options)
23
+ }
24
+
25
+ return value
26
+ }
27
+
28
+ export function maybeIdentifier(value: unknown, options?: IdentifierValueOptions): string {
29
+ if (typeof value !== "string") {
30
+ return value == null ? (options?.missing ?? "none") : (options?.unknown ?? "unknown")
31
+ }
32
+
33
+ return identifier(value, options)
34
+ }
35
+
36
+ export function identifierList(values: Array<string> | undefined, options?: IdentifierListOptions): string {
37
+ if (!values || values.length === 0) {
38
+ return options?.missing ?? "none"
39
+ }
40
+
41
+ const separator = options?.separator ?? ", "
42
+ return values.map((value) => identifier(value, options)).join(separator)
43
+ }
44
+
45
+ export function maskIdentifier(
46
+ value: string,
47
+ options?: { hiddenReason?: MetaSynergyHiddenReason | null; showStart?: number; showEnd?: number },
48
+ ): string {
49
+ const showStart = options?.showStart ?? defaultPrefixLength(value)
50
+ const showEnd = options?.showEnd ?? defaultSuffixLength(value)
51
+
52
+ if (value.length <= showStart + showEnd + 3) {
53
+ return `${value.slice(0, Math.max(1, Math.min(4, value.length)))}...`
54
+ }
55
+
56
+ return `${value.slice(0, showStart)}...${value.slice(-showEnd)}`
57
+ }
58
+
59
+ function defaultPrefixLength(value: string) {
60
+ if (value.startsWith("env_")) return 8
61
+ if (value.startsWith("ses_")) return 8
62
+ return 8
63
+ }
64
+
65
+ function defaultSuffixLength(value: string) {
66
+ if (value.startsWith("env_")) return 7
67
+ if (value.startsWith("ses_")) return 6
68
+ return 8
69
+ }
70
+ }
@@ -83,14 +83,21 @@ export class ProcessRegistry {
83
83
  return this.#backgroundResult(launched.record, envID, request.description, "Background")
84
84
  }
85
85
 
86
- if (request.yieldMs && request.yieldMs > 0) {
86
+ if (request.yieldSeconds && request.yieldSeconds > 0) {
87
+ const yieldMs = request.yieldSeconds * 1000
87
88
  const autoBackground = await Promise.race([
88
89
  this.#waitForExit(launched.record.processId).then(() => false),
89
- Platform.sleep(request.yieldMs).then(() => !launched.record.exited),
90
+ Platform.sleep(yieldMs).then(() => !launched.record.exited),
90
91
  ])
91
92
  if (autoBackground) {
92
93
  launched.record.backgrounded = true
93
- return this.#backgroundResult(launched.record, envID, request.description, "Auto-Background", request.yieldMs)
94
+ return this.#backgroundResult(
95
+ launched.record,
96
+ envID,
97
+ request.description,
98
+ "Auto-Background",
99
+ request.yieldSeconds,
100
+ )
94
101
  }
95
102
  }
96
103
 
@@ -547,10 +554,12 @@ export class ProcessRegistry {
547
554
  envID: string,
548
555
  description: string,
549
556
  mode: "Background" | "Auto-Background",
550
- yieldMs?: number,
557
+ yieldSeconds?: number,
551
558
  ): MetaProtocolBash.Result {
552
559
  const prefix =
553
- mode === "Auto-Background" ? `Command auto-backgrounded after ${yieldMs}ms.` : "Command started in background."
560
+ mode === "Auto-Background"
561
+ ? `Command auto-backgrounded after ${yieldSeconds}s.`
562
+ : "Command started in background."
554
563
  return {
555
564
  title: `[${mode}] ${description}`,
556
565
  metadata: {
@@ -0,0 +1,183 @@
1
+ import os from "node:os"
2
+ import path from "node:path"
3
+ import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises"
4
+ import { applyEdits, modify } from "jsonc-parser"
5
+ import z from "zod"
6
+ import { MetaSynergyStore, type MetaSynergyAuthState } from "../state/store"
7
+
8
+ export type MetaSynergyHolosAuthSource = "shared" | "legacy-migrated"
9
+
10
+ export const HOLOS_API_HOST = "api.holosai.io"
11
+ export const HOLOS_PORTAL_HOST = "www.holosai.io"
12
+ export const HOLOS_URL = `https://${HOLOS_API_HOST}`
13
+ export const HOLOS_WS_URL = `wss://${HOLOS_API_HOST}`
14
+ export const HOLOS_PORTAL_URL = `https://${HOLOS_PORTAL_HOST}`
15
+
16
+ const JSONC_FORMATTING = {
17
+ insertSpaces: true,
18
+ tabSize: 2,
19
+ eol: "\n",
20
+ } as const
21
+
22
+ const SynergyHolosAuth = z.object({
23
+ type: z.literal("holos"),
24
+ agentId: z.string(),
25
+ agentSecret: z.string(),
26
+ })
27
+
28
+ const SynergyAuthRecord = z.record(z.string(), z.unknown())
29
+ const SynergyConfigSetMetadata = z.object({ active: z.string().min(1).default("default") })
30
+
31
+ export namespace MetaSynergyHolosAuth {
32
+ export function synergyRoot() {
33
+ return path.join(process.env.SYNERGY_TEST_HOME || os.homedir(), ".synergy")
34
+ }
35
+
36
+ export function sharedAuthPath() {
37
+ return path.join(synergyRoot(), "data", "auth", "api-key.json")
38
+ }
39
+
40
+ export function configMetadataPath() {
41
+ return path.join(synergyRoot(), "config", "config-set.json")
42
+ }
43
+
44
+ export async function globalConfigPath() {
45
+ try {
46
+ const raw = await readFile(configMetadataPath(), "utf8")
47
+ const metadata = SynergyConfigSetMetadata.parse(JSON.parse(raw))
48
+ return metadata.active === "default"
49
+ ? path.join(synergyRoot(), "config", "synergy.jsonc")
50
+ : path.join(synergyRoot(), "config", "config-sets", metadata.active, "synergy.jsonc")
51
+ } catch {
52
+ return path.join(synergyRoot(), "config", "synergy.jsonc")
53
+ }
54
+ }
55
+
56
+ export async function inspect(): Promise<
57
+ { auth: MetaSynergyAuthState; source: MetaSynergyHolosAuthSource } | { auth: undefined; source: null }
58
+ > {
59
+ const shared = await loadShared()
60
+ if (shared) {
61
+ return {
62
+ auth: shared,
63
+ source: "shared",
64
+ }
65
+ }
66
+
67
+ const legacy = await loadLegacy()
68
+ if (!legacy) {
69
+ return {
70
+ auth: undefined,
71
+ source: null,
72
+ }
73
+ }
74
+
75
+ await saveShared(legacy)
76
+ return {
77
+ auth: legacy,
78
+ source: "legacy-migrated",
79
+ }
80
+ }
81
+
82
+ export async function load(): Promise<MetaSynergyAuthState | undefined> {
83
+ return (await inspect()).auth
84
+ }
85
+
86
+ export async function save(auth: MetaSynergyAuthState): Promise<void> {
87
+ await saveShared(auth)
88
+ await MetaSynergyStore.saveLegacyAuth(auth)
89
+ await configureHolos()
90
+ }
91
+
92
+ export async function configureHolos(): Promise<void> {
93
+ const filePath = await globalConfigPath()
94
+ const source = await loadGlobalConfigSource(filePath)
95
+ const next = applyEdits(
96
+ source,
97
+ modify(
98
+ source,
99
+ ["holos"],
100
+ {
101
+ enabled: true,
102
+ apiUrl: HOLOS_URL,
103
+ wsUrl: HOLOS_WS_URL,
104
+ portalUrl: HOLOS_PORTAL_URL,
105
+ },
106
+ { formattingOptions: JSONC_FORMATTING },
107
+ ),
108
+ )
109
+
110
+ await mkdir(path.dirname(filePath), { recursive: true })
111
+ await writeFile(filePath, next.endsWith("\n") ? next : `${next}\n`)
112
+ await chmod(filePath, 0o600).catch(() => undefined)
113
+ }
114
+
115
+ export async function clear(): Promise<void> {
116
+ await removeShared()
117
+ await MetaSynergyStore.clearLegacyAuth()
118
+ }
119
+
120
+ async function loadShared(): Promise<MetaSynergyAuthState | undefined> {
121
+ try {
122
+ const parsed = SynergyAuthRecord.parse(JSON.parse(await readFile(sharedAuthPath(), "utf8")))
123
+ const holos = SynergyHolosAuth.safeParse(parsed.holos)
124
+ if (!holos.success) return undefined
125
+ return {
126
+ agentID: holos.data.agentId,
127
+ agentSecret: holos.data.agentSecret,
128
+ }
129
+ } catch {
130
+ return undefined
131
+ }
132
+ }
133
+
134
+ async function loadLegacy(): Promise<MetaSynergyAuthState | undefined> {
135
+ return await MetaSynergyStore.loadLegacyAuth()
136
+ }
137
+
138
+ async function saveShared(auth: MetaSynergyAuthState): Promise<void> {
139
+ const filePath = sharedAuthPath()
140
+ let data: Record<string, unknown> = {}
141
+ try {
142
+ data = SynergyAuthRecord.parse(JSON.parse(await readFile(filePath, "utf8")))
143
+ } catch {
144
+ data = {}
145
+ }
146
+
147
+ const next = {
148
+ ...data,
149
+ holos: {
150
+ type: "holos",
151
+ agentId: auth.agentID,
152
+ agentSecret: auth.agentSecret,
153
+ },
154
+ }
155
+
156
+ await mkdir(path.dirname(filePath), { recursive: true })
157
+ await writeFile(filePath, JSON.stringify(next, null, 2) + "\n")
158
+ await chmod(filePath, 0o600)
159
+ }
160
+
161
+ async function removeShared(): Promise<void> {
162
+ let data: Record<string, unknown>
163
+ try {
164
+ data = SynergyAuthRecord.parse(JSON.parse(await readFile(sharedAuthPath(), "utf8")))
165
+ } catch {
166
+ await unlink(sharedAuthPath()).catch(() => undefined)
167
+ return
168
+ }
169
+
170
+ delete data.holos
171
+ await mkdir(path.dirname(sharedAuthPath()), { recursive: true })
172
+ await writeFile(sharedAuthPath(), JSON.stringify(data, null, 2) + "\n")
173
+ }
174
+
175
+ async function loadGlobalConfigSource(filePath: string): Promise<string> {
176
+ try {
177
+ const source = await readFile(filePath, "utf8")
178
+ return source.trim().length > 0 ? source : "{}\n"
179
+ } catch {
180
+ return "{}\n"
181
+ }
182
+ }
183
+ }
@@ -1,16 +1,18 @@
1
1
  import process from "node:process"
2
2
  import { createServer, type IncomingMessage } from "node:http"
3
3
  import { spawn } from "node:child_process"
4
- import { MetaSynergyStore } from "../state/store"
4
+ import { createInterface } from "node:readline/promises"
5
+ import { stdin as input, stdout as output } from "node:process"
6
+ import { MetaSynergyStore, type MetaSynergyAuthState } from "../state/store"
7
+ import { HOLOS_PORTAL_URL, HOLOS_URL, MetaSynergyHolosAuth } from "./auth"
5
8
  import { MetaSynergyHolosProtocol } from "./protocol"
6
9
 
7
- const HOLOS_HOST = "www.holosai.io"
8
- const HOLOS_URL = `https://${HOLOS_HOST}`
10
+ const LOGIN_TIMEOUT_MS = 5 * 60_000
9
11
 
10
12
  export namespace MetaSynergyHolosLogin {
11
13
  export function createBindURL(input: { callbackURL: string; state: string }) {
12
14
  return (
13
- `${HOLOS_URL}/api/v1/holos/agent_tunnel/bind/start` +
15
+ `${HOLOS_PORTAL_URL}/api/v1/holos/agent_tunnel/bind/start` +
14
16
  `?local_callback=${encodeURIComponent(input.callbackURL)}` +
15
17
  `&state=${encodeURIComponent(input.state)}`
16
18
  )
@@ -27,6 +29,57 @@ export namespace MetaSynergyHolosLogin {
27
29
  return { valid: true }
28
30
  }
29
31
 
32
+ export async function loginWithExistingCredentials(auth: MetaSynergyAuthState): Promise<{ agentID: string }> {
33
+ const verification = await verifySecret(auth.agentSecret)
34
+ if (!verification.valid) {
35
+ throw new Error(`Credential validation failed: ${verification.reason}`)
36
+ }
37
+
38
+ await MetaSynergyHolosAuth.save(auth)
39
+ return { agentID: auth.agentID }
40
+ }
41
+
42
+ export async function promptForExistingCredentials(): Promise<MetaSynergyAuthState | null> {
43
+ const agentID = await promptText("Agent ID: ")
44
+ if (!agentID) {
45
+ return null
46
+ }
47
+
48
+ const agentSecret = await promptSecret("Agent Secret: ")
49
+ if (!agentSecret) {
50
+ return null
51
+ }
52
+
53
+ return {
54
+ agentID,
55
+ agentSecret,
56
+ }
57
+ }
58
+
59
+ export async function promptLoginMode(): Promise<"browser" | "existing" | null> {
60
+ if (!input.isTTY || !output.isTTY) {
61
+ return null
62
+ }
63
+
64
+ while (true) {
65
+ output.write(
66
+ ["Choose login mode:", " 1) Browser login", " 2) Import existing agent credentials", "Select [1]: "].join(
67
+ "\n",
68
+ ),
69
+ )
70
+
71
+ const answer = await readLine()
72
+ const normalized = answer.trim().toLowerCase()
73
+ if (normalized === "" || normalized === "1" || normalized === "browser" || normalized === "b") {
74
+ return "browser"
75
+ }
76
+ if (normalized === "2" || normalized === "existing" || normalized === "import" || normalized === "i") {
77
+ return "existing"
78
+ }
79
+ output.write("Invalid selection. Enter 1 or 2.\n\n")
80
+ }
81
+ }
82
+
30
83
  export async function login(): Promise<{ agentID: string }> {
31
84
  await MetaSynergyStore.ensureRoot()
32
85
  const state = crypto.randomUUID()
@@ -75,7 +128,7 @@ export namespace MetaSynergyHolosLogin {
75
128
  const timer = setTimeout(() => {
76
129
  server.close()
77
130
  reject(new Error("Login timed out."))
78
- }, 5 * 60_000)
131
+ }, LOGIN_TIMEOUT_MS)
79
132
  timer.unref?.()
80
133
  })
81
134
 
@@ -113,12 +166,10 @@ export namespace MetaSynergyHolosLogin {
113
166
  throw new Error("Holos exchange did not return an agent secret.")
114
167
  }
115
168
 
116
- await MetaSynergyStore.saveAuth({
169
+ return await loginWithExistingCredentials({
117
170
  agentID: exchangeBody.data.agent_id,
118
171
  agentSecret,
119
172
  })
120
-
121
- return { agentID: exchangeBody.data.agent_id }
122
173
  }
123
174
  }
124
175
 
@@ -143,6 +194,95 @@ async function launchBrowser(url: string): Promise<void> {
143
194
  child.unref()
144
195
  }
145
196
 
197
+ async function promptText(prompt: string): Promise<string | null> {
198
+ if (!input.isTTY || !output.isTTY) {
199
+ return null
200
+ }
201
+
202
+ output.write(prompt)
203
+ const answer = await readLine()
204
+ const value = answer.trim()
205
+ return value ? value : null
206
+ }
207
+
208
+ async function promptSecret(prompt: string): Promise<string | null> {
209
+ if (!input.isTTY || !output.isTTY) {
210
+ return null
211
+ }
212
+
213
+ output.write(prompt)
214
+ const secret = await readSecretLine()
215
+ output.write("\n")
216
+ const value = secret.trim()
217
+ return value ? value : null
218
+ }
219
+
220
+ async function readLine(): Promise<string> {
221
+ const rl = createInterface({ input, output, terminal: false })
222
+ try {
223
+ return await rl.question("")
224
+ } finally {
225
+ rl.close()
226
+ }
227
+ }
228
+
229
+ async function readSecretLine(): Promise<string> {
230
+ if (!input.isTTY) {
231
+ return await readLine()
232
+ }
233
+
234
+ const previousRawMode = typeof input.setRawMode === "function" ? input.isRaw : undefined
235
+ const chunks: string[] = []
236
+
237
+ return await new Promise<string>((resolve, reject) => {
238
+ const cleanup = () => {
239
+ input.off("data", onData)
240
+ input.off("error", onError)
241
+ if (typeof input.setRawMode === "function") {
242
+ input.setRawMode(Boolean(previousRawMode))
243
+ }
244
+ input.pause()
245
+ }
246
+
247
+ const finish = () => {
248
+ cleanup()
249
+ resolve(chunks.join(""))
250
+ }
251
+
252
+ const onError = (error: Error) => {
253
+ cleanup()
254
+ reject(error)
255
+ }
256
+
257
+ const onData = (chunk: Buffer | string) => {
258
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8")
259
+ for (const char of text) {
260
+ if (char === "\r" || char === "\n") {
261
+ finish()
262
+ return
263
+ }
264
+ if (char === "\u0003") {
265
+ cleanup()
266
+ reject(new Error("Cancelled"))
267
+ return
268
+ }
269
+ if (char === "\u007f" || char === "\b") {
270
+ chunks.pop()
271
+ continue
272
+ }
273
+ chunks.push(char)
274
+ }
275
+ }
276
+
277
+ if (typeof input.setRawMode === "function") {
278
+ input.setRawMode(true)
279
+ }
280
+ input.resume()
281
+ input.on("data", onData)
282
+ input.on("error", onError)
283
+ })
284
+ }
285
+
146
286
  function htmlPage(input: { title: string; status: "success" | "failed"; heading: string; message: string }): string {
147
287
  const accent = input.status === "success" ? "#86efac" : "#fca5a5"
148
288
  const badge = input.status === "success" ? "Connected" : "Error"
@@ -1,5 +1,5 @@
1
1
  import { MetaProtocolEnvelope, MetaProtocolError, MetaProtocolSession } from "@ericsanchezok/meta-protocol"
2
- import type { HolosCaller } from "../types"
2
+ import { HolosCallerSchema, type HolosCaller } from "../types"
3
3
  import { RPCHandler } from "../rpc/handler"
4
4
  import { RPCRequestSchema, type RPCResult } from "../rpc/schema"
5
5
  import { SessionManager } from "../session/manager"
@@ -14,12 +14,13 @@ export class MetaSynergyInboundHandler {
14
14
  readonly decideOpen: (input: { caller: HolosCaller; label?: string }) => Promise<SessionOpenDecision>,
15
15
  ) {}
16
16
 
17
- async handle(input: { caller: HolosCaller; body: unknown }): Promise<RPCResult> {
17
+ async handle(input: { caller: HolosCaller | unknown; body: unknown }): Promise<RPCResult> {
18
18
  try {
19
+ const caller = HolosCallerSchema.parse(input.caller)
19
20
  const request = RPCRequestSchema.parse(input.body)
20
21
  MetaSynergyLog.info("inbound.request.accepted", {
21
- callerAgentID: input.caller.agentID,
22
- callerOwnerUserID: input.caller.ownerUserID,
22
+ callerAgentID: caller.agentID,
23
+ callerOwnerUserID: caller.ownerUserID,
23
24
  tool: request.tool,
24
25
  action: request.action,
25
26
  requestID: request.requestID,
@@ -29,13 +30,13 @@ export class MetaSynergyInboundHandler {
29
30
  })
30
31
 
31
32
  if (request.tool === "session") {
32
- return this.#handleSession(input.caller, request)
33
+ return this.#handleSession(caller, request)
33
34
  }
34
35
 
35
- this.sessions.validateCaller(input.caller, request.sessionID)
36
+ this.sessions.validateCaller(caller, request.sessionID)
36
37
  const result = await this.rpc.handle(request)
37
38
  MetaSynergyLog.info("inbound.request.completed", {
38
- callerAgentID: input.caller.agentID,
39
+ callerAgentID: caller.agentID,
39
40
  tool: request.tool,
40
41
  action: request.action,
41
42
  requestID: request.requestID,
@@ -44,8 +45,13 @@ export class MetaSynergyInboundHandler {
44
45
  return result
45
46
  } catch (error) {
46
47
  if (isEnvelopeError(error)) {
48
+ const callerAgentID =
49
+ typeof input.caller === "object" && input.caller !== null && "agentID" in input.caller
50
+ ? String((input.caller as { agentID?: unknown }).agentID ?? "unknown")
51
+ : "unknown"
47
52
  MetaSynergyLog.warn("inbound.request.failed.envelope", {
48
- callerAgentID: input.caller.agentID,
53
+ callerAgentID,
54
+
49
55
  code: error.code,
50
56
  message: error.message,
51
57
  details: error.details,
@@ -62,8 +68,13 @@ export class MetaSynergyInboundHandler {
62
68
  )
63
69
  }
64
70
 
71
+ const callerAgentID =
72
+ typeof input.caller === "object" && input.caller !== null && "agentID" in input.caller
73
+ ? String((input.caller as { agentID?: unknown }).agentID ?? "unknown")
74
+ : "unknown"
65
75
  MetaSynergyLog.error("inbound.request.failed.unexpected", {
66
- callerAgentID: input.caller.agentID,
76
+ callerAgentID,
77
+
67
78
  error: error instanceof Error ? error.message : String(error),
68
79
  })
69
80
  return errorResult(undefined, "host_internal_error", error instanceof Error ? error.message : String(error))
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export * from "./log"
3
3
  export * from "./host"
4
4
  export * from "./platform"
5
5
  export * from "./runtime"
6
+ export * from "./owner-registry"
6
7
  export * from "./client/holos-client"
7
8
  export * from "./session/manager"
8
9
  export * from "./inbound/handler"
@@ -0,0 +1,22 @@
1
+ import { MetaSynergyStore } from "../state/store"
2
+ import { MetaSynergyStateMigrations } from "../state/migration"
3
+ import type { MetaSynergyMigration } from "./types"
4
+ export type { MetaSynergyMigration } from "./types"
5
+
6
+ export namespace MetaSynergyMigrationRunner {
7
+ export async function run(): Promise<void> {
8
+ const applied = await MetaSynergyStore.loadMigrationLog()
9
+ const migrations = collect().sort((left, right) => left.id.localeCompare(right.id))
10
+
11
+ for (const migration of migrations) {
12
+ if (applied[migration.id]) continue
13
+ await migration.run()
14
+ applied[migration.id] = Date.now()
15
+ await MetaSynergyStore.saveMigrationLog(applied)
16
+ }
17
+ }
18
+ }
19
+
20
+ function collect(): MetaSynergyMigration[] {
21
+ return [...MetaSynergyStateMigrations.migrations]
22
+ }
@@ -0,0 +1,5 @@
1
+ export interface MetaSynergyMigration {
2
+ id: string
3
+ description: string
4
+ run(): Promise<void>
5
+ }