IPC tweaks

This commit is contained in:
cte 2025-03-21 22:33:00 -07:00
parent 8b367116b1
commit 8c1a22f5dd
12 changed files with 892 additions and 354 deletions

View file

@ -1,10 +1,12 @@
import { IpcClient } from "../src/ipcClient"
import { IpcClientMessageType, IpcClient } from "../src/index.js"
async function main(socketPath: string) {
async function main(socketPath: string, prompt: string) {
try {
const startTime = Date.now()
const client = new IpcClient(socketPath)
client.on("message", (data) => console.log(data))
while (!client.isConnected) {
if (Date.now() - startTime > 5000) {
throw new Error("Failed to connect to server.")
@ -13,9 +15,10 @@ async function main(socketPath: string) {
await new Promise((resolve) => setTimeout(resolve, 1000))
}
client.sendMessage({ type: IpcClientMessageType.StartNewTask, data: { text: prompt } })
while (client.isConnected) {
client.ping()
await new Promise((resolve) => setTimeout(resolve, 5000))
await new Promise((resolve) => setTimeout(resolve, 1000))
}
process.exit(0)
@ -26,8 +29,8 @@ async function main(socketPath: string) {
}
if (!process.argv[2]) {
console.error("Usage: npx tsx scripts/client.ts <socketPath>")
console.error("Usage: npx tsx scripts/client.ts <socketPath> <prompt>")
process.exit(1)
}
main(process.argv[2])
main(process.argv[2], process.argv[3])

View file

@ -1,27 +0,0 @@
import { IpcServer } from "../src/ipcServer"
import { ServerMessageType } from "../src/types"
async function main(socketPath: string) {
try {
const server = new IpcServer(socketPath)
server.listen()
console.log(`listening @ ${server.socketPath}`)
while (server.isListening) {
server.broadcast({ type: ServerMessageType.Message, data: { message: "hello" } })
await new Promise((resolve) => setTimeout(resolve, 1000))
}
process.exit(0)
} catch (e) {
console.error(e)
process.exit(1)
}
}
if (!process.argv[2]) {
console.error("Usage: npx tsx scripts/client.ts <socketPath>")
process.exit(1)
}
main(process.argv[2])

View file

@ -1,4 +1,262 @@
export { IpcClient } from "./ipcClient.js"
export { IpcServer } from "./ipcServer.js"
import EventEmitter from "node:events"
import { Socket } from "node:net"
import * as crypto from "node:crypto"
export * from "./types.js"
import ipc from "node-ipc"
import { z } from "zod"
/**
* IpcClient
*/
export type IpcClientId = string
export enum IpcClientMessageType {
Message = "Message",
StartNewTask = "StartNewTask",
}
export const ipcClientMessageSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal(IpcClientMessageType.StartNewTask),
data: z.object({
text: z.string(),
images: z.array(z.string()).optional(),
}),
}),
])
export type IpcClientMessage = z.infer<typeof ipcClientMessageSchema>
export interface IpcClientEvents {
connect: []
disconnect: []
message: [data: IpcServerMessage]
}
export class IpcClient extends EventEmitter<IpcClientEvents> {
private readonly _socketPath: string
private readonly _log: (...args: unknown[]) => void
private _isConnected = false
private _clientId?: IpcClientId
constructor(socketPath: string, log = console.log) {
super()
this._socketPath = socketPath
this._log = log
ipc.config.silent = true
ipc.connectTo("benchmarkServer", this.socketPath, () => {
ipc.of.benchmarkServer?.on("connect", (args) => this.onConnect(args))
ipc.of.benchmarkServer?.on("disconnect", (args) => this.onDisconnect(args))
ipc.of.benchmarkServer?.on("message", (data) => this.onMessage(data))
})
}
private onConnect(args: unknown) {
if (this._isConnected) {
return
}
this.log("[client#onConnect]", args)
this._isConnected = true
this.emit("connect")
}
private onDisconnect(args: unknown) {
if (!this._isConnected) {
return
}
this.log("[client#onDisconnect]", args)
this._isConnected = false
this.emit("disconnect")
}
private onMessage(data: unknown) {
if (typeof data !== "object") {
this._log("[client#onMessage] invalid data", data)
return
}
const result = ipcServerMessageSchema.safeParse(data)
if (!result.success) {
this.log("[client#onMessage] invalid payload", result.error)
return
}
this.emit("message", result.data)
}
private log(...args: unknown[]) {
this._log(...args)
}
public sendMessage(message: IpcClientMessage) {
ipc.of.benchmarkServer?.emit("message", message)
}
public disconnect() {
try {
ipc.disconnect("benchmarkServer")
// @TODO: Should we set _disconnect here?
} catch (error) {
this.log("[client#disconnect] error disconnecting", error)
}
}
public get socketPath() {
return this._socketPath
}
public get clientId() {
return this._clientId
}
public get isConnected() {
return this._isConnected
}
public get isReady() {
return this._isConnected && this._clientId !== undefined
}
}
/**
* IpcServer
*/
export enum IpcServerMessageType {
Ack = "Ack",
TaskEvent = "TaskEvent",
}
export const ipcServerMessageSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal(IpcServerMessageType.Ack),
data: z.object({ clientId: z.string() }),
}),
z.object({
type: z.literal(IpcServerMessageType.TaskEvent),
data: z.object({
eventName: z.string(),
data: z.unknown(),
}),
}),
])
export type IpcServerMessage = z.infer<typeof ipcServerMessageSchema>
type IpcServerEvents = {
connect: [id: IpcClientId]
disconnect: [id: IpcClientId]
message: [data: IpcClientMessage]
}
export class IpcServer extends EventEmitter<IpcServerEvents> {
private readonly _socketPath: string
private readonly _log: (...args: unknown[]) => void
private readonly _clients: Map<string, Socket>
private _isListening = false
constructor(socketPath: string, log = console.log) {
super()
this._socketPath = socketPath
this._log = log
this._clients = new Map()
}
public listen() {
this._isListening = true
ipc.config.silent = true
ipc.serve(this.socketPath, () => {
ipc.server.on("connect", (socket) => this.onConnect(socket))
ipc.server.on("socket.disconnected", (socket) => this.onDisconnect(socket))
ipc.server.on("message", (data) => this.onMessage(data))
})
ipc.server.start()
}
private onConnect(socket: Socket) {
const clientId = crypto.randomBytes(6).toString("hex")
this._clients.set(clientId, socket)
this.log(`[server#onConnect] clientId = ${clientId}, # clients = ${this._clients.size}`)
this.send(socket, { type: IpcServerMessageType.Ack, data: { clientId } })
this.emit("connect", clientId)
}
private onDisconnect(destroyedSocket: Socket) {
let disconnectedClientId: IpcClientId | undefined
for (const [clientId, socket] of this._clients.entries()) {
if (socket === destroyedSocket) {
disconnectedClientId = clientId
this._clients.delete(clientId)
break
}
}
this.log(`[server#socket.disconnected] clientId = ${disconnectedClientId}, # clients = ${this._clients.size}`)
if (disconnectedClientId) {
this.emit("disconnect", disconnectedClientId)
}
}
private onMessage(data: unknown) {
if (typeof data !== "object") {
this.log("[server#onMessage] invalid data", data)
return
}
const result = ipcClientMessageSchema.safeParse(data)
if (!result.success) {
this.log("[server#onMessage] invalid payload", result.error)
return
}
const payload = result.data
this.emit("message", payload)
}
private log(...args: unknown[]) {
this._log(...args)
}
public broadcast(message: IpcServerMessage) {
this.log("[server#broadcast] message =", message)
ipc.server.broadcast("message", message)
}
public send(client: IpcClientId | Socket, message: IpcServerMessage) {
this.log("[server#send] message =", message)
if (typeof client === "string") {
const socket = this._clients.get(client)
if (socket) {
ipc.server.emit(socket, "message", message)
}
} else {
ipc.server.emit(client, "message", message)
}
}
public get socketPath() {
return this._socketPath
}
public get isListening() {
return this._isListening
}
}

