@ericsanchezok/meta-synergy 0.0.0-dev-202603260728

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 (48) hide show
  1. package/.turbo/turbo-typecheck.log +1 -0
  2. package/dist/meta-protocol/src/bash.d.ts +89 -0
  3. package/dist/meta-protocol/src/bash.js +40 -0
  4. package/dist/meta-protocol/src/client.d.ts +9 -0
  5. package/dist/meta-protocol/src/client.js +1 -0
  6. package/dist/meta-protocol/src/env.d.ts +16 -0
  7. package/dist/meta-protocol/src/env.js +17 -0
  8. package/dist/meta-protocol/src/envelope.d.ts +50 -0
  9. package/dist/meta-protocol/src/envelope.js +23 -0
  10. package/dist/meta-protocol/src/error.d.ts +39 -0
  11. package/dist/meta-protocol/src/error.js +24 -0
  12. package/dist/meta-protocol/src/host.d.ts +90 -0
  13. package/dist/meta-protocol/src/host.js +27 -0
  14. package/dist/meta-protocol/src/index.d.ts +7 -0
  15. package/dist/meta-protocol/src/index.js +7 -0
  16. package/dist/meta-protocol/src/process.d.ts +274 -0
  17. package/dist/meta-protocol/src/process.js +89 -0
  18. package/dist/meta-synergy/src/client/holos-client.d.ts +15 -0
  19. package/dist/meta-synergy/src/client/holos-client.js +35 -0
  20. package/dist/meta-synergy/src/exec/bash-runner.d.ts +7 -0
  21. package/dist/meta-synergy/src/exec/bash-runner.js +9 -0
  22. package/dist/meta-synergy/src/exec/process-registry.d.ts +11 -0
  23. package/dist/meta-synergy/src/exec/process-registry.js +597 -0
  24. package/dist/meta-synergy/src/host.d.ts +32 -0
  25. package/dist/meta-synergy/src/host.js +27 -0
  26. package/dist/meta-synergy/src/index.d.ts +8 -0
  27. package/dist/meta-synergy/src/index.js +8 -0
  28. package/dist/meta-synergy/src/platform.d.ts +25 -0
  29. package/dist/meta-synergy/src/platform.js +230 -0
  30. package/dist/meta-synergy/src/rpc/handler.d.ts +66 -0
  31. package/dist/meta-synergy/src/rpc/handler.js +60 -0
  32. package/dist/meta-synergy/src/rpc/schema.d.ts +163 -0
  33. package/dist/meta-synergy/src/rpc/schema.js +11 -0
  34. package/dist/meta-synergy/src/types.d.ts +14 -0
  35. package/dist/meta-synergy/src/types.js +1 -0
  36. package/package.json +30 -0
  37. package/script/publish.ts +32 -0
  38. package/src/client/holos-client.ts +49 -0
  39. package/src/exec/bash-runner.ts +10 -0
  40. package/src/exec/process-registry.ts +728 -0
  41. package/src/host.ts +39 -0
  42. package/src/index.ts +8 -0
  43. package/src/platform.ts +227 -0
  44. package/src/rpc/handler.ts +76 -0
  45. package/src/rpc/schema.ts +16 -0
  46. package/src/types.ts +17 -0
  47. package/test/rpc-handler.test.ts +76 -0
  48. package/tsconfig.json +23 -0
