style(computer-use-mcp): house keeping, lint fixes, type missing, and many more
This commit is contained in:
Generated
+4
@@ -3760,6 +3760,10 @@ importers:
|
||||
zod:
|
||||
specifier: ^4.3.6
|
||||
version: 4.3.6
|
||||
devDependencies:
|
||||
'@types/ws':
|
||||
specifier: 'catalog:'
|
||||
version: 8.18.1
|
||||
|
||||
services/discord-bot:
|
||||
dependencies:
|
||||
|
||||
@@ -53,5 +53,8 @@
|
||||
"node-pty": "catalog:",
|
||||
"ws": "^8.20.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ws": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,15 @@ const knownApps: KnownAppDefinition[] = [
|
||||
{ canonical: 'Electron', aliases: ['electron'] },
|
||||
]
|
||||
|
||||
const APP_SUFFIX_RE = /\.app$/u
|
||||
const WHITESPACE_RE = /\s+/gu
|
||||
|
||||
function normalizeAppNameKey(value: string) {
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\.app$/u, '')
|
||||
.replace(/\s+/gu, ' ')
|
||||
.replace(APP_SUFFIX_RE, '')
|
||||
.replace(WHITESPACE_RE, ' ')
|
||||
}
|
||||
|
||||
function getCanonicalKnownAppName(value: string) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import process, { env, exit } from 'node:process'
|
||||
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { env, exit } from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { resolveComputerUseConfig } from '../config'
|
||||
@@ -8,8 +9,8 @@ import { runProcess } from '../utils/process'
|
||||
|
||||
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const distDir = resolve(packageDir, 'dist')
|
||||
const remoteInstallDir = normalizeRemoteShellPath(env.COMPUTER_USE_REMOTE_INSTALL_DIR?.trim() || '${HOME}/.local/share/airi-desktop-runner')
|
||||
const remoteRunnerPath = normalizeRemoteShellPath(env.COMPUTER_USE_REMOTE_RUNNER_COMMAND?.trim() || '${HOME}/.local/bin/airi-desktop-runner')
|
||||
const remoteInstallDir = normalizeRemoteShellPath(env.COMPUTER_USE_REMOTE_INSTALL_DIR?.trim() || '$HOME/.local/share/airi-desktop-runner')
|
||||
const remoteRunnerPath = normalizeRemoteShellPath(env.COMPUTER_USE_REMOTE_RUNNER_COMMAND?.trim() || '$HOME/.local/bin/airi-desktop-runner')
|
||||
|
||||
async function buildLocalBundle() {
|
||||
await runProcess('pnpm', ['build'], {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
|
||||
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const homeDir = env.HOME || '$HOME'
|
||||
|
||||
async function main() {
|
||||
console.info('🚀 Starting computer-use-mcp server …')
|
||||
@@ -55,11 +56,11 @@ async function main() {
|
||||
}
|
||||
|
||||
// 2. Step 1: Create folder + Python file via terminal_exec
|
||||
console.info('\n📁 Step 1: Creating folder ~/hello-python-project …')
|
||||
console.info('\n📁 Step 1: Creating folder $HOME/hello-python-project …')
|
||||
const mkdirResult = await client.callTool({
|
||||
name: 'terminal_exec',
|
||||
arguments: {
|
||||
command: 'mkdir -p ~/hello-python-project',
|
||||
command: 'mkdir -p "$HOME/hello-python-project"',
|
||||
timeoutMs: 10_000,
|
||||
},
|
||||
})
|
||||
@@ -70,7 +71,7 @@ async function main() {
|
||||
const writeResult = await client.callTool({
|
||||
name: 'terminal_exec',
|
||||
arguments: {
|
||||
command: `cat > ~/hello-python-project/main.py << 'PYEOF'
|
||||
command: `cat > "$HOME/hello-python-project/main.py" << 'PYEOF'
|
||||
#!/usr/bin/env python3
|
||||
"""Hello World project — created by AIRI computer-use-mcp"""
|
||||
|
||||
@@ -87,12 +88,12 @@ PYEOF`,
|
||||
printResult('write main.py', writeResult)
|
||||
|
||||
// 4. Step 3: Run it!
|
||||
console.info('\n🐍 Step 3: Running python3 ~/hello-python-project/main.py …')
|
||||
console.info('\n🐍 Step 3: Running python3 $HOME/hello-python-project/main.py …')
|
||||
const runResult = await client.callTool({
|
||||
name: 'terminal_exec',
|
||||
arguments: {
|
||||
command: 'python3 ~/hello-python-project/main.py',
|
||||
cwd: `${env.HOME}/hello-python-project`,
|
||||
command: 'python3 "$HOME/hello-python-project/main.py"',
|
||||
cwd: `${homeDir}/hello-python-project`,
|
||||
timeoutMs: 15_000,
|
||||
},
|
||||
})
|
||||
@@ -103,7 +104,7 @@ PYEOF`,
|
||||
const lsResult = await client.callTool({
|
||||
name: 'terminal_exec',
|
||||
arguments: {
|
||||
command: 'ls -la ~/hello-python-project && echo "---" && cat ~/hello-python-project/main.py',
|
||||
command: 'ls -la "$HOME/hello-python-project" && echo "---" && cat "$HOME/hello-python-project/main.py"',
|
||||
timeoutMs: 10_000,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -88,6 +88,11 @@ const promptMarker = `airi-e2e-${runId.slice(-8)}`
|
||||
// which makes the follow-up Enter key commit composition instead of submitting
|
||||
// the AIRI chat textarea. The prompt remains overrideable via AIRI_E2E_PROMPT.
|
||||
const promptBaseText = env.AIRI_E2E_PROMPT?.trim() || 'Reply with one short sentence only: hello from AIRI desktop E2E.'
|
||||
const WHITESPACE_SPLIT_RE = /\s+/
|
||||
const DOTENV_LINE_SPLIT_RE = /\r?\n/u
|
||||
const QUOTED_VALUE_RE = /^['"]|['"]$/gu
|
||||
const DEVTOOLS_BROWSER_WS_PATH_RE = /\/devtools\/browser\/[^/]+$/
|
||||
const DEVTOOLS_LISTENING_RE = /DevTools listening on (ws:\/\/\S+)/
|
||||
const promptText = `${promptBaseText} [${promptMarker}]`
|
||||
const reportDir = resolve(packageDir, '.computer-use-mcp', 'reports', `airi-chat-observable-${runId}`)
|
||||
const reportPath = resolve(reportDir, 'report.json')
|
||||
@@ -126,7 +131,7 @@ function parseCommandArgs(raw: string | undefined, fallback: string[]) {
|
||||
}
|
||||
|
||||
return raw
|
||||
.split(/\s+/)
|
||||
.split(WHITESPACE_SPLIT_RE)
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
@@ -226,7 +231,7 @@ async function waitFor<T>(label: string, task: () => Promise<T | undefined>, tim
|
||||
function parseDotEnv(text: string) {
|
||||
const values: Record<string, string> = {}
|
||||
|
||||
for (const line of text.split(/\r?\n/u)) {
|
||||
for (const line of text.split(DOTENV_LINE_SPLIT_RE)) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue
|
||||
@@ -239,7 +244,7 @@ function parseDotEnv(text: string) {
|
||||
|
||||
const key = trimmed.slice(0, separatorIndex).trim()
|
||||
const rawValue = trimmed.slice(separatorIndex + 1).trim()
|
||||
const unwrapped = rawValue.replace(/^['"]|['"]$/gu, '')
|
||||
const unwrapped = rawValue.replace(QUOTED_VALUE_RE, '')
|
||||
values[key] = unwrapped
|
||||
}
|
||||
|
||||
@@ -362,7 +367,7 @@ async function listDebugTargets(browserWsUrl: string) {
|
||||
title: String(target.title || ''),
|
||||
type: String(target.type || ''),
|
||||
url: String(target.url || ''),
|
||||
webSocketDebuggerUrl: browserWsUrl.replace(/\/devtools\/browser\/[^/]+$/, `/devtools/page/${targetId}`),
|
||||
webSocketDebuggerUrl: browserWsUrl.replace(DEVTOOLS_BROWSER_WS_PATH_RE, `/devtools/page/${targetId}`),
|
||||
} satisfies DebugTarget
|
||||
})
|
||||
}
|
||||
@@ -502,14 +507,14 @@ async function main() {
|
||||
|
||||
stageProcess.stdout.on('data', (chunk) => {
|
||||
stageLogStream.write(chunk)
|
||||
const match = chunk.toString('utf-8').match(/DevTools listening on (ws:\/\/\S+)/)
|
||||
const match = chunk.toString('utf-8').match(DEVTOOLS_LISTENING_RE)
|
||||
if (match?.[1]) {
|
||||
browserWsUrl = match[1]
|
||||
}
|
||||
})
|
||||
stageProcess.stderr.on('data', (chunk) => {
|
||||
stageLogStream.write(chunk)
|
||||
const match = chunk.toString('utf-8').match(/DevTools listening on (ws:\/\/\S+)/)
|
||||
const match = chunk.toString('utf-8').match(DEVTOOLS_LISTENING_RE)
|
||||
if (match?.[1]) {
|
||||
browserWsUrl = match[1]
|
||||
}
|
||||
|
||||
@@ -76,6 +76,10 @@ const preferredDebugPort = Number(env.AIRI_E2E_DEBUG_PORT || '9222')
|
||||
const runId = new Date().toISOString().replaceAll(':', '-').replaceAll('.', '-')
|
||||
const repoChangesCommand = 'git diff --stat -- services/computer-use-mcp packages/stage-ui apps/stage-tamagotchi'
|
||||
const summaryMarker = 'Terminal self-acquire demo complete.'
|
||||
const DOTENV_LINE_SPLIT_RE = /\r?\n/u
|
||||
const QUOTED_VALUE_RE = /^['"]|['"]$/gu
|
||||
const DEVTOOLS_BROWSER_WS_PATH_RE = /\/devtools\/browser\/[^/]+$/
|
||||
const DEVTOOLS_LISTENING_RE = /DevTools listening on (ws:\/\/\S+)/
|
||||
const promptText = [
|
||||
`Use MCP tools to validate the AIRI repository at ${repoDir}.`,
|
||||
'Call the real workflow, do not narrate or simulate tool results.',
|
||||
@@ -136,7 +140,7 @@ function addTimeline(event: string, detail?: Record<string, unknown>) {
|
||||
function parseDotEnv(text: string) {
|
||||
const values: Record<string, string> = {}
|
||||
|
||||
for (const line of text.split(/\r?\n/u)) {
|
||||
for (const line of text.split(DOTENV_LINE_SPLIT_RE)) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue
|
||||
@@ -149,7 +153,7 @@ function parseDotEnv(text: string) {
|
||||
|
||||
const key = trimmed.slice(0, separatorIndex).trim()
|
||||
const rawValue = trimmed.slice(separatorIndex + 1).trim()
|
||||
const unwrapped = rawValue.replace(/^['"]|['"]$/gu, '')
|
||||
const unwrapped = rawValue.replace(QUOTED_VALUE_RE, '')
|
||||
values[key] = unwrapped
|
||||
}
|
||||
|
||||
@@ -468,7 +472,7 @@ async function listDebugTargets(browserWsUrl: string) {
|
||||
title: String(target.title || ''),
|
||||
type: String(target.type || ''),
|
||||
url: String(target.url || ''),
|
||||
webSocketDebuggerUrl: browserWsUrl.replace(/\/devtools\/browser\/[^/]+$/, `/devtools/page/${targetId}`),
|
||||
webSocketDebuggerUrl: browserWsUrl.replace(DEVTOOLS_BROWSER_WS_PATH_RE, `/devtools/page/${targetId}`),
|
||||
} satisfies DebugTarget
|
||||
})
|
||||
}
|
||||
@@ -669,14 +673,14 @@ async function main() {
|
||||
|
||||
stageProcess.stdout.on('data', (chunk) => {
|
||||
stageLogStream.write(chunk)
|
||||
const match = chunk.toString('utf-8').match(/DevTools listening on (ws:\/\/\S+)/)
|
||||
const match = chunk.toString('utf-8').match(DEVTOOLS_LISTENING_RE)
|
||||
if (match?.[1]) {
|
||||
browserWsUrl = match[1]
|
||||
}
|
||||
})
|
||||
stageProcess.stderr.on('data', (chunk) => {
|
||||
stageLogStream.write(chunk)
|
||||
const match = chunk.toString('utf-8').match(/DevTools listening on (ws:\/\/\S+)/)
|
||||
const match = chunk.toString('utf-8').match(DEVTOOLS_LISTENING_RE)
|
||||
if (match?.[1]) {
|
||||
browserWsUrl = match[1]
|
||||
}
|
||||
|
||||
@@ -127,6 +127,11 @@ const discordBotLogPath = resolve(reportDir, 'discord-bot.log')
|
||||
const mcpSessionRoot = resolve(reportDir, 'computer-use-session')
|
||||
const rootEnvPath = resolve(repoDir, '.env')
|
||||
const verifyDiscordBot = parseBooleanEnv(env.AIRI_E2E_DISCORD_VERIFY_BOT, false)
|
||||
const WHITESPACE_SPLIT_RE = /\s+/
|
||||
const DOTENV_LINE_SPLIT_RE = /\r?\n/u
|
||||
const QUOTED_VALUE_RE = /^['"]|['"]$/gu
|
||||
const DEVTOOLS_BROWSER_WS_PATH_RE = /\/devtools\/browser\/[^/]+$/
|
||||
const DEVTOOLS_LISTENING_RE = /DevTools listening on (ws:\/\/\S+)/
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
@@ -183,7 +188,7 @@ function parseCommandArgs(raw: string | undefined, fallback: string[]) {
|
||||
}
|
||||
|
||||
return raw
|
||||
.split(/\s+/)
|
||||
.split(WHITESPACE_SPLIT_RE)
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
@@ -296,7 +301,7 @@ async function waitFor<T>(label: string, task: () => Promise<T | undefined>, tim
|
||||
function parseDotEnv(text: string) {
|
||||
const values: Record<string, string> = {}
|
||||
|
||||
for (const line of text.split(/\r?\n/u)) {
|
||||
for (const line of text.split(DOTENV_LINE_SPLIT_RE)) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue
|
||||
@@ -309,7 +314,7 @@ function parseDotEnv(text: string) {
|
||||
|
||||
const key = trimmed.slice(0, separatorIndex).trim()
|
||||
const rawValue = trimmed.slice(separatorIndex + 1).trim()
|
||||
const unwrapped = rawValue.replace(/^['"]|['"]$/gu, '')
|
||||
const unwrapped = rawValue.replace(QUOTED_VALUE_RE, '')
|
||||
values[key] = unwrapped
|
||||
}
|
||||
|
||||
@@ -358,7 +363,7 @@ function createLineListener(onLine: (line: string) => void) {
|
||||
|
||||
return (chunk: { toString: (encoding: string) => string }) => {
|
||||
buffer += chunk.toString('utf-8')
|
||||
const lines = buffer.split(/\r?\n/u)
|
||||
const lines = buffer.split(DOTENV_LINE_SPLIT_RE)
|
||||
buffer = lines.pop() ?? ''
|
||||
|
||||
for (const line of lines) {
|
||||
@@ -476,7 +481,7 @@ async function listDebugTargets(browserWsUrl: string) {
|
||||
title: String(target.title || ''),
|
||||
type: String(target.type || ''),
|
||||
url: String(target.url || ''),
|
||||
webSocketDebuggerUrl: browserWsUrl.replace(/\/devtools\/browser\/[^/]+$/, `/devtools/page/${targetId}`),
|
||||
webSocketDebuggerUrl: browserWsUrl.replace(DEVTOOLS_BROWSER_WS_PATH_RE, `/devtools/page/${targetId}`),
|
||||
} satisfies DebugTarget
|
||||
})
|
||||
}
|
||||
@@ -658,7 +663,7 @@ async function main() {
|
||||
})
|
||||
|
||||
const onStageChunk = createLineListener((line) => {
|
||||
const match = line.match(/DevTools listening on (ws:\/\/\S+)/)
|
||||
const match = line.match(DEVTOOLS_LISTENING_RE)
|
||||
if (match?.[1]) {
|
||||
browserWsUrl = match[1]
|
||||
}
|
||||
|
||||
@@ -89,6 +89,11 @@ const stageLogPath = resolve(reportDir, 'stage-tamagotchi.log')
|
||||
const discordBotLogPath = resolve(reportDir, 'discord-bot.log')
|
||||
const mcpSessionRoot = resolve(reportDir, 'computer-use-session')
|
||||
const rootEnvPath = resolve(repoDir, '.env')
|
||||
const WHITESPACE_SPLIT_RE = /\s+/
|
||||
const DOTENV_LINE_SPLIT_RE = /\r?\n/u
|
||||
const QUOTED_VALUE_RE = /^['"]|['"]$/gu
|
||||
const DEVTOOLS_BROWSER_WS_PATH_RE = /\/devtools\/browser\/[^/]+$/
|
||||
const DEVTOOLS_LISTENING_RE = /DevTools listening on (ws:\/\/\S+)/
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
@@ -135,7 +140,7 @@ function parseCommandArgs(raw: string | undefined, fallback: string[]) {
|
||||
}
|
||||
|
||||
return raw
|
||||
.split(/\s+/)
|
||||
.split(WHITESPACE_SPLIT_RE)
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
@@ -248,7 +253,7 @@ async function waitFor<T>(label: string, task: () => Promise<T | undefined>, tim
|
||||
function parseDotEnv(text: string) {
|
||||
const values: Record<string, string> = {}
|
||||
|
||||
for (const line of text.split(/\r?\n/u)) {
|
||||
for (const line of text.split(DOTENV_LINE_SPLIT_RE)) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue
|
||||
@@ -261,7 +266,7 @@ function parseDotEnv(text: string) {
|
||||
|
||||
const key = trimmed.slice(0, separatorIndex).trim()
|
||||
const rawValue = trimmed.slice(separatorIndex + 1).trim()
|
||||
const unwrapped = rawValue.replace(/^['"]|['"]$/gu, '')
|
||||
const unwrapped = rawValue.replace(QUOTED_VALUE_RE, '')
|
||||
values[key] = unwrapped
|
||||
}
|
||||
|
||||
@@ -310,7 +315,7 @@ function createLineListener(onLine: (line: string) => void) {
|
||||
|
||||
return (chunk: { toString: (encoding: string) => string }) => {
|
||||
buffer += chunk.toString('utf-8')
|
||||
const lines = buffer.split(/\r?\n/u)
|
||||
const lines = buffer.split(DOTENV_LINE_SPLIT_RE)
|
||||
buffer = lines.pop() ?? ''
|
||||
|
||||
for (const line of lines) {
|
||||
@@ -428,7 +433,7 @@ async function listDebugTargets(browserWsUrl: string) {
|
||||
title: String(target.title || ''),
|
||||
type: String(target.type || ''),
|
||||
url: String(target.url || ''),
|
||||
webSocketDebuggerUrl: browserWsUrl.replace(/\/devtools\/browser\/[^/]+$/, `/devtools/page/${targetId}`),
|
||||
webSocketDebuggerUrl: browserWsUrl.replace(DEVTOOLS_BROWSER_WS_PATH_RE, `/devtools/page/${targetId}`),
|
||||
} satisfies DebugTarget
|
||||
})
|
||||
}
|
||||
@@ -554,7 +559,7 @@ async function main() {
|
||||
})
|
||||
|
||||
const onStageChunk = createLineListener((line) => {
|
||||
const match = line.match(/DevTools listening on (ws:\/\/\S+)/)
|
||||
const match = line.match(DEVTOOLS_LISTENING_RE)
|
||||
if (match?.[1]) {
|
||||
browserWsUrl = match[1]
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
|
||||
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const WHITESPACE_SPLIT_RE = /\s+/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -42,7 +43,7 @@ function requireStructuredContent(result: unknown, label: string): Record<string
|
||||
|
||||
async function createClient(): Promise<Client> {
|
||||
const command = env.COMPUTER_USE_SMOKE_SERVER_COMMAND?.trim() || 'pnpm'
|
||||
const args = (env.COMPUTER_USE_SMOKE_SERVER_ARGS || 'start').split(/\s+/).filter(Boolean)
|
||||
const args = (env.COMPUTER_USE_SMOKE_SERVER_ARGS || 'start').split(WHITESPACE_SPLIT_RE).filter(Boolean)
|
||||
const cwd = env.COMPUTER_USE_SMOKE_SERVER_CWD?.trim() || packageDir
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
|
||||
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const WHITESPACE_SPLIT_RE = /\s+/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -51,7 +52,7 @@ function createProjectDir(): string {
|
||||
|
||||
async function createClient(): Promise<Client> {
|
||||
const command = env.COMPUTER_USE_SMOKE_SERVER_COMMAND?.trim() || 'pnpm'
|
||||
const args = (env.COMPUTER_USE_SMOKE_SERVER_ARGS || 'start').split(/\s+/).filter(Boolean)
|
||||
const args = (env.COMPUTER_USE_SMOKE_SERVER_ARGS || 'start').split(WHITESPACE_SPLIT_RE).filter(Boolean)
|
||||
const cwd = env.COMPUTER_USE_SMOKE_SERVER_CWD?.trim() || packageDir
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
|
||||
@@ -26,6 +26,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
|
||||
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const WHITESPACE_SPLIT_RE = /\s+/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -56,7 +57,7 @@ function createProjectDir(): string {
|
||||
|
||||
async function createClient(): Promise<Client> {
|
||||
const command = env.COMPUTER_USE_SMOKE_SERVER_COMMAND?.trim() || 'pnpm'
|
||||
const args = (env.COMPUTER_USE_SMOKE_SERVER_ARGS || 'start').split(/\s+/).filter(Boolean)
|
||||
const args = (env.COMPUTER_USE_SMOKE_SERVER_ARGS || 'start').split(WHITESPACE_SPLIT_RE).filter(Boolean)
|
||||
const cwd = env.COMPUTER_USE_SMOKE_SERVER_CWD?.trim() || packageDir
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
|
||||
@@ -27,6 +27,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
|
||||
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const WHITESPACE_SPLIT_RE = /\s+/
|
||||
const fixtureScript = resolve(packageDir, 'fixtures/interactive-echo.mjs')
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -51,7 +52,7 @@ function requireStructuredContent(result: unknown, label: string): Record<string
|
||||
|
||||
async function createClient(): Promise<Client> {
|
||||
const command = env.COMPUTER_USE_SMOKE_SERVER_COMMAND?.trim() || 'pnpm'
|
||||
const args = (env.COMPUTER_USE_SMOKE_SERVER_ARGS || 'start').split(/\s+/).filter(Boolean)
|
||||
const args = (env.COMPUTER_USE_SMOKE_SERVER_ARGS || 'start').split(WHITESPACE_SPLIT_RE).filter(Boolean)
|
||||
const cwd = env.COMPUTER_USE_SMOKE_SERVER_CWD?.trim() || packageDir
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
|
||||
@@ -33,6 +33,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
|
||||
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const WHITESPACE_SPLIT_RE = /\s+/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -63,7 +64,7 @@ function createProjectDir(): string {
|
||||
|
||||
async function createClient(): Promise<Client> {
|
||||
const command = env.COMPUTER_USE_SMOKE_SERVER_COMMAND?.trim() || 'pnpm'
|
||||
const args = (env.COMPUTER_USE_SMOKE_SERVER_ARGS || 'start').split(/\s+/).filter(Boolean)
|
||||
const args = (env.COMPUTER_USE_SMOKE_SERVER_ARGS || 'start').split(WHITESPACE_SPLIT_RE).filter(Boolean)
|
||||
const cwd = env.COMPUTER_USE_SMOKE_SERVER_CWD?.trim() || packageDir
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { RunnerRequest, RunnerResponse } from '../runner/protocol'
|
||||
|
||||
import process from 'node:process'
|
||||
|
||||
import { createInterface } from 'node:readline'
|
||||
|
||||
import { LinuxX11RunnerService } from '../runner/service'
|
||||
|
||||
@@ -6,13 +6,14 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
|
||||
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const WHITESPACE_SPLIT_RE = /\s+/
|
||||
|
||||
function parseCommandArgs(raw: string | undefined, fallback: string[]) {
|
||||
if (!raw?.trim())
|
||||
return fallback
|
||||
|
||||
return raw
|
||||
.split(/\s+/)
|
||||
.split(WHITESPACE_SPLIT_RE)
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
|
||||
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const WHITESPACE_SPLIT_RE = /\s+/
|
||||
|
||||
function parseCommandArgs(raw: string | undefined, fallback: string[]) {
|
||||
if (!raw?.trim()) {
|
||||
@@ -13,7 +14,7 @@ function parseCommandArgs(raw: string | undefined, fallback: string[]) {
|
||||
}
|
||||
|
||||
return raw
|
||||
.split(/\s+/)
|
||||
.split(WHITESPACE_SPLIT_RE)
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
|
||||
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const WHITESPACE_SPLIT_RE = /\s+/
|
||||
|
||||
function parseCommandArgs(raw: string | undefined, fallback: string[]) {
|
||||
if (!raw?.trim()) {
|
||||
@@ -13,7 +14,7 @@ function parseCommandArgs(raw: string | undefined, fallback: string[]) {
|
||||
}
|
||||
|
||||
return raw
|
||||
.split(/\s+/)
|
||||
.split(WHITESPACE_SPLIT_RE)
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import { appNamesMatch, findKnownAppMention } from '../app-aliases'
|
||||
|
||||
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const WHITESPACE_SPLIT_RE = /\s+/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -57,7 +58,7 @@ function createSmokeProjectDir() {
|
||||
|
||||
async function createClient(overrides: Record<string, string> = {}): Promise<Client> {
|
||||
const command = env.COMPUTER_USE_SMOKE_SERVER_COMMAND?.trim() || 'pnpm'
|
||||
const args = (env.COMPUTER_USE_SMOKE_SERVER_ARGS || 'start').split(/\s+/).filter(Boolean)
|
||||
const args = (env.COMPUTER_USE_SMOKE_SERVER_ARGS || 'start').split(WHITESPACE_SPLIT_RE).filter(Boolean)
|
||||
const cwd = env.COMPUTER_USE_SMOKE_SERVER_CWD?.trim() || packageDir
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
|
||||
@@ -72,21 +72,21 @@ export class BrowserDomExtensionBridge {
|
||||
port: this.config.port,
|
||||
})
|
||||
|
||||
const cleanup = () => {
|
||||
nextServer.off('listening', onListening)
|
||||
nextServer.off('error', onError)
|
||||
}
|
||||
|
||||
const onListening = () => {
|
||||
function onListening() {
|
||||
cleanup()
|
||||
resolve(nextServer)
|
||||
}
|
||||
|
||||
const onError = (error: Error) => {
|
||||
function onError(error: Error) {
|
||||
cleanup()
|
||||
reject(error)
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
nextServer.off('listening', onListening)
|
||||
nextServer.off('error', onError)
|
||||
}
|
||||
|
||||
nextServer.once('listening', onListening)
|
||||
nextServer.once('error', onError)
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('resolveComputerUseConfig', () => {
|
||||
expect(config.remoteSshHost).toBe('20.196.212.37')
|
||||
expect(config.remoteSshUser).toBe('airi')
|
||||
expect(config.remoteSshPort).toBe(2201)
|
||||
expect(config.remoteRunnerCommand).toBe('~/.local/bin/custom-runner')
|
||||
expect(config.remoteRunnerCommand).toBe('$HOME/.local/bin/custom-runner')
|
||||
expect(config.remoteDisplaySize).toEqual({
|
||||
width: 1366,
|
||||
height: 768,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ApprovalMode, Bounds, ComputerUseConfig, DisplaySize, ExecutorKind } from './types'
|
||||
|
||||
import { join } from 'node:path'
|
||||
import { cwd, env } from 'node:process'
|
||||
import { cwd, env, platform } from 'node:process'
|
||||
|
||||
const defaultDeniedApps = [
|
||||
'1password',
|
||||
@@ -19,6 +19,45 @@ const defaultOpenableApps = [
|
||||
'Google Chrome',
|
||||
]
|
||||
|
||||
const DISPLAY_SIZE_RE = /^(\d+)x(\d+)$/i
|
||||
const HOME_PREFIX_RE = /^~(?=\/|$)/
|
||||
|
||||
function normalizeHomePathToken(value: string) {
|
||||
return value.replace(HOME_PREFIX_RE, '$HOME')
|
||||
}
|
||||
|
||||
function resolveDefaultOpenableApps(executor: ExecutorKind, hostPlatform: NodeJS.Platform) {
|
||||
if (executor === 'linux-x11') {
|
||||
return ['Terminal', 'Visual Studio Code', 'Google Chrome']
|
||||
}
|
||||
|
||||
if (executor === 'macos-local') {
|
||||
return defaultOpenableApps
|
||||
}
|
||||
|
||||
if (hostPlatform === 'darwin') {
|
||||
return defaultOpenableApps
|
||||
}
|
||||
|
||||
if (hostPlatform === 'win32') {
|
||||
return ['Windows Terminal', 'Visual Studio Code', 'Google Chrome']
|
||||
}
|
||||
|
||||
return ['Terminal', 'Visual Studio Code', 'Google Chrome']
|
||||
}
|
||||
|
||||
function resolveDefaultTerminalShell(hostPlatform: NodeJS.Platform) {
|
||||
if (hostPlatform === 'win32') {
|
||||
return 'powershell.exe'
|
||||
}
|
||||
|
||||
if (hostPlatform === 'linux') {
|
||||
return '/bin/bash'
|
||||
}
|
||||
|
||||
return '/bin/zsh'
|
||||
}
|
||||
|
||||
function parseBoolean(value: string | undefined, fallback: boolean) {
|
||||
if (value == null)
|
||||
return fallback
|
||||
@@ -82,7 +121,7 @@ function parseDisplaySize(value: string | undefined, fallback: DisplaySize): Dis
|
||||
if (!value)
|
||||
return fallback
|
||||
|
||||
const match = value.trim().match(/^(\d+)x(\d+)$/i)
|
||||
const match = value.trim().match(DISPLAY_SIZE_RE)
|
||||
if (!match) {
|
||||
throw new Error(`invalid COMPUTER_USE_REMOTE_DISPLAY_SIZE: ${value}`)
|
||||
}
|
||||
@@ -113,6 +152,7 @@ function inferPortFromUrl(value: string | undefined) {
|
||||
}
|
||||
|
||||
export function resolveComputerUseConfig(): ComputerUseConfig {
|
||||
const hostPlatform = platform
|
||||
const executor = parseExecutor(env.COMPUTER_USE_EXECUTOR)
|
||||
const sessionRoot = env.COMPUTER_USE_SESSION_ROOT?.trim() || join(cwd(), '.computer-use-mcp')
|
||||
const launchHostProcess = env.COMPUTER_USE_LAUNCH_HOST_PROCESS?.trim()
|
||||
@@ -161,7 +201,7 @@ export function resolveComputerUseConfig(): ComputerUseConfig {
|
||||
allowApps: parseList(env.COMPUTER_USE_ALLOW_APPS),
|
||||
denyApps: parseList(env.COMPUTER_USE_DENY_APPS, defaultDeniedApps),
|
||||
denyWindowTitles: parseList(env.COMPUTER_USE_DENY_WINDOW_TITLES),
|
||||
openableApps: parseList(env.COMPUTER_USE_OPENABLE_APPS, defaultOpenableApps),
|
||||
openableApps: parseList(env.COMPUTER_USE_OPENABLE_APPS, resolveDefaultOpenableApps(executor, hostPlatform)),
|
||||
timeoutMs: parseInteger(env.COMPUTER_USE_TIMEOUT_MS, 15_000),
|
||||
sessionTag: env.COMPUTER_USE_SESSION_TAG?.trim() || undefined,
|
||||
launchHostProcess,
|
||||
@@ -174,11 +214,11 @@ export function resolveComputerUseConfig(): ComputerUseConfig {
|
||||
requireSessionTagForMutatingActions,
|
||||
requireAllowedBoundsForMutatingActions,
|
||||
requireCoordinateAlignmentForMutatingActions,
|
||||
terminalShell: env.COMPUTER_USE_TERMINAL_SHELL?.trim() || env.SHELL?.trim() || '/bin/zsh',
|
||||
terminalShell: env.COMPUTER_USE_TERMINAL_SHELL?.trim() || env.SHELL?.trim() || resolveDefaultTerminalShell(hostPlatform),
|
||||
remoteSshHost,
|
||||
remoteSshUser,
|
||||
remoteSshPort: parseInteger(env.COMPUTER_USE_REMOTE_SSH_PORT, 22),
|
||||
remoteRunnerCommand: env.COMPUTER_USE_REMOTE_RUNNER_COMMAND?.trim() || '~/.local/bin/airi-desktop-runner',
|
||||
remoteRunnerCommand: normalizeHomePathToken(env.COMPUTER_USE_REMOTE_RUNNER_COMMAND?.trim() || '$HOME/.local/bin/airi-desktop-runner'),
|
||||
remoteDisplaySize,
|
||||
remoteObservationBaseUrl,
|
||||
remoteObservationServePort,
|
||||
|
||||
@@ -16,10 +16,11 @@ import type {
|
||||
WindowObservation,
|
||||
} from '../types'
|
||||
|
||||
import process, { platform } from 'node:process'
|
||||
|
||||
import { existsSync, readdirSync } from 'node:fs'
|
||||
import { hostname } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { platform } from 'node:process'
|
||||
|
||||
import { appNamesMatch, getKnownAppLaunchNames } from '../app-aliases'
|
||||
import { probeDisplayInfo, probePermissionInfo } from '../runtime-probes'
|
||||
@@ -33,6 +34,8 @@ const buttonNames = {
|
||||
middle: 2,
|
||||
} as const
|
||||
|
||||
const APP_SUFFIX_RE = /\.app$/u
|
||||
|
||||
const keyCodeMap: Record<string, number> = {
|
||||
a: 0,
|
||||
b: 11,
|
||||
@@ -419,12 +422,12 @@ function resolveInstalledMacAppName(app: string) {
|
||||
return false
|
||||
}
|
||||
|
||||
const bundleName = entry.replace(/\.app$/u, '')
|
||||
const bundleName = entry.replace(APP_SUFFIX_RE, '')
|
||||
return getKnownAppLaunchNames(app).some(candidate => appNamesMatch(bundleName, candidate))
|
||||
})
|
||||
|
||||
if (appBundle) {
|
||||
return appBundle.replace(/\.app$/u, '')
|
||||
return appBundle.replace(APP_SUFFIX_RE, '')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { ComputerUseConfig } from '../types'
|
||||
|
||||
import process from 'node:process'
|
||||
|
||||
import { spawn } from 'node:child_process'
|
||||
|
||||
import { runProcess } from '../utils/process'
|
||||
|
||||
function quoteShell(value: string) {
|
||||
return `'${value.replaceAll(`'`, `'\\''`)}'`
|
||||
}
|
||||
const HOME_PREFIX_RE = /^~(?=\/|$)/
|
||||
|
||||
function buildSshTarget(config: ComputerUseConfig) {
|
||||
if (!config.remoteSshHost || !config.remoteSshUser) {
|
||||
@@ -17,7 +17,7 @@ function buildSshTarget(config: ComputerUseConfig) {
|
||||
}
|
||||
|
||||
export function normalizeRemoteShellPath(value: string) {
|
||||
return value.replace(/^~(?=\/|$)/, '${HOME}')
|
||||
return value.replace(HOME_PREFIX_RE, '$HOME')
|
||||
}
|
||||
|
||||
export function buildRemoteShellCommandArgs(config: ComputerUseConfig, command: string) {
|
||||
@@ -34,7 +34,7 @@ export function buildRemoteShellCommandArgs(config: ComputerUseConfig, command:
|
||||
buildSshTarget(config),
|
||||
'sh',
|
||||
'-lc',
|
||||
quoteShell(command),
|
||||
command,
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -26,14 +26,12 @@ import type {
|
||||
RunnerScreenshotResult,
|
||||
} from './protocol'
|
||||
|
||||
import process from 'node:process'
|
||||
|
||||
import { spawn } from 'node:child_process'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { createInterface } from 'node:readline'
|
||||
|
||||
function quoteShell(value: string) {
|
||||
return `'${value.replaceAll(`'`, `'\\''`)}'`
|
||||
}
|
||||
|
||||
function buildSshTarget(config: ComputerUseConfig) {
|
||||
if (!config.remoteSshHost || !config.remoteSshUser) {
|
||||
throw new Error('linux-x11 executor requires COMPUTER_USE_REMOTE_SSH_HOST and COMPUTER_USE_REMOTE_SSH_USER')
|
||||
@@ -75,7 +73,7 @@ export function createSshRunnerTransportFactory(config: ComputerUseConfig): Runn
|
||||
buildSshTarget(config),
|
||||
'sh',
|
||||
'-lc',
|
||||
quoteShell(config.remoteRunnerCommand),
|
||||
config.remoteRunnerCommand,
|
||||
],
|
||||
env: process.env,
|
||||
})
|
||||
|
||||
@@ -21,6 +21,8 @@ import type {
|
||||
RunnerScreenshotResult,
|
||||
} from './protocol'
|
||||
|
||||
import process, { platform } from 'node:process'
|
||||
|
||||
import { spawn } from 'node:child_process'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { createReadStream } from 'node:fs'
|
||||
@@ -28,12 +30,18 @@ import { access, mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'
|
||||
import { createServer } from 'node:http'
|
||||
import { homedir, tmpdir } from 'node:os'
|
||||
import { basename, join } from 'node:path'
|
||||
import { platform } from 'node:process'
|
||||
|
||||
import { runProcess, sanitizeFileSegment } from '../utils/process'
|
||||
|
||||
const sessionDisplayStart = 90
|
||||
const sessionDisplayEnd = 110
|
||||
const ACTIVE_WINDOW_ID_RE = /window id # (0x[0-9a-fA-F]+)/
|
||||
const XPROP_TITLE_RE = /=\s*"([^"]*)"/
|
||||
const XPROP_CLASS_RE = /=\s*"([^"]*)",\s*"([^"]*)"/
|
||||
const CRLF_SPLIT_RE = /\r?\n/
|
||||
const WHITESPACE_SPLIT_RE = /\s+/
|
||||
const TRAILING_SLASH_RE = /\/$/
|
||||
const DUPLICATE_SLASH_RE = /\/{2,}/g
|
||||
|
||||
async function sleep(durationMs: number) {
|
||||
await new Promise(resolve => setTimeout(resolve, durationMs))
|
||||
@@ -199,7 +207,7 @@ export class LinuxX11RunnerService {
|
||||
timeoutMs: 5_000,
|
||||
env: this.getX11Env(),
|
||||
})
|
||||
const match = stdout.match(/window id # (0x[0-9a-fA-F]+)/)
|
||||
const match = stdout.match(ACTIVE_WINDOW_ID_RE)
|
||||
if (!match || match[1] === '0x0') {
|
||||
return {
|
||||
available: false,
|
||||
@@ -220,8 +228,8 @@ export class LinuxX11RunnerService {
|
||||
}).catch(() => ({ stdout: '', stderr: '' })),
|
||||
])
|
||||
|
||||
const title = titleResult.stdout.match(/=\s*"([^"]*)"/)?.[1]
|
||||
const classes = classResult.stdout.match(/=\s*"([^"]*)",\s*"([^"]*)"/)
|
||||
const title = titleResult.stdout.match(XPROP_TITLE_RE)?.[1]
|
||||
const classes = classResult.stdout.match(XPROP_CLASS_RE)
|
||||
|
||||
return {
|
||||
available: true,
|
||||
@@ -521,12 +529,12 @@ export class LinuxX11RunnerService {
|
||||
env: this.getX11Env(),
|
||||
}).catch(() => ({ stdout: '', stderr: '' }))
|
||||
|
||||
const match = stdout.split(/\r?\n/).find((line) => {
|
||||
return line.trim().split(/\s+/)[2] === String(pid)
|
||||
const match = stdout.split(CRLF_SPLIT_RE).find((line) => {
|
||||
return line.trim().split(WHITESPACE_SPLIT_RE)[2] === String(pid)
|
||||
})
|
||||
|
||||
if (match) {
|
||||
return match.trim().split(/\s+/)[0]
|
||||
return match.trim().split(WHITESPACE_SPLIT_RE)[0]
|
||||
}
|
||||
|
||||
await sleep(250)
|
||||
@@ -649,7 +657,7 @@ export class LinuxX11RunnerService {
|
||||
return ''
|
||||
}
|
||||
|
||||
return this.observationBaseUrl.pathname.replace(/\/$/, '')
|
||||
return this.observationBaseUrl.pathname.replace(TRAILING_SLASH_RE, '')
|
||||
}
|
||||
|
||||
private buildObservationPublicUrl(fileName: string) {
|
||||
@@ -658,7 +666,7 @@ export class LinuxX11RunnerService {
|
||||
}
|
||||
|
||||
const basePath = this.getObservationBasePath()
|
||||
const pathName = `${basePath}/${this.observationToken}/${fileName}`.replace(/\/{2,}/g, '/')
|
||||
const pathName = `${basePath}/${this.observationToken}/${fileName}`.replace(DUPLICATE_SLASH_RE, '/')
|
||||
return new URL(pathName, this.observationBaseUrl).toString()
|
||||
}
|
||||
|
||||
|
||||
@@ -36,10 +36,11 @@ export interface BrowserAgentTaskResult {
|
||||
|
||||
const computerUseRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../bin/computer_use')
|
||||
const cliModule = 'google_computer_use.cli'
|
||||
const CRLF_SPLIT_RE = /\r?\n/u
|
||||
|
||||
function trimNonEmptyLines(text: string) {
|
||||
return text
|
||||
.split(/\r?\n/u)
|
||||
.split(CRLF_SPLIT_RE)
|
||||
.map(line => line.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ const CODE_CLI_CANDIDATES = [
|
||||
'code-insiders',
|
||||
'cursor',
|
||||
] as const
|
||||
const TYPESCRIPT_ERROR_LINE_RE = /^([^(]+)\((\d+),(\d+)\): +(error|warning) +(TS\d+): +(\S.*)$/
|
||||
const VUE_TSC_ERROR_LINE_RE = /^([^:]+):(\d+):(\d+) +- +(error|warning) +(TS\d+): +(\S.*)$/
|
||||
|
||||
/**
|
||||
* Attempt to detect the active `code` CLI binary.
|
||||
@@ -380,10 +382,9 @@ export function registerVscodeTools({ server, runtime, executeTerminalCommand }:
|
||||
const output = truncated ? lines.slice(0, limit).join('\n') : combined
|
||||
|
||||
// Parse TypeScript-style error lines: "src/foo.ts(10,5): error TS2345: ..."
|
||||
const errorPattern = /^([^(]+)\((\d+),(\d+)\): +(error|warning) +(TS\d+): +(\S.*)$/
|
||||
const problems: VscodeProblem[] = []
|
||||
for (const line of lines) {
|
||||
const match = line.match(errorPattern)
|
||||
const match = line.match(TYPESCRIPT_ERROR_LINE_RE)
|
||||
if (match) {
|
||||
problems.push({
|
||||
file: match[1],
|
||||
@@ -397,9 +398,8 @@ export function registerVscodeTools({ server, runtime, executeTerminalCommand }:
|
||||
}
|
||||
|
||||
// Also try "file:line:col - error TS..." format (vue-tsc)
|
||||
const vueTscPattern = /^([^:]+):(\d+):(\d+) +- +(error|warning) +(TS\d+): +(\S.*)$/
|
||||
for (const line of lines) {
|
||||
const match = line.match(vueTscPattern)
|
||||
const match = line.match(VUE_TSC_ERROR_LINE_RE)
|
||||
if (match && !problems.some(p => p.file === match[1] && p.line === Number(match[2]))) {
|
||||
problems.push({
|
||||
file: match[1],
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { ComputerUseConfig, DesktopExecutor, TerminalRunner } from '../types'
|
||||
import type { CdpBridgeManager } from './cdp-manager'
|
||||
|
||||
import { platform } from 'node:process'
|
||||
|
||||
import { BrowserDomExtensionBridge } from '../browser-dom/extension-bridge'
|
||||
import { resolveComputerUseConfig } from '../config'
|
||||
import { createDryRunExecutor } from '../executors/dry-run'
|
||||
@@ -34,6 +36,10 @@ function createExecutor(config: ComputerUseConfig, options: ComputerUseServerOpt
|
||||
if (options.executorFactory)
|
||||
return options.executorFactory(config)
|
||||
|
||||
if (config.executor === 'macos-local' && platform !== 'darwin') {
|
||||
throw new Error(`macos-local executor requires a darwin host, current platform is ${platform}`)
|
||||
}
|
||||
|
||||
if (config.executor === 'linux-x11')
|
||||
return createLinuxX11Executor(config)
|
||||
if (config.executor === 'macos-local')
|
||||
|
||||
@@ -7,6 +7,8 @@ import type {
|
||||
TerminalState,
|
||||
} from './types'
|
||||
|
||||
import process from 'node:process'
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { appendFile, mkdir } from 'node:fs/promises'
|
||||
|
||||
|
||||
@@ -612,32 +612,13 @@ const KNOWN_BROWSERS = new Set([
|
||||
'chromium',
|
||||
'orion',
|
||||
])
|
||||
const APP_SUFFIX_RE = /\.app$/u
|
||||
|
||||
/** Check if the foreground app is a known web browser. */
|
||||
function isBrowserApp(appName: string | undefined): boolean {
|
||||
if (!appName)
|
||||
return false
|
||||
return KNOWN_BROWSERS.has(appName.trim().toLowerCase().replace(/\.app$/u, ''))
|
||||
}
|
||||
|
||||
const TERMINAL_APPS = new Set([
|
||||
'terminal',
|
||||
'iterm2',
|
||||
'iterm',
|
||||
'alacritty',
|
||||
'kitty',
|
||||
'wezterm',
|
||||
'hyper',
|
||||
'warp',
|
||||
'rio',
|
||||
'ghostty',
|
||||
])
|
||||
|
||||
/** Check if the foreground app is a known terminal emulator. */
|
||||
function isTerminalApp(appName: string | undefined): boolean {
|
||||
if (!appName)
|
||||
return false
|
||||
return TERMINAL_APPS.has(appName.trim().toLowerCase().replace(/\.app$/u, ''))
|
||||
return KNOWN_BROWSERS.has(appName.trim().toLowerCase().replace(APP_SUFFIX_RE, ''))
|
||||
}
|
||||
|
||||
const KNOWN_TUI_PROGRAMS = [
|
||||
|
||||
@@ -38,14 +38,14 @@ export function hasMeaningfulTaskMemoryExtraction(ext: TaskMemoryExtraction): bo
|
||||
return isValidStatus(ext.status)
|
||||
|| isNonEmptyString(ext.goal)
|
||||
|| isNonEmptyString(ext.currentStep)
|
||||
|| isStringArray(ext.confirmedFacts) && ext.confirmedFacts.length > 0
|
||||
|| isArtifactArray(ext.artifacts) && ext.artifacts.length > 0
|
||||
|| isStringArray(ext.blockers) && ext.blockers.length > 0
|
||||
|| (isStringArray(ext.confirmedFacts) && ext.confirmedFacts.length > 0)
|
||||
|| (isArtifactArray(ext.artifacts) && ext.artifacts.length > 0)
|
||||
|| (isStringArray(ext.blockers) && ext.blockers.length > 0)
|
||||
|| isNonEmptyString(ext.nextStep)
|
||||
|| isStringArray(ext.plan) && ext.plan.length > 0
|
||||
|| isStringArray(ext.workingAssumptions) && ext.workingAssumptions.length > 0
|
||||
|| (isStringArray(ext.plan) && ext.plan.length > 0)
|
||||
|| (isStringArray(ext.workingAssumptions) && ext.workingAssumptions.length > 0)
|
||||
|| isNonEmptyString(ext.recentFailureReason)
|
||||
|| isStringArray(ext.completionCriteria) && ext.completionCriteria.length > 0
|
||||
|| (isStringArray(ext.completionCriteria) && ext.completionCriteria.length > 0)
|
||||
|| ext.newTask === true
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ let nodePtyLoadError: string | undefined
|
||||
|
||||
const NODE_PTY_MODULE = 'node-pty'
|
||||
const requireNodeModule = createRequire(import.meta.url)
|
||||
const PTY_LINE_SPLIT_RE = /\r?\n/
|
||||
|
||||
function stringifyLoadError(error: unknown) {
|
||||
return error instanceof Error ? error.stack || error.message : String(error)
|
||||
@@ -158,7 +159,7 @@ export async function createPtySession(
|
||||
|
||||
pty.onData((data: string) => {
|
||||
// Split on newlines and append to scrollback buffer
|
||||
const lines = data.split(/\r?\n/)
|
||||
const lines = data.split(PTY_LINE_SPLIT_RE)
|
||||
for (const line of lines) {
|
||||
instance.buffer.push(line)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export function createTestConfig(overrides: Partial<ComputerUseConfig> = {}): Co
|
||||
remoteSshHost: '20.196.212.37',
|
||||
remoteSshUser: 'airi',
|
||||
remoteSshPort: 22,
|
||||
remoteRunnerCommand: '~/.local/bin/airi-desktop-runner',
|
||||
remoteRunnerCommand: '$HOME/.local/bin/airi-desktop-runner',
|
||||
remoteDisplaySize: {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
|
||||
@@ -155,6 +155,7 @@ export function resolveStepAction(step: WorkflowStepTemplate): ActionInvocation
|
||||
return { kind: 'wait', input: { durationMs: step.params.durationMs as number, captureAfter: true } }
|
||||
case 'evaluate':
|
||||
case 'summarize':
|
||||
return undefined
|
||||
// PTY step family — handled by the engine's PTY execution path, not resolveStepAction
|
||||
case 'pty_send_input':
|
||||
case 'pty_read_screen':
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
Reference in New Issue
Block a user