View file

@ -1,123 +0,0 @@
import EventEmitter from "node:events"
import ipc from "node-ipc"
import { ClientMessage, ClientMessageType, ServerMessage, ServerMessageType, serverMessageSchema } from "./types.js"
export interface IpcClientEvents {
connect: []
message: [data: ServerMessage]
disconnect: []
}
export class IpcClient extends EventEmitter<IpcClientEvents> {
private readonly _socketPath: string
private readonly _log: (...args: unknown[]) => void
private _isConnected = false
private _clientId?: string
constructor(socketPath: string, log = console.log) {
super()
this._socketPath = socketPath
this._log = log
ipc.config.silent = true
ipc.connectTo("benchmarkServer", this.socketPath, () => {
ipc.of.benchmarkServer?.on("connect", (args) => this.onConnect(args))
ipc.of.benchmarkServer?.on("message", (data) => this.onMessage(data))
ipc.of.benchmarkServer?.on("disconnect", (args) => this.onDisconnect(args))
})
}
private onConnect(args: unknown) {
if (this._isConnected) {
return
}
this.log("[client#onConnect]", args)
this._isConnected = true
this.emit("connect")
}
private onMessage(data: unknown) {
if (typeof data !== "object") {
this._log("[client#onMessage] invalid data", data)
return
}
const result = serverMessageSchema.safeParse(data)
if (!result.success) {
this.log("[client#onMessage] invalid payload", result.error)
return
}
const payload = result.data
switch (payload.type) {
case ServerMessageType.Hello:
this.log(`[client#Hello] ${payload.data.clientId}`)
this._clientId = payload.data.clientId
break
case ServerMessageType.Pong:
this.log(`[client#Pong]`)
break
}
this.emit("message", payload)
}
private onDisconnect(args: unknown) {
if (!this._isConnected) {
return
}
this.log("[client#onDisconnect]", args)
this._isConnected = false
this.emit("disconnect")
}
public sendMessage(message: ClientMessage) {
ipc.of.benchmarkServer?.emit("message", message)
}
private log(...args: unknown[]) {
this._log(...args)
}
public ping() {
if (!this.isReady) {
return false
}
this.sendMessage({ type: ClientMessageType.Ping, data: { clientId: this._clientId! } })
return true
}
public disconnect() {
try {
ipc.disconnect("benchmarkServer")
// @TODO: Should we set _disconnect here?
} catch (error) {
this.log("[client#disconnect] error disconnecting", error)
}
}
public get socketPath() {
return this._socketPath
}
public get clientId() {
return this._clientId
}
public get isConnected() {
return this._isConnected
}
public get isReady() {
return this._isConnected && this._clientId !== undefined
}
}