package/src/host.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { MetaProtocolEnv, MetaProtocolHost } from "@ericsanchezok/meta-protocol"
2
+ import { Platform } from "./platform"
3
+
4
+ export interface MetaSynergyHostOptions {
5
+ envID?: MetaProtocolEnv.EnvID
6
+ hostSessionID?: MetaProtocolEnv.HostSessionID
7
+ capabilities?: MetaProtocolHost.Capabilities
8
+ }
9
+
10
+ export class MetaSynergyHost {
11
+ readonly envID?: MetaProtocolEnv.EnvID
12
+ readonly hostSessionID: MetaProtocolEnv.HostSessionID
13
+ readonly capabilities: MetaProtocolHost.Capabilities
14
+
15
+ constructor(options: MetaSynergyHostOptions = {}) {
16
+ this.envID = options.envID
17
+ this.hostSessionID = options.hostSessionID || crypto.randomUUID()
18
+ this.capabilities = options.capabilities || Platform.detectCapabilities()
19
+ }
20
+
21
+ hello() {
22
+ if (!this.envID) {
23
+ throw new Error("MetaSynergyHost requires envID to emit host.hello")
24
+ }
25
+
26
+ return {
27
+ type: "host.hello" as const,
28
+ envID: this.envID,
29
+ hostSessionID: this.hostSessionID,
30
+ capabilities: this.capabilities,
31
+ }
32
+ }
33
+
34
+ assertEnv(envID: string) {
35
+ if (this.envID && envID !== this.envID) {
36
+ throw new Error(`env mismatch: host bound to ${this.envID}, received ${envID}`)
37
+ }
38
+ }
39
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./types"
2
+ export * from "./host"
3
+ export * from "./platform"
4
+ export * from "./client/holos-client"
5
+ export * from "./rpc/schema"
6
+ export * from "./rpc/handler"
7
+ export * from "./exec/bash-runner"
8
+ export * from "./exec/process-registry"
@@ -0,0 +1,227 @@
1
+ import os from "node:os"
2
+ import path from "node:path"
3
+ import process from "node:process"
4
+ import { spawn } from "node:child_process"
5
+ import { MetaProtocolHost } from "@ericsanchezok/meta-protocol"
6
+
7
+ const SIGKILL_TIMEOUT_MS = 200
8
+ const ESC = "\u001b"
9
+
10
+ export type ProcessEnv = Record<string, string | undefined>
11
+ export type ChildLike = { pid?: number; kill(signal?: number | NodeJS.Signals): boolean }
12
+
13
+ export namespace Platform {
14
+ export function runtime(): MetaProtocolHost.Runtime {
15
+ if (typeof process.versions?.bun === "string") return "bun"
16
+ if (typeof process.versions?.node === "string") return "node"
17
+ return "unknown"
18
+ }
19
+
20
+ export function defaultShell(): MetaProtocolHost.Shell {
21
+ if (process.platform === "win32") {
22
+ const comspec = (process.env.ComSpec || process.env.COMSPEC || "").toLowerCase()
23
+ if (comspec.includes("pwsh")) return "pwsh"
24
+ if (comspec.includes("powershell")) return "powershell"
25
+ return "cmd"
26
+ }
27
+ return "sh"
28
+ }
29
+
30
+ export function supportedShells(): MetaProtocolHost.Shell[] {
31
+ return process.platform === "win32" ? ["cmd", "powershell", "pwsh"] : ["sh"]
32
+ }
33
+
34
+ export function detectCapabilities(): MetaProtocolHost.Capabilities {
35
+ return {
36
+ platform: process.platform,
37
+ arch: process.arch,
38
+ hostname: safeHostname(),
39
+ runtime: runtime(),
40
+ defaultShell: defaultShell(),
41
+ supportedShells: supportedShells(),
42
+ supportsPty: false,
43
+ supportsSendKeys: true,
44
+ supportsSoftKill: process.platform !== "win32",
45
+ supportsProcessGroups: process.platform !== "win32",
46
+ envCaseInsensitive: process.platform === "win32",
47
+ lineEndings: process.platform === "win32" ? "crlf" : "lf",
48
+ }
49
+ }
50
+
51
+ export function normalizeEnv(env: ProcessEnv): ProcessEnv {
52
+ if (process.platform !== "win32") {
53
+ return { ...env }
54
+ }
55
+
56
+ const result: ProcessEnv = {}
57
+ const entries = Object.entries(env).sort(([left], [right]) => left.localeCompare(right))
58
+ const seen = new Set<string>()
59
+ for (const [key, value] of entries) {
60
+ const upper = key.toUpperCase()
61
+ if (seen.has(upper) && key !== "Path") continue
62
+ seen.add(upper)
63
+ result[key === "PATH" ? "Path" : key] = value
64
+ }
65
+ return result
66
+ }
67
+
68
+ export function resolveShellLaunch(command: string): { shell: MetaProtocolHost.Shell; file: string; args: string[] } {
69
+ if (process.platform === "win32") {
70
+ return {
71
+ shell: "cmd",
72
+ file: process.env.ComSpec || process.env.COMSPEC || "cmd.exe",
73
+ args: ["/d", "/s", "/c", command],
74
+ }
75
+ }
76
+
77
+ return {
78
+ shell: "sh",
79
+ file: "/bin/sh",
80
+ args: ["-c", command],
81
+ }
82
+ }
83
+
84
+ export async function killTree(child: ChildLike, exited?: () => boolean): Promise<void> {
85
+ const pid = child.pid
86
+ if (!pid || exited?.()) return
87
+
88
+ if (process.platform === "win32") {
89
+ await new Promise<void>((resolve) => {
90
+ const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], {
91
+ stdio: "ignore",
92
+ windowsHide: true,
93
+ })
94
+ killer.once("exit", () => resolve())
95
+ killer.once("error", () => resolve())
96
+ })
97
+ return
98
+ }
99
+
100
+ try {
101
+ process.kill(-pid, "SIGTERM")
102
+ await sleep(SIGKILL_TIMEOUT_MS)
103
+ if (!exited?.()) process.kill(-pid, "SIGKILL")
104
+ return
105
+ } catch {}
106
+
107
+ child.kill("SIGTERM")
108
+ await sleep(SIGKILL_TIMEOUT_MS)
109
+ if (!exited?.()) child.kill("SIGKILL")
110
+ }
111
+
112
+ export function encodeKeySequence(keys: string[]): { data: string; warnings: string[] } {
113
+ const warnings: string[] = []
114
+ let data = ""
115
+ for (const token of keys) {
116
+ data += encodeKeyToken(token, warnings)
117
+ }
118
+ return { data, warnings }
119
+ }
120
+
121
+ export function resolveWorkdir(workdir?: string): string {
122
+ if (!workdir) return process.cwd()
123
+ if (path.isAbsolute(workdir)) return workdir
124
+ return path.resolve(process.cwd(), workdir)
125
+ }
126
+
127
+ export function sleep(ms: number): Promise<void> {
128
+ return new Promise((resolve) => setTimeout(resolve, ms))
129
+ }
130
+ }
131
+
132
+ function encodeKeyToken(raw: string, warnings: string[]): string {
133
+ const token = raw.trim()
134
+ if (!token) return ""
135
+ if (token.length === 2 && token.startsWith("^")) {
136
+ const ctrl = toCtrlChar(token[1])
137
+ if (ctrl) return ctrl
138
+ }
139
+
140
+ const parsed = parseModifiers(token)
141
+ const named = namedKey(parsed.base.toLowerCase())
142
+ if (named) {
143
+ return parsed.alt ? `${ESC}${named}` : named
144
+ }
145
+
146
+ if (parsed.base.length === 1) {
147
+ let value = parsed.shift && /[a-z]/.test(parsed.base) ? parsed.base.toUpperCase() : parsed.base
148
+ if (parsed.ctrl) value = toCtrlChar(value) || value
149
+ if (parsed.alt) value = `${ESC}${value}`
150
+ return value
151
+ }
152
+
153
+ if (parsed.hasModifiers) {
154
+ warnings.push(`Unknown key \"${parsed.base}\" for modifiers; sending literal.`)
155
+ }
156
+ return parsed.base
157
+ }
158
+
159
+ function parseModifiers(token: string) {
160
+ let rest = token
161
+ let ctrl = false
162
+ let alt = false
163
+ let shift = false
164
+ let hasModifiers = false
165
+
166
+ while (rest.length > 2 && rest[1] === "-") {
167
+ const mod = rest[0].toLowerCase()
168
+ if (mod === "c") ctrl = true
169
+ else if (mod === "m") alt = true
170
+ else if (mod === "s") shift = true
171
+ else break
172
+ hasModifiers = true
173
+ rest = rest.slice(2)
174
+ }
175
+
176
+ return { base: rest, ctrl, alt, shift, hasModifiers }
177
+ }
178
+
179
+ function namedKey(input: string): string | undefined {
180
+ const map = new Map<string, string>([
181
+ ["enter", "\r"],
182
+ ["return", "\r"],
183
+ ["tab", "\t"],
184
+ ["escape", ESC],
185
+ ["esc", ESC],
186
+ ["space", " "],
187
+ ["backspace", process.platform === "win32" ? "\b" : "\u007f"],
188
+ ["up", `${ESC}[A`],
189
+ ["down", `${ESC}[B`],
190
+ ["right", `${ESC}[C`],
191
+ ["left", `${ESC}[D`],
192
+ ["home", `${ESC}[1~`],
193
+ ["end", `${ESC}[4~`],
194
+ ["pageup", `${ESC}[5~`],
195
+ ["pagedown", `${ESC}[6~`],
196
+ ["insert", `${ESC}[2~`],
197
+ ["delete", `${ESC}[3~`],
198
+ ["f1", `${ESC}OP`],
199
+ ["f2", `${ESC}OQ`],
200
+ ["f3", `${ESC}OR`],
201
+ ["f4", `${ESC}OS`],
202
+ ["f5", `${ESC}[15~`],
203
+ ["f6", `${ESC}[17~`],
204
+ ["f7", `${ESC}[18~`],
205
+ ["f8", `${ESC}[19~`],
206
+ ["f9", `${ESC}[20~`],
207
+ ["f10", `${ESC}[21~`],
208
+ ["f11", `${ESC}[23~`],
209
+ ["f12", `${ESC}[24~`],
210
+ ])
211
+ return map.get(input)
212
+ }
213
+
214
+ function toCtrlChar(char: string): string | null {
215
+ if (char.length !== 1) return null
216
+ if (char === "?") return "\u007f"
217
+ const code = char.toUpperCase().charCodeAt(0)
218
+ return code >= 64 && code <= 95 ? String.fromCharCode(code & 0x1f) : null
219
+ }
220
+
221
+ function safeHostname(): string | undefined {
222
+ try {
223
+ return os.hostname()
224
+ } catch {
225
+ return undefined
226
+ }
227
+ }
@@ -0,0 +1,76 @@
1
+ import { MetaProtocolEnvelope, MetaProtocolError } from "@ericsanchezok/meta-protocol"
2
+ import { ProcessRegistry } from "../exec/process-registry"
3
+ import { MetaSynergyHost, type MetaSynergyHostOptions } from "../host"
4
+ import { BashRunner } from "../exec/bash-runner"
5
+ import { RPCRequestSchema } from "./schema"
6
+
7
+ export class RPCHandler {
8
+ readonly host: MetaSynergyHost
9
+ readonly processRegistry: ProcessRegistry
10
+ readonly bashRunner: BashRunner
11
+
12
+ constructor(options: MetaSynergyHostOptions = {}) {
13
+ this.host = new MetaSynergyHost(options)
14
+ this.processRegistry = new ProcessRegistry(this.host)
15
+ this.bashRunner = new BashRunner(this.processRegistry)
16
+ }
17
+
18
+ async handle(input: unknown) {
19
+ try {
20
+ const request = RPCRequestSchema.parse(input)
21
+ this.host.assertEnv(request.envID)
22
+
23
+ if (request.tool === "bash") {
24
+ const result = await this.bashRunner.run(request.payload, request.envID)
25
+ return {
26
+ version: 1,
27
+ requestID: request.requestID,
28
+ ok: true,
29
+ result,
30
+ } as const
31
+ }
32
+
33
+ if (request.tool === "process") {
34
+ const result = await this.processRegistry.execute(request.payload, request.envID)
35
+ return {
36
+ version: 1,
37
+ requestID: request.requestID,
38
+ ok: true,
39
+ result,
40
+ } as const
41
+ }
42
+
43
+ return errorResult(undefined, "unsupported_tool", "Unsupported tool")
44
+ } catch (error) {
45
+ if (isEnvelopeError(error)) {
46
+ return errorResult(error.requestID, error.code, error.message, error.details)
47
+ }
48
+
49
+ return errorResult(undefined, "host_internal_error", error instanceof Error ? error.message : String(error))
50
+ }
51
+ }
52
+ }
53
+
54
+ function errorResult(
55
+ requestID: string | undefined,
56
+ code: MetaProtocolError.Code,
57
+ message: string,
58
+ details?: unknown,
59
+ ): MetaProtocolEnvelope.ErrorResult {
60
+ return {
61
+ version: 1,
62
+ requestID: requestID || crypto.randomUUID(),
63
+ ok: false,
64
+ error: {
65
+ code,
66
+ message,
67
+ details,
68
+ },
69
+ }
70
+ }
71
+
72
+ function isEnvelopeError(
73
+ error: unknown,
74
+ ): error is { requestID?: string; code: MetaProtocolError.Code; message: string; details?: unknown } {
75
+ return typeof error === "object" && error !== null && "code" in error && "message" in error
76
+ }
@@ -0,0 +1,16 @@
1
+ import z from "zod"
2
+ import { MetaProtocolBash, MetaProtocolEnvelope, MetaProtocolProcess } from "@ericsanchezok/meta-protocol"
3
+
4
+ export const RPCRequestSchema = z.discriminatedUnion("tool", [
5
+ MetaProtocolBash.ExecuteRequest,
6
+ MetaProtocolProcess.ExecuteRequest,
7
+ ])
8
+
9
+ export const RPCResultSchema = z.discriminatedUnion("ok", [
10
+ MetaProtocolBash.ExecuteResult,
11
+ MetaProtocolProcess.ExecuteResult,
12
+ MetaProtocolEnvelope.ErrorResult,
13
+ ])
14
+
15
+ export type RPCRequest = z.infer<typeof RPCRequestSchema>
16
+ export type RPCResult = z.infer<typeof RPCResultSchema>
package/src/types.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { MetaProtocolEnv, MetaProtocolHost } from "@ericsanchezok/meta-protocol"
2
+
3
+ export type EnvID = MetaProtocolEnv.EnvID
4
+ export type RequestID = string
5
+
6
+ export interface RemoteHostIdentity {
7
+ envID: EnvID
8
+ hostSessionID: MetaProtocolEnv.HostSessionID
9
+ capabilities: MetaProtocolHost.Capabilities
10
+ label?: string
11
+ }
12
+
13
+ export interface HostTransport {
14
+ connect(): Promise<void>
15
+ disconnect(): Promise<void>
16
+ send(message: unknown): Promise<void>
17
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { RPCHandler } from "../src/rpc/handler"
3
+
4
+ describe("meta-synergy rpc handler", () => {
5
+ test("bash background execution returns process id", async () => {
6
+ const handler = new RPCHandler({ envID: "env_test" })
7
+ const result = await handler.handle({
8
+ version: 1,
9
+ requestID: "req_1",
10
+ envID: "env_test",
11
+ tool: "bash",
12
+ action: "execute",
13
+ payload: {
14
+ command: "echo hello && sleep 1",
15
+ description: "background test",
16
+ background: true,
17
+ },
18
+ })
19
+
20
+ expect(result.ok).toBe(true)
21
+ if (!result.ok) return
22
+ const metadata = result.result.metadata as { processId?: string; background?: boolean }
23
+ expect(metadata.processId).toBeTruthy()
24
+ expect(metadata.background).toBe(true)
25
+ })
26
+
27
+ test("process list includes backgrounded process", async () => {
28
+ const handler = new RPCHandler({ envID: "env_test" })
29
+ const started = await handler.handle({
30
+ version: 1,
31
+ requestID: "req_2",
32
+ envID: "env_test",
33
+ tool: "bash",
34
+ action: "execute",
35
+ payload: {
36
+ command: "echo hello && sleep 1",
37
+ description: "background test",
38
+ background: true,
39
+ },
40
+ })
41
+
42
+ expect(started.ok).toBe(true)
43
+ if (!started.ok) return
44
+
45
+ const listed = await handler.handle({
46
+ version: 1,
47
+ requestID: "req_3",
48
+ envID: "env_test",
49
+ tool: "process",
50
+ action: "list",
51
+ payload: { action: "list" },
52
+ })
53
+
54
+ expect(listed.ok).toBe(true)
55
+ if (!listed.ok) return
56
+ const startedMetadata = started.result.metadata as { processId?: string }
57
+ const listedMetadata = listed.result.metadata as { processes?: Array<{ processId: string }> }
58
+ expect(listedMetadata.processes?.some((item) => item.processId === startedMetadata.processId)).toBe(true)
59
+ })
60
+
61
+ test("env mismatch returns error envelope", async () => {
62
+ const handler = new RPCHandler({ envID: "env_bound" })
63
+ const result = await handler.handle({
64
+ version: 1,
65
+ requestID: "req_4",
66
+ envID: "env_other",
67
+ tool: "process",
68
+ action: "list",
69
+ payload: { action: "list" },
70
+ })
71
+
72
+ expect(result.ok).toBe(false)
73
+ if (result.ok) return
74
+ expect(result.error.code).toBe("host_internal_error")
75
+ })
76
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig.json",
3
+ "compilerOptions": {
4
+ "target": "ESNext",
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "lib": ["ESNext", "DOM"],
8
+ "types": ["node"],
9
+ "skipLibCheck": true,
10
+ "allowSyntheticDefaultImports": true,
11
+ "esModuleInterop": true,
12
+ "outDir": "dist",
13
+ "declaration": true,
14
+ "noEmit": false,
15
+ "strict": true,
16
+ "isolatedModules": true,
17
+ "baseUrl": ".",
18
+ "paths": {
19
+ "@ericsanchezok/meta-protocol": ["../meta-protocol/src/index.ts"]
20
+ }
21
+ },
22
+ "include": ["src"]
23
+ }