@struktoai/mirage-cli 0.0.7-alpha.2 → 0.0.7

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/credentials.ts","../src/settings.ts","../src/env.ts","../src/client.ts"],"sourcesContent":["// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport {\n existsSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n rmSync,\n writeSync,\n closeSync,\n} from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { mirageHome } from '@struktoai/mirage-server/paths'\n\nconst LOGIN_FILE = 'login.json'\nexport const LOGIN_MAX_AGE_SECONDS = 30 * 24 * 60 * 60\nconst REFRESH_MARGIN_SECONDS = 60\n\n/** The login cannot give a token: it is too old, or the issuer refused to refresh it. */\nexport class LoginError extends Error {\n override readonly name = 'LoginError'\n}\n\n/**\n * What `mirage login` keeps, for the one server it logged in to: the\n * server the token is for (it goes nowhere else), the bearer token, when\n * the user signed in (the login ends 30 days later, however often it was\n * refreshed), the refresh token (null for a pasted token), when the\n * access token ends if known, the OAuth client that signed in, and where\n * a refresh goes.\n */\nexport interface Login {\n url: string\n access_token: string\n logged_in_at: number\n refresh_token: string | null\n expires_at: number | null\n client_id: string | null\n token_endpoint: string | null\n}\n\n/** When the user has to log in again. */\nexport function renewBy(login: Login): number {\n const ends = login.logged_in_at + LOGIN_MAX_AGE_SECONDS\n if (login.refresh_token === null && login.expires_at !== null) {\n return Math.min(ends, login.expires_at)\n }\n return ends\n}\n\nexport function loginPath(env: Record<string, string | undefined> = process.env): string {\n return join(mirageHome(env), LOGIN_FILE)\n}\n\n/** The stored login, or null when there is none. */\nexport function readLogin(path: string = loginPath()): Login | null {\n if (!existsSync(path)) return null\n return JSON.parse(readFileSync(path, 'utf-8')) as Login\n}\n\n/** Store a login, readable by this user only. */\nexport function writeLogin(login: Login, path: string = loginPath()): void {\n mkdirSync(dirname(path), { recursive: true })\n const staged = join(dirname(path), `.${LOGIN_FILE}.${String(process.pid)}`)\n const fd = openSync(staged, 'w', 0o600)\n try {\n writeSync(fd, JSON.stringify(login, null, 2))\n } finally {\n closeSync(fd)\n }\n renameSync(staged, path)\n}\n\n/** Forget the stored login; returns the login removed, or null when there was none. */\nexport function removeLogin(path: string = loginPath()): Login | null {\n const login = readLogin(path)\n rmSync(path, { force: true })\n return login\n}\n\n/** A JWT's claims, unchecked, to show and to time; empty when it is not a JWT. */\nexport function tokenClaims(token: string): Record<string, unknown> {\n const parts = token.split('.')\n if (parts.length !== 3) return {}\n try {\n const claims: unknown = JSON.parse(Buffer.from(parts[1] ?? '', 'base64url').toString('utf-8'))\n return typeof claims === 'object' && claims !== null && !Array.isArray(claims)\n ? (claims as Record<string, unknown>)\n : {}\n } catch {\n return {}\n }\n}\n\n/** An OAuth endpoint's JSON object, or empty when it sent none. */\nexport async function jsonBody(reply: Response): Promise<Record<string, unknown>> {\n try {\n const body: unknown = await reply.json()\n return typeof body === 'object' && body !== null && !Array.isArray(body)\n ? (body as Record<string, unknown>)\n : {}\n } catch {\n return {}\n }\n}\n\n/** When a token endpoint's access token ends, or null when it gave no `expires_in`. */\nexport function endsAt(body: Record<string, unknown>, now: number): number | null {\n const expiresIn = body.expires_in\n return typeof expiresIn === 'number' && expiresIn > 0 ? now + expiresIn : null\n}\n\n/**\n * The login's access token, refreshed first when it is about to end;\n * `login` is updated in place and stored at `path` on a refresh. Throws\n * `LoginError` when the login is over 30 days old, its token ended with\n * nothing to refresh it, or the issuer refused the refresh.\n */\nexport async function freshToken(login: Login, path: string = loginPath()): Promise<string> {\n const now = Date.now() / 1000\n const again = `run \\`mirage login\\` to log in to ${login.url} again`\n if (now >= login.logged_in_at + LOGIN_MAX_AGE_SECONDS) {\n throw new LoginError(`the login is 30 days old; ${again}`)\n }\n if (login.expires_at === null || now < login.expires_at - REFRESH_MARGIN_SECONDS) {\n return login.access_token\n }\n if (login.refresh_token === null || login.token_endpoint === null || login.client_id === null) {\n throw new LoginError(`the token has expired; ${again}`)\n }\n let reply: Response\n try {\n reply = await fetch(login.token_endpoint, {\n method: 'POST',\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: login.refresh_token,\n client_id: login.client_id,\n }),\n signal: AbortSignal.timeout(30_000),\n })\n } catch (error) {\n throw new LoginError(`could not reach the issuer: ${String(error)}`)\n }\n const body = await jsonBody(reply)\n if (reply.status !== 200 || typeof body.access_token !== 'string') {\n const reason = typeof body.error === 'string' ? body.error : String(reply.status)\n throw new LoginError(`the issuer refused to refresh the login (${reason}); ${again}`)\n }\n login.access_token = body.access_token\n if (typeof body.refresh_token === 'string' && body.refresh_token !== '') {\n login.refresh_token = body.refresh_token\n }\n login.expires_at = endsAt(body, now)\n writeLogin(login, path)\n return login.access_token\n}\n","// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { rstripSlash } from '@struktoai/mirage-core/utils/slash'\nimport { compareCodePoints } from '@struktoai/mirage-core/utils/sort'\nimport { CONFIG_ENV_KEYS } from '@struktoai/mirage-server/auth/config'\nimport { defaultTokenFile, readTokenFile } from '@struktoai/mirage-server/auth/storage'\nimport {\n ENV_ALLOWED_HOSTS,\n ENV_DAEMON_PORT,\n ENV_IDLE_GRACE_SECONDS,\n} from '@struktoai/mirage-server/env'\nimport {\n ALLOWED_KEYS,\n DaemonConfigError,\n NUMERIC_KEYS,\n parseDaemonTable,\n readDaemonTable,\n} from '@struktoai/mirage-server/daemon_config'\nimport { DEFAULT_ALLOWED_HOSTS } from '@struktoai/mirage-server/host_validation_constants'\nimport { mirageHome } from '@struktoai/mirage-server/paths'\nimport {\n AUTHORIZED_KEYS_NAME,\n DEFAULT_SSH_HOST,\n HOST_KEY_NAME,\n SSH_ENV_KEYS,\n} from '@struktoai/mirage-server/ssh/constants'\nimport { defaultSSHDir } from '@struktoai/mirage-server/ssh/config'\n\nimport { loginPath, readLogin, type Login } from './credentials.ts'\nimport { ENV_DAEMON_URL, ENV_TOKEN } from './env.ts'\n\nexport const DEFAULT_DAEMON_URL = 'http://127.0.0.1:8765'\n\n/**\n * Whether a daemon URL points at this machine. Only such a URL may use\n * the local token file or start a daemon, so neither the file's token\n * nor a spawn ever goes to a remote host.\n *\n * @param url - The daemon URL.\n * @returns True when its host is a loopback name.\n */\nexport function isLocalUrl(url: string): boolean {\n let host: string\n try {\n host = new URL(url).hostname\n } catch {\n return false\n }\n return DEFAULT_ALLOWED_HOSTS.includes(host.replace(/^\\[(.*)\\]$/, '$1'))\n}\n\nexport interface DaemonSettings {\n url: string\n authToken: string\n idleGraceSeconds: number\n /** The `mirage login` made for this URL, used when no token is set. */\n login?: Login\n}\n\nexport interface LoadOptions {\n env?: Record<string, string | undefined>\n configPath?: string\n tokenFile?: string\n}\n\nexport function loadDaemonSettings(options: LoadOptions = {}): DaemonSettings {\n const env = options.env ?? (process.env as Record<string, string | undefined>)\n const table =\n options.configPath !== undefined\n ? existsSync(options.configPath)\n ? parseDaemonTable(readFileSync(options.configPath, 'utf-8'))\n : {}\n : readDaemonTable(mirageHome(env))\n const settings: DaemonSettings = {\n url: table.url ?? DEFAULT_DAEMON_URL,\n authToken: table.auth_token ?? '',\n idleGraceSeconds: Number(table.idle_grace_seconds ?? '30'),\n }\n const envUrl = env[ENV_DAEMON_URL]\n if (envUrl !== undefined && envUrl !== '') {\n settings.url = envUrl\n }\n const envToken = env[ENV_TOKEN]\n if (envToken !== undefined && envToken !== '') {\n settings.authToken = envToken\n }\n if (settings.authToken === '') {\n const login = readLogin(loginPath(env))\n if (login !== null && login.url === rstripSlash(settings.url)) {\n settings.login = login\n return settings\n }\n }\n if (settings.authToken === '' && isLocalUrl(settings.url)) {\n const fileToken = readTokenFile(options.tokenFile ?? defaultTokenFile(env))\n if (fileToken !== undefined && fileToken !== '') {\n settings.authToken = fileToken\n }\n }\n return settings\n}\n\nfunction configPath(env: Record<string, string | undefined> = process.env): string {\n return join(mirageHome(env), 'config.toml')\n}\n\nconst ENV_FOR_KEY: Record<string, string> = {\n url: ENV_DAEMON_URL,\n allowed_hosts: ENV_ALLOWED_HOSTS,\n ...CONFIG_ENV_KEYS,\n auth_token: ENV_TOKEN,\n idle_grace_seconds: ENV_IDLE_GRACE_SECONDS,\n port: ENV_DAEMON_PORT,\n ...SSH_ENV_KEYS,\n}\n\nfunction defaultForKey(key: string, home: string): string {\n const defaults: Record<string, string> = {\n url: DEFAULT_DAEMON_URL,\n allowed_hosts: DEFAULT_ALLOWED_HOSTS.join(','),\n auth_mode: 'local',\n jwt_alg: '',\n jwt_issuer: '',\n jwt_audience: '',\n jwt_pubkey_file: '',\n jwt_jwks_url: '',\n jwt_clock_skew: '5',\n jwt_authorized_parties: '',\n login_client_id: '',\n socket: '',\n auth_token: '',\n idle_grace_seconds: '30',\n port: '8765',\n ssh_port: '',\n ssh_host: DEFAULT_SSH_HOST,\n ssh_host_key_file: join(defaultSSHDir(home), HOST_KEY_NAME),\n ssh_authorized_keys: join(defaultSSHDir(home), AUTHORIZED_KEYS_NAME),\n }\n return defaults[key] ?? ''\n}\n\nexport function resolvedConfig(\n env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,\n): Record<string, [string, string]> {\n const home = mirageHome(env)\n const table = readDaemonTable(home)\n const out: Record<string, [string, string]> = {}\n for (const key of [...ALLOWED_KEYS].sort(compareCodePoints)) {\n const envName = ENV_FOR_KEY[key]\n if (envName !== undefined) {\n const envValue = env[envName]\n if (envValue !== undefined && envValue !== '') {\n out[key] = [envValue, `env ${envName}`]\n continue\n }\n }\n const fileValue = table[key]\n if (fileValue !== undefined && fileValue !== '') {\n out[key] = [fileValue, 'file']\n } else {\n out[key] = [defaultForKey(key, home), 'default']\n }\n }\n return out\n}\n\nfunction checkKey(key: string): void {\n if (!ALLOWED_KEYS.has(key)) {\n throw new DaemonConfigError(\n `unknown config key: '${key}'; allowed: ${[...ALLOWED_KEYS].sort(compareCodePoints).join(', ')}`,\n )\n }\n}\n\nfunction formatValue(key: string, value: string): string {\n if (NUMERIC_KEYS.has(key)) return value\n return `\"${value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`\n}\n\nexport function listConfig(path?: string): Record<string, string> {\n const p = path ?? configPath(process.env as Record<string, string | undefined>)\n if (!existsSync(p)) return {}\n return parseDaemonTable(readFileSync(p, 'utf-8'))\n}\n\nexport function getConfig(key: string, path?: string): string | undefined {\n checkKey(key)\n return listConfig(path)[key]\n}\n\nexport function setConfig(key: string, value: string, path?: string): void {\n checkKey(key)\n const p = path ?? configPath(process.env as Record<string, string | undefined>)\n const lines = existsSync(p) ? readFileSync(p, 'utf-8').split('\\n') : []\n if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()\n const rendered = `${key} = ${formatValue(key, value)}`\n let headerIdx = -1\n for (let i = 0; i < lines.length; i++) {\n if ((lines[i] ?? '').trim() === '[daemon]') {\n headerIdx = i\n break\n }\n }\n if (headerIdx < 0) {\n if (lines.length > 0 && (lines[lines.length - 1] ?? '').trim() !== '') lines.push('')\n lines.push('[daemon]', rendered)\n } else {\n let end = lines.length\n for (let i = headerIdx + 1; i < lines.length; i++) {\n if ((lines[i] ?? '').trim().startsWith('[')) {\n end = i\n break\n }\n }\n let replaced = false\n for (let i = headerIdx + 1; i < end; i++) {\n const t = (lines[i] ?? '').trim()\n if (t.startsWith('#') || !t.includes('=')) continue\n if (t.slice(0, t.indexOf('=')).trim() === key) {\n lines[i] = rendered\n replaced = true\n break\n }\n }\n if (!replaced) lines.splice(end, 0, rendered)\n }\n mkdirSync(dirname(p), { recursive: true })\n writeFileSync(p, lines.join('\\n') + '\\n')\n chmodSync(p, 0o600)\n}\n\nexport function unsetConfig(key: string, path?: string): void {\n const p = path ?? configPath(process.env as Record<string, string | undefined>)\n if (!existsSync(p)) return\n const lines = readFileSync(p, 'utf-8').split('\\n')\n if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()\n const kept: string[] = []\n let inDaemon = false\n for (const line of lines) {\n const t = line.trim()\n if (t === '[daemon]') {\n inDaemon = true\n kept.push(line)\n continue\n }\n if (t.startsWith('[')) inDaemon = false\n if (\n inDaemon &&\n t.includes('=') &&\n !t.startsWith('#') &&\n t.slice(0, t.indexOf('=')).trim() === key\n )\n continue\n kept.push(line)\n }\n writeFileSync(p, kept.join('\\n') + '\\n')\n chmodSync(p, 0o600)\n}\n","// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nexport { ENV_AUTH_MODE, ENV_AUTH_TOKEN } from '@struktoai/mirage-server/auth/config'\nexport { ENV_DAEMON_PORT, ENV_IDLE_GRACE_SECONDS } from '@struktoai/mirage-server/env'\n\nexport const ENV_DAEMON_URL = 'MIRAGE_DAEMON_URL'\nexport const ENV_TOKEN = 'MIRAGE_TOKEN'\n","// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { spawn, type ChildProcess } from 'node:child_process'\nimport { randomUUID } from 'node:crypto'\nimport { existsSync, mkdirSync, openSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { connect } from 'node:net'\nimport { join } from 'node:path'\nimport { AuthMode } from '@struktoai/mirage-server/auth/config'\nimport { defaultTokenFile, ensureTokenFile } from '@struktoai/mirage-server/auth/storage'\nimport { readDaemonTable, validateDaemonTable } from '@struktoai/mirage-server/daemon_config'\nimport { mirageHome } from '@struktoai/mirage-server/paths'\n\nimport { ENV_AUTH_MODE, ENV_AUTH_TOKEN, ENV_DAEMON_PORT, ENV_IDLE_GRACE_SECONDS } from './env.ts'\nimport { LoginError, freshToken, readLogin } from './credentials.ts'\nimport { isLocalUrl, type DaemonSettings } from './settings.ts'\n\nconst requireFromHere = createRequire(import.meta.url)\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 60_000\n\n/** The daemon is down and may not be spawned, or a spawned one never answered. */\nexport class DaemonUnreachable extends Error {\n override readonly name = 'DaemonUnreachable'\n}\n\nexport class DaemonClient {\n readonly settings: DaemonSettings\n private refreshing: Promise<string> | undefined\n /** The last login token this client sent. */\n held = ''\n\n constructor(settings: DaemonSettings) {\n this.settings = settings\n }\n\n /**\n * The bearer token to send: the settings' own, else the login's,\n * refreshed when it is about to end; empty when there is none. The client\n * stays bound to the login it started with. That login is read from its\n * file each time, so a refresh made by another process is shared, while\n * `mirage logout` or a new login stops this one; requests sent at once\n * share one read, so they refresh it once. The token sent is kept as\n * `held`. Throws `LoginError` when the login ended or changed, or cannot\n * give a token.\n */\n async token(): Promise<string> {\n const login = this.settings.login\n if (this.settings.authToken !== '' || login === undefined) {\n this.held = this.settings.authToken\n return this.held\n }\n this.refreshing ??= (async () => {\n const stored = readLogin()\n if (stored?.url !== login.url) {\n throw new LoginError(`not logged in to ${login.url} any more; run \\`mirage login\\``)\n }\n if (stored.logged_in_at !== login.logged_in_at) {\n throw new LoginError(`the login to ${login.url} changed; run the command again`)\n }\n this.held = await freshToken(stored)\n return this.held\n })().finally(() => {\n this.refreshing = undefined\n })\n return this.refreshing\n }\n\n private async headers(extra?: Record<string, string>): Promise<Record<string, string>> {\n const h: Record<string, string> = { ...extra }\n const token = await this.token()\n if (token !== '') h.Authorization = `Bearer ${token}`\n return h\n }\n\n async request(\n method: string,\n path: string,\n init: RequestInit & { timeoutMs?: number | null } = {},\n ): Promise<Response> {\n const { timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, ...rest } = init\n const headers: Record<string, string> = {\n ...(await this.headers()),\n ...((rest.headers ?? {}) as Record<string, string>),\n }\n if (rest.body !== undefined && headers['Content-Type'] === undefined) {\n headers['Content-Type'] = 'application/json'\n }\n if (timeoutMs === null) return fetch(this.settings.url + path, { ...rest, method, headers })\n const ctrl = new AbortController()\n const t = setTimeout(() => {\n ctrl.abort()\n }, timeoutMs)\n try {\n return await fetch(this.settings.url + path, {\n ...rest,\n method,\n headers,\n signal: ctrl.signal,\n })\n } finally {\n clearTimeout(t)\n }\n }\n\n /**\n * Send a JSON `request` part and then `part`, streamed from its data\n * as it is read, as one multipart body. There is no overall timeout,\n * since the upload lasts as long as its input; `signal` aborts it.\n */\n async requestUpload(\n method: string,\n path: string,\n request: Record<string, unknown>,\n part: { name: string; data: AsyncIterable<Uint8Array> },\n signal?: AbortSignal,\n ): Promise<Response> {\n const boundary = `mirage-${randomUUID()}`\n const text = new TextEncoder()\n const head = text.encode(\n `--${boundary}\\r\\nContent-Disposition: form-data; name=\"request\"; filename=\"request.json\"\\r\\n` +\n `Content-Type: application/json\\r\\n\\r\\n${JSON.stringify(request)}\\r\\n` +\n `--${boundary}\\r\\nContent-Disposition: form-data; name=\"${part.name}\"; filename=\"${part.name}.bin\"\\r\\n` +\n 'Content-Type: application/octet-stream\\r\\n\\r\\n',\n )\n const tail = text.encode(`\\r\\n--${boundary}--\\r\\n`)\n async function* parts(): AsyncGenerator<Uint8Array> {\n yield head\n for await (const chunk of part.data) yield chunk\n yield tail\n }\n return fetch(this.settings.url + path, {\n method,\n headers: await this.headers({\n 'Content-Type': `multipart/form-data; boundary=${boundary}`,\n }),\n body: ReadableStream.from(parts()),\n duplex: 'half',\n ...(signal !== undefined ? { signal } : {}),\n } as RequestInit)\n }\n\n async isReachable(timeoutMs = 500): Promise<boolean> {\n const headers = await this.headers()\n const ctrl = new AbortController()\n const t = setTimeout(() => {\n ctrl.abort()\n }, timeoutMs)\n try {\n const r = await fetch(this.settings.url + '/v1/health', {\n headers,\n signal: ctrl.signal,\n })\n return r.status === 200\n } catch {\n return false\n } finally {\n clearTimeout(t)\n }\n }\n\n /**\n * Ensure the daemon is reachable, optionally spawning it. `timeoutMs` is\n * how long a spawned daemon has to answer `/v1/health`: a first start\n * loads the whole server. A daemon that exits first ends the wait, unless\n * another daemon already holds the port. Mirrors Python's\n * `ensure_running`.\n */\n async ensureRunning(opts: { allowSpawn?: boolean; timeoutMs?: number } = {}): Promise<void> {\n const allowSpawn = opts.allowSpawn ?? true\n const timeoutMs = opts.timeoutMs ?? 30_000\n if (await this.isReachable()) return\n if (!isLocalUrl(this.settings.url)) {\n throw new DaemonUnreachable(`daemon not reachable at ${this.settings.url}`)\n }\n if (!allowSpawn) {\n throw new DaemonUnreachable(\n `daemon not reachable at ${this.settings.url}; run \\`mirage workspace create CONFIG.yaml\\` to spawn one`,\n )\n }\n const daemon = this.spawnDaemon()\n const deadline = Date.now() + timeoutMs\n while (Date.now() < deadline) {\n if (await this.isReachable(300)) return\n // Gone without answering: a crash, or a bind lost to a daemon another\n // command spawned, which holds the port and is waited for.\n const code = daemon.exitCode ?? daemon.signalCode\n if (code !== null && !(await this.listening())) {\n throw new DaemonUnreachable(\n `daemon exited with code ${String(code)} before answering /v1/health; see ${join(mirageHome(), 'daemon.log')}`,\n )\n }\n await new Promise((r) => setTimeout(r, 100))\n }\n throw new DaemonUnreachable(\n `daemon spawned but did not answer /v1/health within ${String(timeoutMs)}ms`,\n )\n }\n\n /** Whether anything accepts a connection on the daemon's port. */\n private listening(): Promise<boolean> {\n return new Promise((resolve) => {\n const sock = connect({ host: new URL(this.settings.url).hostname, port: this.portFromUrl() })\n const done = (up: boolean): void => {\n sock.destroy()\n resolve(up)\n }\n sock.setTimeout(300)\n sock.once('connect', () => {\n done(true)\n })\n sock.once('timeout', () => {\n done(false)\n })\n sock.once('error', (err) => {\n console.debug(`nothing accepts on the daemon's port: ${String(err)}`)\n done(false)\n })\n })\n }\n\n private spawnDaemon(): ChildProcess {\n const table = readDaemonTable(mirageHome())\n validateDaemonTable(table)\n const env: Record<string, string> = {}\n for (const [k, v] of Object.entries(process.env)) {\n if (typeof v === 'string') env[k] = v\n }\n env[ENV_DAEMON_PORT] = String(this.resolvePort(table))\n if ((env[ENV_IDLE_GRACE_SECONDS] ?? '') === '') {\n env[ENV_IDLE_GRACE_SECONDS] = String(this.settings.idleGraceSeconds)\n }\n if (this.settings.authToken === '') {\n this.settings.authToken = ensureTokenFile(defaultTokenFile())\n }\n env[ENV_AUTH_TOKEN] = this.settings.authToken\n if ((env[ENV_AUTH_MODE] ?? '') === '' && (table.auth_mode ?? '') === '') {\n env[ENV_AUTH_MODE] = AuthMode.Local\n }\n const logDir = mirageHome()\n mkdirSync(logDir, { recursive: true })\n const out = openSync(join(logDir, 'daemon.log'), 'a')\n const daemonEntry = requireFromHere.resolve('@struktoai/mirage-server/bin/daemon')\n if (!existsSync(daemonEntry)) {\n throw new Error(\n `daemon binary not found at ${daemonEntry}; reinstall @struktoai/mirage-server`,\n )\n }\n const child = spawn(process.execPath, [daemonEntry], {\n env,\n detached: true,\n stdio: ['ignore', out, out],\n })\n child.on('error', (err) => {\n console.error('failed to spawn daemon:', err)\n })\n child.unref()\n return child\n }\n\n private resolvePort(table: Record<string, string>): number {\n const envPort = process.env[ENV_DAEMON_PORT]\n if (envPort !== undefined && envPort !== '') return Number(envPort)\n const configPort = table.port\n if (configPort !== undefined && configPort !== '') return Number(configPort)\n return this.portFromUrl()\n }\n\n private portFromUrl(): number {\n const u = new URL(this.settings.url)\n return Number(u.port) || 8765\n }\n}\n\nexport function makeClient(settings: DaemonSettings): DaemonClient {\n return new DaemonClient(settings)\n}\n"],"mappings":"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","names":["existsSync","mkdirSync","readFileSync","dirname","join","ENV_DAEMON_PORT","ENV_IDLE_GRACE_SECONDS","mirageHome","existsSync","readFileSync","mirageHome","join","ENV_IDLE_GRACE_SECONDS","ENV_DAEMON_PORT","mkdirSync","dirname","existsSync","mkdirSync","openSync","join","defaultTokenFile","readDaemonTable","mirageHome","join","mirageHome","readDaemonTable","defaultTokenFile","mkdirSync","openSync","existsSync"]}
@@ -0,0 +1,77 @@
1
+ // src/output.ts
2
+ function emit(obj, human) {
3
+ if (human !== void 0 && process.stdout.isTTY) {
4
+ process.stdout.write(human(obj) + "\n");
5
+ return;
6
+ }
7
+ process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
8
+ }
9
+ function fail(message, exitCode = 1) {
10
+ process.stderr.write(message + "\n");
11
+ process.exit(exitCode);
12
+ }
13
+ function exitCodeFromResponse(r) {
14
+ if (typeof r !== "object" || r === null) return 0;
15
+ const obj = r;
16
+ const inner = obj.kind === "io" ? obj : obj.result ?? null;
17
+ if (inner !== null && inner.kind === "io" && typeof inner.exit_code === "number" && Number.isFinite(inner.exit_code)) {
18
+ return Math.min(255, Math.max(0, Math.trunc(inner.exit_code)));
19
+ }
20
+ if (typeof obj.status === "string" && (obj.status === "failed" || obj.status === "canceled")) {
21
+ return 2;
22
+ }
23
+ return 0;
24
+ }
25
+ async function handleResponse(r) {
26
+ if (r.status >= 400) {
27
+ let detail = await r.text();
28
+ try {
29
+ const parsed = JSON.parse(detail);
30
+ if (parsed.detail !== void 0) detail = parsed.detail;
31
+ } catch {
32
+ }
33
+ fail(`daemon error ${String(r.status)}: ${detail}`, 2);
34
+ }
35
+ if (r.status === 204) return {};
36
+ const text = await r.text();
37
+ if (text === "") return {};
38
+ return JSON.parse(text);
39
+ }
40
+ function formatAge(epoch) {
41
+ const delta = Math.max(0, Date.now() / 1e3 - epoch);
42
+ if (delta < 60) return `${String(Math.floor(delta))}s`;
43
+ if (delta < 3600) return `${String(Math.floor(delta / 60))}m`;
44
+ if (delta < 86400) return `${String(Math.floor(delta / 3600))}h`;
45
+ return `${String(Math.floor(delta / 86400))}d`;
46
+ }
47
+ function formatTable(headers, rows) {
48
+ if (rows.length === 0) return headers.join(" ");
49
+ const norm = rows.map((row) => row.map((c) => c ?? ""));
50
+ const widths = headers.map((h) => h.length);
51
+ for (const row of norm) {
52
+ for (let i = 0; i < row.length; i++) {
53
+ widths[i] = Math.max(widths[i] ?? 0, row[i]?.length ?? 0);
54
+ }
55
+ }
56
+ const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
57
+ const out = [];
58
+ out.push(
59
+ headers.map((h, i) => pad(h, widths[i] ?? h.length)).join(" ").trimEnd()
60
+ );
61
+ for (const row of norm) {
62
+ out.push(
63
+ row.map((c, i) => pad(c, widths[i] ?? c.length)).join(" ").trimEnd()
64
+ );
65
+ }
66
+ return out.join("\n");
67
+ }
68
+
69
+ export {
70
+ emit,
71
+ fail,
72
+ exitCodeFromResponse,
73
+ handleResponse,
74
+ formatAge,
75
+ formatTable
76
+ };
77
+ //# sourceMappingURL=chunk-VOPUKMVY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/output.ts"],"sourcesContent":["// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nexport type Formatter<T = unknown> = (obj: T) => string\n\nexport function emit<T = unknown>(obj: T, human?: Formatter<T>): void {\n if (human !== undefined && process.stdout.isTTY) {\n process.stdout.write(human(obj) + '\\n')\n return\n }\n process.stdout.write(JSON.stringify(obj, null, 2) + '\\n')\n}\n\nexport function fail(message: string, exitCode = 1): never {\n process.stderr.write(message + '\\n')\n process.exit(exitCode)\n}\n\nexport function exitCodeFromResponse(r: unknown): number {\n if (typeof r !== 'object' || r === null) return 0\n const obj = r as Record<string, unknown>\n const inner =\n obj.kind === 'io' ? obj : ((obj.result as Record<string, unknown> | null | undefined) ?? null)\n if (\n inner !== null &&\n inner.kind === 'io' &&\n typeof inner.exit_code === 'number' &&\n Number.isFinite(inner.exit_code)\n ) {\n return Math.min(255, Math.max(0, Math.trunc(inner.exit_code)))\n }\n if (typeof obj.status === 'string' && (obj.status === 'failed' || obj.status === 'canceled')) {\n return 2\n }\n return 0\n}\n\nexport async function handleResponse(r: Response): Promise<unknown> {\n if (r.status >= 400) {\n let detail = await r.text()\n try {\n const parsed = JSON.parse(detail) as { detail?: string }\n if (parsed.detail !== undefined) detail = parsed.detail\n } catch {\n // body wasn't JSON; fall through with the raw text as detail\n }\n fail(`daemon error ${String(r.status)}: ${detail}`, 2)\n }\n if (r.status === 204) return {}\n const text = await r.text()\n if (text === '') return {}\n return JSON.parse(text) as unknown\n}\n\nexport function formatAge(epoch: number): string {\n const delta = Math.max(0, Date.now() / 1000 - epoch)\n if (delta < 60) return `${String(Math.floor(delta))}s`\n if (delta < 3600) return `${String(Math.floor(delta / 60))}m`\n if (delta < 86400) return `${String(Math.floor(delta / 3600))}h`\n return `${String(Math.floor(delta / 86400))}d`\n}\n\nexport function formatTable(headers: string[], rows: (string | undefined)[][]): string {\n if (rows.length === 0) return headers.join(' ')\n const norm: string[][] = rows.map((row) => row.map((c) => c ?? ''))\n const widths = headers.map((h) => h.length)\n for (const row of norm) {\n for (let i = 0; i < row.length; i++) {\n widths[i] = Math.max(widths[i] ?? 0, row[i]?.length ?? 0)\n }\n }\n const pad = (s: string, w: number) => s + ' '.repeat(Math.max(0, w - s.length))\n const out: string[] = []\n out.push(\n headers\n .map((h, i) => pad(h, widths[i] ?? h.length))\n .join(' ')\n .trimEnd(),\n )\n for (const row of norm) {\n out.push(\n row\n .map((c, i) => pad(c, widths[i] ?? c.length))\n .join(' ')\n .trimEnd(),\n )\n }\n return out.join('\\n')\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,8 +1,28 @@
1
+ /**
2
+ * What `mirage login` keeps, for the one server it logged in to: the
3
+ * server the token is for (it goes nowhere else), the bearer token, when
4
+ * the user signed in (the login ends 30 days later, however often it was
5
+ * refreshed), the refresh token (null for a pasted token), when the
6
+ * access token ends if known, the OAuth client that signed in, and where
7
+ * a refresh goes.
8
+ */
9
+ interface Login {
10
+ url: string;
11
+ access_token: string;
12
+ logged_in_at: number;
13
+ refresh_token: string | null;
14
+ expires_at: number | null;
15
+ client_id: string | null;
16
+ token_endpoint: string | null;
17
+ }
18
+
1
19
  declare const DEFAULT_DAEMON_URL = "http://127.0.0.1:8765";