View file

@ -1,120 +0,0 @@
import { Socket } from "node:net"
import * as crypto from "node:crypto"
import EventEmitter from "node:events"
import ipc from "node-ipc"
import { ClientMessageType, ServerMessage, ServerMessageType, clientMessageSchema } from "./types.js"
type IpcServerEvents = {
client: [id: string]
}
export class IpcServer extends EventEmitter<IpcServerEvents> {
private readonly _socketPath: string
private readonly _log: (...args: unknown[]) => void
private readonly _clients: Map<string, Socket>
private _isListening = false
constructor(socketPath: string, log = console.log) {
super()
this._socketPath = socketPath
this._log = log
this._clients = new Map()
}
public listen() {
this._isListening = true
ipc.config.silent = true
ipc.serve(this.socketPath, () => {
ipc.server.on("connect", (socket) => this.onConnect(socket))
ipc.server.on("message", (data, socket) => this.onMessage(data, socket))
ipc.server.on("socket.disconnected", (socket) => this.onDisconnect(socket))
})
ipc.server.start()
}
private onConnect(socket: Socket) {
const clientId = crypto.randomBytes(6).toString("hex")
this._clients.set(clientId, socket)
this.log(`[server#onConnect] clientId = ${clientId}, # clients = ${this._clients.size}`)
this.send(socket, { type: ServerMessageType.Hello, data: { clientId } })
this.emit("client", clientId)
}
private onMessage(data: unknown, socket: Socket) {
if (typeof data !== "object") {
this.log("[server#onMessage] invalid data", data)
return
}
const result = clientMessageSchema.safeParse(data)
if (!result.success) {
this.log("[server#onMessage] invalid payload", result.error)
return
}
const payload = result.data
switch (payload.type) {
case ClientMessageType.Message:
this.log(`[server#Message] ${payload.data.message}`)
break
case ClientMessageType.Ping:
this.log(`[server#Ping]`)
this.send(socket, { type: ServerMessageType.Pong })
break
}
}
private onDisconnect(destroyedSocket: Socket) {
let destroyedClientId: string | undefined
for (const [clientId, socket] of this._clients.entries()) {
if (socket === destroyedSocket) {
destroyedClientId = clientId
this._clients.delete(clientId)
break
}
}
this.log(`[server#socket.disconnected] clientId = ${destroyedClientId}, # clients = ${this._clients.size}`)
}
private log(...args: unknown[]) {
this._log(...args)
}
public broadcast(message: ServerMessage) {
this.log("[server#broadcast] message =", message)
ipc.server.broadcast("message", message)
}
public send(client: string | Socket, message: ServerMessage) {
this.log("[server#send] message =", message)
if (typeof client === "string") {
const socket = this._clients.get(client)
if (socket) {
ipc.server.emit(socket, "message", message)
}
} else {
ipc.server.emit(client, "message", message)
}
}
public get socketPath() {
return this._socketPath
}
public get isListening() {
return this._isListening
}
}

