Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/universal-client.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Universal client
on:
push:
branches: [main]
pull_request:
paths: ['src/**', 'scripts/postbuild.mjs', 'tests/**', 'apps/universal/**', 'package*.json', 'tsconfig.json', '.github/workflows/universal-client.yml']
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: |
package-lock.json
apps/universal/package-lock.json
- run: npm ci
- run: npm run build
- run: npm test
- run: npm ci --prefix apps/universal
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
- run: npm run typecheck --prefix apps/universal
- run: npm run export --prefix apps/universal
env:
EXPO_OFFLINE: '1'
- uses: actions/upload-artifact@v4
with:
name: monkey-universal-export
path: apps/universal/dist
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ MonkeyElectron/dist-electron/
# Never commit personal data
# ~/.monkey-cli/ lives outside this repo, but just in case:
.monkey-cli/

# Universal client build output
apps/universal/.expo/
apps/universal/ios/
apps/universal/android/
apps/universal/release/
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,30 @@ Monkey remembers things across sessions. It stores knowledge in `~/.monkey-cli/m
- **`/clean`** — full cleanup: stale sessions + LLM-reviewed memory deduplication
- **Safety guard** — all deletions are restricted to `~/.monkey-cli/` only (path validation + traversal protection)

## 📱 Universal client (iOS / Android / Web / Desktop)

The new Expo client lives in `apps/universal`. It connects to your own Monkey host
using authenticated WebSockets; agent tools still execute on that host.

```bash
npm ci
npm run build
npm ci --prefix apps/universal
npm run export:web --prefix apps/universal
# Configure the host with `monkey` first, then:
npm run serve
```

Open `http://127.0.0.1:8787`, then enter the connection key from
`~/.monkey-cli/server-token`. Phones need a reachable HTTPS host address.
The client supports shared sessions, streaming, tool approvals, model switching,
image/text attachments, and reconnect recovery. It requires Node 22.13+.

See [多端改造计划、运行方式与验收边界](docs/universal-client-plan.md) for native
builds, desktop packaging, migration, HTTPS, and validation status. Native store
releases still require signing and device testing. The legacy clients below are
preserved; stop them before using the new service against the same session folder.

## 🍎 macOS Native App

Monkey also comes as a native macOS app — no terminal needed.
Expand Down
224 changes: 224 additions & 0 deletions apps/universal/App.tsx

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions apps/universal/app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ExpoConfig } from 'expo/config'
const config: ExpoConfig = {
name: 'Monkey', slug: 'monkey', version: '0.3.0', scheme: 'monkey',
orientation: 'default', userInterfaceStyle: 'light', icon: './assets/icon.png',
ios: { supportsTablet: true, bundleIdentifier: 'com.monkey.agent', infoPlist: {
NSLocalNetworkUsageDescription: '连接你局域网中的 Monkey 服务。',
NSAppTransportSecurity: { NSAllowsLocalNetworking: true },
} },
android: { package: 'com.monkey.agent', softwareKeyboardLayoutMode: 'resize', predictiveBackGestureEnabled: true },
web: { bundler: 'metro', output: 'single', name: 'Monkey', favicon: './assets/icon.png' },
plugins: [['expo-build-properties', { android: { usesCleartextTraffic: process.env.MONKEY_ALLOW_LAN_HTTP === '1' } }], 'expo-status-bar', 'expo-secure-store', ['expo-image-picker', { photosPermission: '选择图片发送给 Monkey。', cameraPermission: false, microphonePermission: false }]],
}
export default config
Binary file added apps/universal/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions apps/universal/desktop/builder.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"appId": "com.monkey.agent.desktop",
"productName": "Monkey",
"directories": {
"output": "release",
"app": "desktop"
},
"files": [
"main.cjs",
"package.json"
],
"mac": {
"target": [
"dmg"
]
},
"win": {
"target": [
"nsis"
]
},
"linux": {
"target": [
"AppImage"
]
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
}
}
27 changes: 27 additions & 0 deletions apps/universal/desktop/main.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { app, BrowserWindow, shell, session, dialog } = require('electron')
// The same exported Expo UI is served by `monkey serve` on the selected host.
const address = process.env.MONKEY_DESKTOP_URL || 'http://127.0.0.1:8787'
let url
try {
url = new URL(address)
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) throw new Error()
if (url.protocol === 'http:' && !['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)) throw new Error()
} catch { console.error('MONKEY_DESKTOP_URL must be HTTPS, or loopback HTTP.'); app.exit(1) }
app.whenReady().then(() => {
session.defaultSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false))
function createWindow() {
const window = new BrowserWindow({ width: 1180, height: 840, minWidth: 390, minHeight: 600, title: 'Monkey', backgroundColor: '#F8F7F3', webPreferences: { nodeIntegration: false, contextIsolation: true, sandbox: true } })
window.webContents.setWindowOpenHandler(({ url: target }) => {
if (/^https?:\/\//i.test(target)) void shell.openExternal(target)
return { action: 'deny' }
})
window.webContents.on('will-navigate', (event, target) => { if (new URL(target).origin !== url.origin) event.preventDefault() })
window.webContents.on('did-fail-load', (_event, code, description, _url, isMainFrame) => {
if (isMainFrame && code !== -3) void dialog.showMessageBox(window, { type: 'error', title: '无法连接 Monkey', message: '请先启动 Monkey 服务', detail: `在仓库中导出 Web 客户端并运行 monkey serve,然后重新打开应用。\n\n${description}` })
})
void window.loadURL(url.toString()).catch(() => {})
}
createWindow()
app.on('activate', () => { if (!BrowserWindow.getAllWindows().length) createWindow() })
})
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit() })
7 changes: 7 additions & 0 deletions apps/universal/desktop/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "monkey-desktop",
"version": "0.3.0",
"description": "Monkey universal desktop client",
"private": true,
"main": "main.cjs"
}
9 changes: 9 additions & 0 deletions apps/universal/eas.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"cli": { "version": ">= 16.0.0", "appVersionSource": "local" },
"build": {
"preview": { "distribution": "internal", "android": { "buildType": "apk" } },
"simulator": { "extends": "preview", "ios": { "simulator": true } },
"production": { "autoIncrement": true }
},
"submit": { "production": {} }
}
3 changes: 3 additions & 0 deletions apps/universal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { registerRootComponent } from 'expo'
import App from './App'
registerRootComponent(App)
Loading
Loading