2
20
  interface DaemonSettings {
3
21
  url: string;
4
22
  authToken: string;
5
23
  idleGraceSeconds: number;
24
+ /** The `mirage login` made for this URL, used when no token is set. */
25
+ login?: Login;
6
26
  }
7
27
  interface LoadOptions {
8
28
  env?: Record<string, string | undefined>;
@@ -13,17 +33,48 @@ declare function loadDaemonSettings(options?: LoadOptions): DaemonSettings;
13
33
 
14
34
  declare class DaemonClient {
15
35
  readonly settings: DaemonSettings;
36
+ private refreshing;
37
+ /** The last login token this client sent. */
38
+ held: string;
16
39
  constructor(settings: DaemonSettings);
40
+ /**
41
+ * The bearer token to send: the settings' own, else the login's,
42
+ * refreshed when it is about to end; empty when there is none. The client
43
+ * stays bound to the login it started with. That login is read from its
44
+ * file each time, so a refresh made by another process is shared, while
45
+ * `mirage logout` or a new login stops this one; requests sent at once
46
+ * share one read, so they refresh it once. The token sent is kept as
47
+ * `held`. Throws `LoginError` when the login ended or changed, or cannot
48
+ * give a token.
49
+ */
50
+ token(): Promise<string>;
17
51
  private headers;
18
52
  request(method: string, path: string, init?: RequestInit & {
19
- timeoutMs?: number;
53
+ timeoutMs?: number | null;
20
54
  }): Promise<Response>;
21
- requestMultipart(method: string, path: string, form: FormData): Promise<Response>;
55
+ /**
56
+ * Send a JSON `request` part and then `part`, streamed from its data
57
+ * as it is read, as one multipart body. There is no overall timeout,
58
+ * since the upload lasts as long as its input; `signal` aborts it.
59
+ */
60
+ requestUpload(method: string, path: string, request: Record<string, unknown>, part: {
61
+ name: string;
62
+ data: AsyncIterable<Uint8Array>;
63
+ }, signal?: AbortSignal): Promise<Response>;
22
64
  isReachable(timeoutMs?: number): Promise<boolean>;
65
+ /**
66
+ * Ensure the daemon is reachable, optionally spawning it. `timeoutMs` is
67
+ * how long a spawned daemon has to answer `/v1/health`: a first start
68
+ * loads the whole server. A daemon that exits first ends the wait, unless
69
+ * another daemon already holds the port. Mirrors Python's
70
+ * `ensure_running`.
71
+ */
23
72
  ensureRunning(opts?: {
24
73
  allowSpawn?: boolean;
25
74
  timeoutMs?: number;
26
75
  }): Promise<void>;
76
+ /** Whether anything accepts a connection on the daemon's port. */
77
+ private listening;
27
78
  private spawnDaemon;
28
79
  private resolvePort;
29
80
  private portFromUrl;
package/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import {
2
2
  DEFAULT_DAEMON_URL,
3
3
  DaemonClient,
4
- emit,
5
- fail,
6
- handleResponse,
7
4
  loadDaemonSettings,
8
5
  makeClient
9
- } from "./chunk-BNUV7A4V.js";
6
+ } from "./chunk-IJUWWUKK.js";
7
+ import {
8
+ emit,
9
+ fail,
10
+ handleResponse
11
+ } from "./chunk-VOPUKMVY.js";
10
12
  export {
11
13
  DEFAULT_DAEMON_URL,
12
14
  DaemonClient,
@@ -0,0 +1,142 @@
1
+ import {
2
+ exitCodeFromResponse,
3
+ handleResponse
4
+ } from "./chunk-VOPUKMVY.js";
5
+
6
+ // src/stream.ts
7
+ import { Buffer } from "buffer";
8
+ import { refusalLine } from "@struktoai/mirage-core/workspace/tools/io_text";
9
+ var MAX_RECORD_BYTES = 1024 * 1024;
10
+ async function write(output, data, signal) {
11
+ signal.throwIfAborted();
12
+ await new Promise((resolve, reject) => {
13
+ const abort = () => {
14
+ reject(signal.reason);
15
+ };
16
+ signal.addEventListener("abort", abort, { once: true });
17
+ output.write(data, (error) => {
18
+ signal.removeEventListener("abort", abort);
19
+ if (error !== null && error !== void 0) reject(error);
20
+ else resolve();
21
+ });
22
+ });
23
+ }
24
+ async function consumeStream(source, stdout, stderr) {
25
+ let pending = Buffer.alloc(0);
26
+ let terminal;
27
+ for await (const chunk of source) {
28
+ pending = Buffer.concat([pending, chunk]);
29
+ let newline;
30
+ while ((newline = pending.indexOf(10)) >= 0) {
31
+ const line = pending.subarray(0, newline);
32
+ pending = pending.subarray(newline + 1);
33
+ if (line.length > MAX_RECORD_BYTES) throw new Error("daemon stream record is too large");
34
+ const record = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(line));
35
+ if (typeof record !== "object" || record === null || Array.isArray(record) || terminal !== void 0)
36
+ throw new Error("invalid daemon stream record");
37
+ const value = record;
38
+ if (value.stream === "stdout" || value.stream === "stderr") {
39
+ if (typeof value.data !== "string") throw new Error("invalid daemon stream bytes");
40
+ const raw = Buffer.from(value.data, "base64");
41
+ if (raw.toString("base64") !== value.data) throw new Error("invalid daemon stream bytes");
42
+ await (value.stream === "stdout" ? stdout(raw) : stderr(raw));
43
+ } else if (value.status === "done" || value.status === "failed" || value.status === "canceled") {
44
+ if (value.result !== null && (typeof value.result !== "object" || Array.isArray(value.result)) || value.error !== null && typeof value.error !== "string")
45
+ throw new Error("invalid daemon stream completion");
46
+ terminal = value;
47
+ } else throw new Error("invalid daemon stream record");
48
+ }
49
+ if (pending.length > MAX_RECORD_BYTES) throw new Error("daemon stream record is too large");
50
+ }
51
+ if (pending.length !== 0 || terminal === void 0)
52
+ throw new Error("daemon stream ended without a completion record");
53
+ return terminal;
54
+ }
55
+ function refusalOf(result) {
56
+ const raw = result?.refusal;
57
+ if (raw === null || raw === void 0) return null;
58
+ return {
59
+ kind: raw.kind,
60
+ reason: raw.reason,
61
+ policy: raw.policy,
62
+ askId: raw.ask_id
63
+ };
64
+ }
65
+ async function streamShell(client, path, payload, piped, jsonOutput = false) {
66
+ const stop = new AbortController();
67
+ const state = { interrupted: false };
68
+ const interrupt = () => {
69
+ state.interrupted = true;
70
+ stop.abort();
71
+ };
72
+ const outputFailure = (error) => {
73
+ stop.abort(error);
74
+ };
75
+ process.stdout.on("error", outputFailure);
76
+ process.stderr.on("error", outputFailure);
77
+ process.on("SIGINT", interrupt);
78
+ try {
79
+ const response = piped ? await client.requestUpload(
80
+ "POST",
81
+ path,
82
+ payload,
83
+ { name: "stdin", data: process.stdin },
84
+ stop.signal
85
+ ) : await client.request("POST", path, {
86
+ body: JSON.stringify(payload),
87
+ signal: stop.signal,
88
+ timeoutMs: null
89
+ });
90
+ if (response.status === 499) return 130;
91
+ if (!response.ok) await handleResponse(response);
92
+ if (response.body === null) throw new Error("daemon stream ended without a completion record");
93
+ const capturedStdout = [];
94
+ const capturedStderr = [];
95
+ const terminal = await consumeStream(
96
+ response.body,
97
+ (data) => {
98
+ if (jsonOutput) {
99
+ capturedStdout.push(data);
100
+ return Promise.resolve();
101
+ }
102
+ return write(process.stdout, data, stop.signal);
103
+ },
104
+ (data) => {
105
+ if (jsonOutput) {
106
+ capturedStderr.push(data);
107
+ return Promise.resolve();
108
+ }
109
+ return write(process.stderr, data, stop.signal);
110
+ }
111
+ );
112
+ if (terminal.status === "failed") throw new Error(`shell failed: ${String(terminal.error)}`);
113
+ if (terminal.status === "canceled") return 130;
114
+ const line = jsonOutput ? "" : refusalLine(refusalOf(terminal.result));
115
+ if (line !== "") await write(process.stderr, Buffer.from(line), stop.signal);
116
+ if (jsonOutput) {
117
+ const result = {
118
+ ...terminal.result,
119
+ stdout: Buffer.concat(capturedStdout).toString("utf8"),
120
+ stderr: Buffer.concat(capturedStderr).toString("utf8")
121
+ };
122
+ await write(process.stdout, Buffer.from(JSON.stringify(result, null, 2) + "\n"), stop.signal);
123
+ }
124
+ return exitCodeFromResponse(terminal.result);
125
+ } catch (error) {
126
+ if (state.interrupted) return 130;
127
+ const failure = stop.signal.aborted ? stop.signal.reason : error;
128
+ if (failure instanceof Error && "code" in failure && failure.code === "EPIPE") return 141;
129
+ throw error;
130
+ } finally {
131
+ stop.abort();
132
+ if (piped) process.stdin.destroy();
133
+ process.off("SIGINT", interrupt);
134
+ process.stdout.off("error", outputFailure);
135
+ process.stderr.off("error", outputFailure);
136
+ }
137
+ }
138
+ export {
139
+ consumeStream,
140
+ streamShell
141
+ };
142
+ //# sourceMappingURL=stream-LJTY7SCX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stream.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\nimport type { Writable } from 'node:stream'\nimport type { Refusal } from '@struktoai/mirage-core/types'\nimport { refusalLine } from '@struktoai/mirage-core/workspace/tools/io_text'\nimport type { DaemonClient } from './client.ts'\nimport { exitCodeFromResponse, handleResponse } from './output.ts'\n\nconst MAX_RECORD_BYTES = 1024 * 1024\n\ninterface Completion {\n status: 'done' | 'failed' | 'canceled'\n result: Record<string, unknown> | null\n error: string | null\n}\n\nasync function write(output: Writable, data: Uint8Array, signal: AbortSignal): Promise<void> {\n signal.throwIfAborted()\n await new Promise<void>((resolve, reject) => {\n const abort = (): void => {\n reject(signal.reason as Error)\n }\n signal.addEventListener('abort', abort, { once: true })\n output.write(data, (error) => {\n signal.removeEventListener('abort', abort)\n if (error !== null && error !== undefined) reject(error)\n else resolve()\n })\n })\n}\n\n/** Decode bounded NDJSON records and await each output write. */\nexport async function consumeStream(\n source: AsyncIterable<Uint8Array>,\n stdout: (data: Uint8Array) => Promise<void>,\n stderr: (data: Uint8Array) => Promise<void>,\n): Promise<Completion> {\n let pending = Buffer.alloc(0)\n let terminal: Completion | undefined\n for await (const chunk of source) {\n pending = Buffer.concat([pending, chunk])\n let newline: number\n while ((newline = pending.indexOf(10)) >= 0) {\n const line = pending.subarray(0, newline)\n pending = pending.subarray(newline + 1)\n if (line.length > MAX_RECORD_BYTES) throw new Error('daemon stream record is too large')\n const record: unknown = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(line))\n if (\n typeof record !== 'object' ||\n record === null ||\n Array.isArray(record) ||\n terminal !== undefined\n )\n throw new Error('invalid daemon stream record')\n const value = record as Record<string, unknown>\n if (value.stream === 'stdout' || value.stream === 'stderr') {\n if (typeof value.data !== 'string') throw new Error('invalid daemon stream bytes')\n const raw = Buffer.from(value.data, 'base64')\n if (raw.toString('base64') !== value.data) throw new Error('invalid daemon stream bytes')\n await (value.stream === 'stdout' ? stdout(raw) : stderr(raw))\n } else if (\n value.status === 'done' ||\n value.status === 'failed' ||\n value.status === 'canceled'\n ) {\n if (\n (value.result !== null &&\n (typeof value.result !== 'object' || Array.isArray(value.result))) ||\n (value.error !== null && typeof value.error !== 'string')\n )\n throw new Error('invalid daemon stream completion')\n terminal = value as unknown as Completion\n } else throw new Error('invalid daemon stream record')\n }\n if (pending.length > MAX_RECORD_BYTES) throw new Error('daemon stream record is too large')\n }\n if (pending.length !== 0 || terminal === undefined)\n throw new Error('daemon stream ended without a completion record')\n return terminal\n}\n\n/** The refusal record off a completion's result, in core's shape. */\nfunction refusalOf(result: Record<string, unknown> | null): Refusal | null {\n const raw = result?.refusal as\n | {\n kind: Refusal['kind']\n reason: string\n policy: string\n ask_id: string | null\n }\n | null\n | undefined\n if (raw === null || raw === undefined) return null\n return {\n kind: raw.kind,\n reason: raw.reason,\n policy: raw.policy,\n askId: raw.ask_id,\n }\n}\n\n/**\n * Upload stdin and drain output concurrently, retaining cancellation through\n * the final record. When a policy refused part of the line, its reason\n * follows the output as one line on stderr, as the SSH door prints it.\n */\nexport async function streamShell(\n client: DaemonClient,\n path: string,\n payload: Record<string, unknown>,\n piped: boolean,\n jsonOutput = false,\n): Promise<number> {\n const stop = new AbortController()\n const state = { interrupted: false }\n const interrupt = (): void => {\n state.interrupted = true\n stop.abort()\n }\n const outputFailure = (error: Error): void => {\n stop.abort(error)\n }\n process.stdout.on('error', outputFailure)\n process.stderr.on('error', outputFailure)\n process.on('SIGINT', interrupt)\n try {\n const response = piped\n ? await client.requestUpload(\n 'POST',\n path,\n payload,\n { name: 'stdin', data: process.stdin },\n stop.signal,\n )\n : await client.request('POST', path, {\n body: JSON.stringify(payload),\n signal: stop.signal,\n timeoutMs: null,\n })\n if (response.status === 499) return 130\n if (!response.ok) await handleResponse(response)\n if (response.body === null) throw new Error('daemon stream ended without a completion record')\n const capturedStdout: Uint8Array[] = []\n const capturedStderr: Uint8Array[] = []\n const terminal = await consumeStream(\n response.body,\n (data) => {\n if (jsonOutput) {\n capturedStdout.push(data)\n return Promise.resolve()\n }\n return write(process.stdout, data, stop.signal)\n },\n (data) => {\n if (jsonOutput) {\n capturedStderr.push(data)\n return Promise.resolve()\n }\n return write(process.stderr, data, stop.signal)\n },\n )\n if (terminal.status === 'failed') throw new Error(`shell failed: ${String(terminal.error)}`)\n if (terminal.status === 'canceled') return 130\n const line = jsonOutput ? '' : refusalLine(refusalOf(terminal.result))\n if (line !== '') await write(process.stderr, Buffer.from(line), stop.signal)\n if (jsonOutput) {\n const result = {\n ...terminal.result,\n stdout: Buffer.concat(capturedStdout).toString('utf8'),\n stderr: Buffer.concat(capturedStderr).toString('utf8'),\n }\n await write(process.stdout, Buffer.from(JSON.stringify(result, null, 2) + '\\n'), stop.signal)\n }\n return exitCodeFromResponse(terminal.result)\n } catch (error) {\n if (state.interrupted) return 130\n const failure: unknown = stop.signal.aborted ? stop.signal.reason : error\n if (failure instanceof Error && 'code' in failure && failure.code === 'EPIPE') return 141\n throw error\n } finally {\n stop.abort()\n if (piped) process.stdin.destroy()\n process.off('SIGINT', interrupt)\n process.stdout.off('error', outputFailure)\n process.stderr.off('error', outputFailure)\n }\n}\n"],"mappings":";;;;;;AAAA,SAAS,cAAc;AAGvB,SAAS,mBAAmB;AAI5B,IAAM,mBAAmB,OAAO;AAQhC,eAAe,MAAM,QAAkB,MAAkB,QAAoC;AAC3F,SAAO,eAAe;AACtB,QAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,UAAM,QAAQ,MAAY;AACxB,aAAO,OAAO,MAAe;AAAA,IAC/B;AACA,WAAO,iBAAiB,SAAS,OAAO,EAAE,MAAM,KAAK,CAAC;AACtD,WAAO,MAAM,MAAM,CAAC,UAAU;AAC5B,aAAO,oBAAoB,SAAS,KAAK;AACzC,UAAI,UAAU,QAAQ,UAAU,OAAW,QAAO,KAAK;AAAA,UAClD,SAAQ;AAAA,IACf,CAAC;AAAA,EACH,CAAC;AACH;AAGA,eAAsB,cACpB,QACA,QACA,QACqB;AACrB,MAAI,UAAU,OAAO,MAAM,CAAC;AAC5B,MAAI;AACJ,mBAAiB,SAAS,QAAQ;AAChC,cAAU,OAAO,OAAO,CAAC,SAAS,KAAK,CAAC;AACxC,QAAI;AACJ,YAAQ,UAAU,QAAQ,QAAQ,EAAE,MAAM,GAAG;AAC3C,YAAM,OAAO,QAAQ,SAAS,GAAG,OAAO;AACxC,gBAAU,QAAQ,SAAS,UAAU,CAAC;AACtC,UAAI,KAAK,SAAS,iBAAkB,OAAM,IAAI,MAAM,mCAAmC;AACvF,YAAM,SAAkB,KAAK,MAAM,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,OAAO,IAAI,CAAC;AACzF,UACE,OAAO,WAAW,YAClB,WAAW,QACX,MAAM,QAAQ,MAAM,KACpB,aAAa;AAEb,cAAM,IAAI,MAAM,8BAA8B;AAChD,YAAM,QAAQ;AACd,UAAI,MAAM,WAAW,YAAY,MAAM,WAAW,UAAU;AAC1D,YAAI,OAAO,MAAM,SAAS,SAAU,OAAM,IAAI,MAAM,6BAA6B;AACjF,cAAM,MAAM,OAAO,KAAK,MAAM,MAAM,QAAQ;AAC5C,YAAI,IAAI,SAAS,QAAQ,MAAM,MAAM,KAAM,OAAM,IAAI,MAAM,6BAA6B;AACxF,eAAO,MAAM,WAAW,WAAW,OAAO,GAAG,IAAI,OAAO,GAAG;AAAA,MAC7D,WACE,MAAM,WAAW,UACjB,MAAM,WAAW,YACjB,MAAM,WAAW,YACjB;AACA,YACG,MAAM,WAAW,SACf,OAAO,MAAM,WAAW,YAAY,MAAM,QAAQ,MAAM,MAAM,MAChE,MAAM,UAAU,QAAQ,OAAO,MAAM,UAAU;AAEhD,gBAAM,IAAI,MAAM,kCAAkC;AACpD,mBAAW;AAAA,MACb,MAAO,OAAM,IAAI,MAAM,8BAA8B;AAAA,IACvD;AACA,QAAI,QAAQ,SAAS,iBAAkB,OAAM,IAAI,MAAM,mCAAmC;AAAA,EAC5F;AACA,MAAI,QAAQ,WAAW,KAAK,aAAa;AACvC,UAAM,IAAI,MAAM,iDAAiD;AACnE,SAAO;AACT;AAGA,SAAS,UAAU,QAAwD;AACzE,QAAM,MAAM,QAAQ;AASpB,MAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO;AAC9C,SAAO;AAAA,IACL,MAAM,IAAI;AAAA,IACV,QAAQ,IAAI;AAAA,IACZ,QAAQ,IAAI;AAAA,IACZ,OAAO,IAAI;AAAA,EACb;AACF;AAOA,eAAsB,YACpB,QACA,MACA,SACA,OACA,aAAa,OACI;AACjB,QAAM,OAAO,IAAI,gBAAgB;AACjC,QAAM,QAAQ,EAAE,aAAa,MAAM;AACnC,QAAM,YAAY,MAAY;AAC5B,UAAM,cAAc;AACpB,SAAK,MAAM;AAAA,EACb;AACA,QAAM,gBAAgB,CAAC,UAAuB;AAC5C,SAAK,MAAM,KAAK;AAAA,EAClB;AACA,UAAQ,OAAO,GAAG,SAAS,aAAa;AACxC,UAAQ,OAAO,GAAG,SAAS,aAAa;AACxC,UAAQ,GAAG,UAAU,SAAS;AAC9B,MAAI;AACF,UAAM,WAAW,QACb,MAAM,OAAO;AAAA,MACX;AAAA,MACA;AAAA,MACA;AAAA,MACA,EAAE,MAAM,SAAS,MAAM,QAAQ,MAAM;AAAA,MACrC,KAAK;AAAA,IACP,IACA,MAAM,OAAO,QAAQ,QAAQ,MAAM;AAAA,MACjC,MAAM,KAAK,UAAU,OAAO;AAAA,MAC5B,QAAQ,KAAK;AAAA,MACb,WAAW;AAAA,IACb,CAAC;AACL,QAAI,SAAS,WAAW,IAAK,QAAO;AACpC,QAAI,CAAC,SAAS,GAAI,OAAM,eAAe,QAAQ;AAC/C,QAAI,SAAS,SAAS,KAAM,OAAM,IAAI,MAAM,iDAAiD;AAC7F,UAAM,iBAA+B,CAAC;AACtC,UAAM,iBAA+B,CAAC;AACtC,UAAM,WAAW,MAAM;AAAA,MACrB,SAAS;AAAA,MACT,CAAC,SAAS;AACR,YAAI,YAAY;AACd,yBAAe,KAAK,IAAI;AACxB,iBAAO,QAAQ,QAAQ;AAAA,QACzB;AACA,eAAO,MAAM,QAAQ,QAAQ,MAAM,KAAK,MAAM;AAAA,MAChD;AAAA,MACA,CAAC,SAAS;AACR,YAAI,YAAY;AACd,yBAAe,KAAK,IAAI;AACxB,iBAAO,QAAQ,QAAQ;AAAA,QACzB;AACA,eAAO,MAAM,QAAQ,QAAQ,MAAM,KAAK,MAAM;AAAA,MAChD;AAAA,IACF;AACA,QAAI,SAAS,WAAW,SAAU,OAAM,IAAI,MAAM,iBAAiB,OAAO,SAAS,KAAK,CAAC,EAAE;AAC3F,QAAI,SAAS,WAAW,WAAY,QAAO;AAC3C,UAAM,OAAO,aAAa,KAAK,YAAY,UAAU,SAAS,MAAM,CAAC;AACrE,QAAI,SAAS,GAAI,OAAM,MAAM,QAAQ,QAAQ,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM;AAC3E,QAAI,YAAY;AACd,YAAM,SAAS;AAAA,QACb,GAAG,SAAS;AAAA,QACZ,QAAQ,OAAO,OAAO,cAAc,EAAE,SAAS,MAAM;AAAA,QACrD,QAAQ,OAAO,OAAO,cAAc,EAAE,SAAS,MAAM;AAAA,MACvD;AACA,YAAM,MAAM,QAAQ,QAAQ,OAAO,KAAK,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,IAAI,GAAG,KAAK,MAAM;AAAA,IAC9F;AACA,WAAO,qBAAqB,SAAS,MAAM;AAAA,EAC7C,SAAS,OAAO;AACd,QAAI,MAAM,YAAa,QAAO;AAC9B,UAAM,UAAmB,KAAK,OAAO,UAAU,KAAK,OAAO,SAAS;AACpE,QAAI,mBAAmB,SAAS,UAAU,WAAW,QAAQ,SAAS,QAAS,QAAO;AACtF,UAAM;AAAA,EACR,UAAE;AACA,SAAK,MAAM;AACX,QAAI,MAAO,SAAQ,MAAM,QAAQ;AACjC,YAAQ,IAAI,UAAU,SAAS;AAC/B,YAAQ,OAAO,IAAI,SAAS,aAAa;AACzC,YAAQ,OAAO,IAAI,SAAS,aAAa;AAAA,EAC3C;AACF;","names":[]}
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@struktoai/mirage-cli",
3
- "version": "0.0.7-alpha.2",
3
+ "version": "0.0.7",
4
4
  "description": "A Unified Virtual Filesystem For AI Agents",
5
5
  "keywords": [
6
6
  "bash",
7
7
  "fuse",
8
8
  "typescript",
9
- "vfs",
9
+ "workspace",
10
10
  "ai-agents",
11
11
  "agent-sandbox",
12
12
  "langchain",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "type": "module",
34
34
  "engines": {
35
- "node": ">=20.10.0"
35
+ "node": "^22.20 || ^24.12 || >=25"
36
36
  },
37
37
  "main": "./dist/index.js",
38
38
  "types": "./dist/index.d.ts",
@@ -45,19 +45,20 @@
45
45
  "dependencies": {
46
46
  "commander": "^14.0.0",
47
47
  "yaml": "^2.6.0",
48
- "@struktoai/mirage-server": "0.0.7-alpha.2",
49
- "@struktoai/mirage-core": "0.0.7-alpha.2",
50
- "@struktoai/mirage-node": "0.0.7-alpha.2",
51
- "@struktoai/mirage-agents": "0.0.7-alpha.2"
48
+ "@struktoai/mirage-server": "0.0.7",
49
+ "@struktoai/mirage-core": "0.0.7",
50
+ "@struktoai/mirage-node": "0.0.7"
52
51
  },
53
52
  "optionalDependencies": {
54
53
  "@zkochan/fuse-native": "^0.1.0"
55
54
  },
56
55
  "devDependencies": {
57
56
  "@types/node": "^22.0.0",
57
+ "@types/ssh2": "^1.15.0",
58
+ "ssh2": "^1.16.0",
58
59
  "tsup": "^8.5.0",
59
60
  "typescript": "^6.0.0",
60
- "vitest": "^3.2.6"
61
+ "vitest": "^4.1.11"
61
62
  },
62
63
  "scripts": {
63
64
  "build": "tsup",