View file

@ -1,63 +0,0 @@
import { z } from "zod"
/**
* Client
*/
export enum ClientMessageType {
Ping = "ping",
Message = "message",
}
export const clientMessageSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal(ClientMessageType.Message),
data: z.object({
clientId: z.string(),
message: z.string(),
}),
}),
z.object({
type: z.literal(ClientMessageType.Ping),
data: z.object({
clientId: z.string(),
}),
}),
])
export type ClientMessage = z.infer<typeof clientMessageSchema>
/**
* Server
*/
export enum ServerMessageType {
Hello = "hello",
Pong = "pong",
Message = "message",
Data = "data",
}
export const serverMessageSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal(ServerMessageType.Hello),
data: z.object({
clientId: z.string(),
}),
}),
z.object({
type: z.literal(ServerMessageType.Pong),
}),
z.object({
type: z.literal(ServerMessageType.Message),
data: z.object({
message: z.string(),
}),
}),
z.object({
type: z.literal(ServerMessageType.Data),
data: z.unknown(),
}),
])
export type ServerMessage = z.infer<typeof serverMessageSchema>

330
package-lock.json generated
View file

@ -41,6 +41,7 @@
"js-tiktoken": "^1.0.19",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"node-ipc": "^12.0.0",
"openai": "^4.78.1",
"os-name": "^6.0.0",
"p-wait-for": "^5.0.2",
@ -74,6 +75,7 @@
"@types/glob": "^8.1.0",
"@types/jest": "^29.5.14",
"@types/node": "20.x",
"@types/node-ipc": "^9.2.3",
"@types/string-similarity": "^4.0.2",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.11.0",
@ -6113,6 +6115,16 @@
"form-data": "^4.0.0"
}
},
"node_modules/@types/node-ipc": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.3.tgz",
"integrity": "sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/pdf-parse": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.4.tgz",
@ -6846,8 +6858,7 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/bare-events": {
"version": "2.5.0",
@ -7521,8 +7532,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/console-control-strings": {
"version": "1.1.0",
@ -7574,6 +7584,176 @@
"node": ">=6.6.0"
}
},
"node_modules/copyfiles": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz",
"integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==",
"license": "MIT",
"dependencies": {
"glob": "^7.0.5",
"minimatch": "^3.0.3",
"mkdirp": "^1.0.4",
"noms": "0.0.0",
"through2": "^2.0.1",
"untildify": "^4.0.0",
"yargs": "^16.1.0"
},
"bin": {
"copyfiles": "copyfiles",
"copyup": "copyfiles"
}
},
"node_modules/copyfiles/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/copyfiles/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/copyfiles/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/copyfiles/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/copyfiles/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/copyfiles/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/copyfiles/node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/copyfiles/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/copyfiles/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/copyfiles/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/copyfiles/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"license": "MIT",
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/copyfiles/node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@ -8041,6 +8221,15 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/easy-stack": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz",
"integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/easy-table": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.2.0.tgz",
@ -8668,6 +8857,28 @@
"node": ">= 0.6"
}
},
"node_modules/event-pubsub": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-5.0.3.tgz",
"integrity": "sha512-2QiHxshejKgJrYMzSI9MEHrvhmzxBL+eLyiM5IiyjDBySkgwS2+tdtnO3gbx8pEisu/yOFCIhfCb63gCEu0yBQ==",
"license": "MIT",
"dependencies": {
"copyfiles": "^2.4.0",
"strong-type": "^0.1.3"
},
"engines": {
"node": ">=13.0.0"
}
},
"node_modules/event-pubsub/node_modules/strong-type": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/strong-type/-/strong-type-0.1.6.tgz",
"integrity": "sha512-eJe5caH6Pi5oMMeQtIoBPpvNu/s4jiyb63u5tkHNnQXomK+puyQ5i+Z5iTLBr/xUz/pIcps0NSfzzFI34+gAXg==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
@ -9299,8 +9510,7 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.3",
@ -10045,7 +10255,6 @@
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@ -11395,6 +11604,27 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-message": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
"integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==",
"license": "MIT",
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/js-queue": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz",
"integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==",
"license": "MIT",
"dependencies": {
"easy-stack": "^1.0.1"
},
"engines": {
"node": ">=1.0.0"
}
},
"node_modules/js-tiktoken": {
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.19.tgz",
@ -12567,12 +12797,61 @@
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
"dev": true
},
"node_modules/node-ipc": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-12.0.0.tgz",
"integrity": "sha512-QHJ2gAJiqA3cM7cQiRjLsfCOBRB0TwQ6axYD4FSllQWipEbP6i7Se1dP8EzPKk5J1nCe27W69eqPmCoKyQ61Vg==",
"license": "MIT",
"dependencies": {
"event-pubsub": "5.0.3",
"js-message": "1.0.7",
"js-queue": "2.0.2",
"strong-type": "^1.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
"dev": true
},
"node_modules/noms": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz",
"integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==",
"license": "ISC",
"dependencies": {
"inherits": "^2.0.1",
"readable-stream": "~1.0.31"
}
},
"node_modules/noms/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"license": "MIT"
},
"node_modules/noms/node_modules/readable-stream": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/noms/node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
"license": "MIT"
},
"node_modules/normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@ -14951,6 +15230,15 @@
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
},
"node_modules/strong-type": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strong-type/-/strong-type-1.1.0.tgz",
"integrity": "sha512-X5Z6riticuH5GnhUyzijfDi1SoXas8ODDyN7K8lJeQK+Jfi4dKdoJGL4CXTskY/ATBcN+rz5lROGn1tAUkOX7g==",
"license": "MIT",
"engines": {
"node": ">=12.21.0"
}
},
"node_modules/summary": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/summary/-/summary-2.1.0.tgz",
@ -15089,6 +15377,16 @@
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
},
"node_modules/through2": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
"integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
"license": "MIT",
"dependencies": {
"readable-stream": "~2.3.6",
"xtend": "~4.0.1"
}
},
"node_modules/tmp": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
@ -15463,6 +15761,15 @@
"node": ">= 0.8"
}
},
"node_modules/untildify": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
"integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
@ -16062,6 +16369,15 @@
"node": ">=4.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View file

