Examples
The same small program three times: start Saiko, find a client, inject, run a script. Each one is a starting point, not a framework.
Node.js
Uses the WebSocket server through the ws package. Node's built-in client also works as long as it sends no Origin.
import { spawn } from 'node:child_process'
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import WebSocket from 'ws' // npm i ws
// 1. start Saiko and wait for it to say it is ready
const saiko = spawn('C:\\Saiko\\saiko.exe', ['--custom'], { stdio: ['ignore', 'pipe', 'inherit'] })
await new Promise((resolve, reject) => {
saiko.stdout.on('data', (chunk) => {
for (const line of chunk.toString().split('\n').filter(Boolean)) {
const message = JSON.parse(line)
if (message.event === 'ready' || message.event === 'already_running') resolve()
else if (message.ok === false) reject(new Error(message.error))
}
})
})
// 2. find the server
const file = join(process.env.APPDATA, 'io.saiko.workspace', 'control.json')
const { port, token } = JSON.parse(await readFile(file, 'utf8'))
const socket = new WebSocket(`ws://127.0.0.1:${port}/`)
await new Promise((resolve) => socket.once('open', resolve))
// 3. one helper: send a request, wait for its reply
let counter = 0
const pending = new Map()
socket.on('message', (data) => {
const reply = JSON.parse(data.toString())
pending.get(reply.id)?.(reply)
})
const call = (cmd, extra = {}) =>
new Promise((resolve, reject) => {
const id = ++counter
pending.set(id, (reply) => (reply.ok ? resolve(reply.result) : reject(new Error(reply.error))))
socket.send(JSON.stringify({ token, id, cmd, ...extra }))
})
const clients = await call('clients')
console.log(clients) // [{ pid: 14212 }]
await call('inject', { pid: clients[0].pid })
await call('execute', { pid: clients[0].pid, script: 'print("hello from Node")' })
socket.close()Python
import json
import os
import subprocess
from websockets.sync.client import connect # pip install websockets
# 1. start Saiko and read its first line
saiko = subprocess.Popen([r"C:\Saiko\saiko.exe", "--custom"], stdout=subprocess.PIPE, text=True)
first = json.loads(saiko.stdout.readline())
if first.get("ok") is False:
raise SystemExit(first["error"])
# 2. find the server
path = os.path.join(os.environ["APPDATA"], "io.saiko.workspace", "control.json")
with open(path) as f:
control = json.load(f)
# 3. talk to it
with connect(f"ws://127.0.0.1:{control['port']}/") as socket:
def call(cmd, **extra):
socket.send(json.dumps({"token": control["token"], "cmd": cmd, **extra}))
reply = json.loads(socket.recv())
if not reply["ok"]:
raise RuntimeError(reply["error"])
return reply["result"]
clients = call("clients")
print(clients)
call("inject", pid=clients[0]["pid"])
call("execute", pid=clients[0]["pid"], script='print("hello from Python")')PowerShell
PowerShell has no WebSocket client worth the trouble here, and does not need one: the command line already gives you JSON.
# Piping the output makes PowerShell wait for the windowed program to finish.
$saiko = "C:\Saiko\saiko.exe"
# start it in the background (this call keeps running, so do not wait for it)
Start-Process $saiko -ArgumentList "--custom" -RedirectStandardOutput "$env:TEMP\saiko-custom.log"
while (-not (Select-String -Path "$env:TEMP\saiko-custom.log" -Pattern '"event":"ready"' -Quiet)) {
Start-Sleep -Milliseconds 500
}
$clients = & $saiko --get-clients | ConvertFrom-Json
if (-not $clients) { throw "Roblox is not running" }
$pid_ = $clients[0].pid
& $saiko --pid $pid_ --inject | ConvertFrom-Json
'print("hello from PowerShell")' | & $saiko --pid $pid_ --execute - | ConvertFrom-Json