@ -370,6 +370,7 @@
"js-tiktoken": "^1.0.19",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"node-ipc": "^12.0.0",
"openai": "^4.78.1",
"os-name": "^6.0.0",
"p-wait-for": "^5.0.2",
@ -403,6 +404,7 @@
"@types/glob": "^8.1.0",
"@types/jest": "^29.5.14",
"@types/node": "20.x",
"@types/node-ipc": "^9.2.3",
"@types/string-similarity": "^4.0.2",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.11.0",

View file

@ -91,9 +91,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
private contextProxy: ContextProxy
configManager: ConfigManager
customModesManager: CustomModesManager
get cwd() {
return getWorkspacePath()
}
constructor(
readonly context: vscode.ExtensionContext,
private readonly outputChannel: vscode.OutputChannel,
@ -2745,6 +2743,12 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
await this.contextProxy.setValues(values)
}
// cwd
get cwd() {
return getWorkspacePath()
}
// dev
async resetState() {

View file

@ -5,14 +5,16 @@ import { ClineProvider } from "../core/webview/ClineProvider"
import { RooCodeAPI, RooCodeEvents, ConfigurationValues, TokenUsage } from "./roo-code"
import { MessageHistory } from "./message-history"
import { IpcClientMessageType, IpcServerMessageType, IpcServer } from "./ipc"
export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
private readonly outputChannel: vscode.OutputChannel
private readonly provider: ClineProvider
private readonly history: MessageHistory
private readonly tokenUsage: Record<string, TokenUsage>
private readonly ipc?: IpcServer
constructor(outputChannel: vscode.OutputChannel, provider: ClineProvider) {
constructor(outputChannel: vscode.OutputChannel, provider: ClineProvider, socketPath?: string) {
super()
this.outputChannel = outputChannel
@ -41,6 +43,30 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
})
this.on("taskTokenUsageUpdated", (taskId, usage) => (this.tokenUsage[taskId] = usage))
if (socketPath) {
this.ipc = new IpcServer(socketPath)
this.ipc.listen()
this.ipc.on("message", (message) => {
switch (message.type) {
case IpcClientMessageType.StartNewTask:
this.startNewTask(message.data.text, message.data.images)
break
}
})
}
}
public override emit<K extends keyof RooCodeEvents>(
eventName: K,
...args: K extends keyof RooCodeEvents ? RooCodeEvents[K] : never
) {
if (this.ipc) {
this.ipc.broadcast({ type: IpcServerMessageType.TaskEvent, data: { eventName, data: { ...args } } })
}
return super.emit(eventName, ...args)
}
public async startNewTask(text?: string, images?: string[]) {

262
src/exports/ipc.ts Normal file
View file

@ -0,0 +1,262 @@
import EventEmitter from "node:events"
import { Socket } from "node:net"
import * as crypto from "node:crypto"
import ipc from "node-ipc"
import { z } from "zod"
/**
* IpcClient
*/
export type IpcClientId = string
export enum IpcClientMessageType {
Message = "Message",
StartNewTask = "StartNewTask",
}
export const ipcClientMessageSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal(IpcClientMessageType.StartNewTask),
data: z.object({
text: z.string(),
images: z.array(z.string()).optional(),
}),
}),
])
export type IpcClientMessage = z.infer<typeof ipcClientMessageSchema>
export interface IpcClientEvents {
connect: []
disconnect: []
message: [data: IpcServerMessage]
}
export class IpcClient extends EventEmitter<IpcClientEvents> {
private readonly _socketPath: string
private readonly _log: (...args: unknown[]) => void
private _isConnected = false
private _clientId?: IpcClientId
constructor(socketPath: string, log = console.log) {
super()
this._socketPath = socketPath
this._log = log
ipc.config.silent = true
ipc.connectTo("benchmarkServer", this.socketPath, () => {
ipc.of.benchmarkServer?.on("connect", (args) => this.onConnect(args))
ipc.of.benchmarkServer?.on("disconnect", (args) => this.onDisconnect(args))
ipc.of.benchmarkServer?.on("message", (data) => this.onMessage(data))
})
}
private onConnect(args: unknown) {
if (this._isConnected) {
return
}
this.log("[client#onConnect]", args)
this._isConnected = true
this.emit("connect")
}
private onDisconnect(args: unknown) {
if (!this._isConnected) {
return
}
this.log("[client#onDisconnect]", args)
this._isConnected = false
this.emit("disconnect")
}
private onMessage(data: unknown) {
if (typeof data !== "object") {
this._log("[client#onMessage] invalid data", data)
return
}
const result = ipcServerMessageSchema.safeParse(data)
if (!result.success) {
this.log("[client#onMessage] invalid payload", result.error)
return
}
this.emit("message", result.data)
}
private log(...args: unknown[]) {
this._log(...args)
}
public sendMessage(message: IpcClientMessage) {
ipc.of.benchmarkServer?.emit("message", message)
}
public disconnect() {
try {
ipc.disconnect("benchmarkServer")
// @TODO: Should we set _disconnect here?
} catch (error) {
this.log("[client#disconnect] error disconnecting", error)
}
}
public get socketPath() {
return this._socketPath
}
public get clientId() {
return this._clientId
}
public get isConnected() {
return this._isConnected
}
public get isReady() {
return this._isConnected && this._clientId !== undefined
}
}
/**
* IpcServer
*/
export enum IpcServerMessageType {
Ack = "Ack",
TaskEvent = "TaskEvent",
}
export const ipcServerMessageSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal(IpcServerMessageType.Ack),
data: z.object({ clientId: z.string() }),
}),
z.object({
type: z.literal(IpcServerMessageType.TaskEvent),
data: z.object({
eventName: z.string(),
data: z.unknown(),
}),
}),
])
export type IpcServerMessage = z.infer<typeof ipcServerMessageSchema>
type IpcServerEvents = {
connect: [id: IpcClientId]
disconnect: [id: IpcClientId]
message: [data: IpcClientMessage]
}
export class IpcServer extends EventEmitter<IpcServerEvents> {
private readonly _socketPath: string
private readonly _log: (...args: unknown[]) => void
private readonly _clients: Map<string, Socket>
private _isListening = false
constructor(socketPath: string, log = console.log) {
super()
this._socketPath = socketPath
this._log = log
this._clients = new Map()
}
public listen() {
this._isListening = true
ipc.config.silent = true
ipc.serve(this.socketPath, () => {
ipc.server.on("connect", (socket) => this.onConnect(socket))
ipc.server.on("socket.disconnected", (socket) => this.onDisconnect(socket))
ipc.server.on("message", (data) => this.onMessage(data))
})
ipc.server.start()
}
private onConnect(socket: Socket) {
const clientId = crypto.randomBytes(6).toString("hex")
this._clients.set(clientId, socket)
this.log(`[server#onConnect] clientId = ${clientId}, # clients = ${this._clients.size}`)
this.send(socket, { type: IpcServerMessageType.Ack, data: { clientId } })
this.emit("connect", clientId)
}
private onDisconnect(destroyedSocket: Socket) {
let disconnectedClientId: IpcClientId | undefined
for (const [clientId, socket] of this._clients.entries()) {
if (socket === destroyedSocket) {
disconnectedClientId = clientId
this._clients.delete(clientId)
break
}
}
this.log(`[server#socket.disconnected] clientId = ${disconnectedClientId}, # clients = ${this._clients.size}`)
if (disconnectedClientId) {
this.emit("disconnect", disconnectedClientId)
}
}
private onMessage(data: unknown) {
if (typeof data !== "object") {
this.log("[server#onMessage] invalid data", data)
return
}
const result = ipcClientMessageSchema.safeParse(data)
if (!result.success) {
this.log("[server#onMessage] invalid payload", result.error)
return
}
const payload = result.data
this.emit("message", payload)
}
private log(...args: unknown[]) {
this._log(...args)
}
public broadcast(message: IpcServerMessage) {
this.log("[server#broadcast] message =", message)
ipc.server.broadcast("message", message)
}
public send(client: IpcClientId | Socket, message: IpcServerMessage) {
this.log("[server#send] message =", message)
if (typeof client === "string") {
const socket = this._clients.get(client)
if (socket) {
ipc.server.emit(socket, "message", message)
}
} else {
ipc.server.emit(client, "message", message)
}
}
public get socketPath() {
return this._socketPath
}
public get isListening() {
return this._isListening
}
}

View file

@ -111,7 +111,7 @@ export function activate(context: vscode.ExtensionContext) {
registerTerminalActions(context)
// Implements the `RooCodeAPI` interface.
return new API(outputChannel, provider)
return new API(outputChannel, provider, "/tmp/roo-code-ipc.sock")
}
// This method is called when your extension is deactivated