feat(cli): integrate clap_complete for shell command completions (#49)
* feat(daemon): introduce daemon host configuration management - Updated the daemon service to support a new configuration structure for managing tray visibility. - Added functions to load and save daemon host configuration from a JSON file. - Implemented Tauri commands to retrieve and update the daemon host configuration. - Enhanced the service management logic to account for legacy application labels and improve compatibility on macOS. - Refactored executable resolution logic to streamline the process of locating the daemon executable across platforms. * feat(daemon): enhance daemon host configuration with tray visibility settings - Added functionality to load and save daemon host configuration, specifically for managing the visibility of the daemon tray icon. - Implemented UI components in both DaemonHealthPanel and TauriCommandsPanel to toggle the tray visibility setting. - Integrated Tauri commands to retrieve and update the daemon host configuration, improving user control over the daemon's display options. - Enhanced loading states and error handling for better user feedback during configuration updates. * fix(local-ai): update default model IDs for local AI configuration - Changed default model IDs from `qwen2.5:1.5b` and `qwen3-vl:2b` to `gemma3:4b-it-qat` for chat and vision models, ensuring consistency in local AI settings. * feat(core): enhance macOS service management and CLI thread stack size - Added dynamic configuration for thread stack size in the CLI, allowing customization via the `OPENHUMAN_CORE_THREAD_STACK_SIZE` environment variable. - Improved macOS service management by validating the LaunchAgent plist and ensuring it is installed before starting the service. - Enhanced error handling and logging for service loading and plist validation, improving user feedback and reliability. * feat(app): initialize Tauri + React + TypeScript project structure - Added essential project files including package.json, tsconfig.json, and Vite configuration for a Tauri application using React and TypeScript. - Created initial HTML template and CSS styles for the application interface. - Included .gitignore to exclude build artifacts and environment-specific files. - Established basic README documentation to guide setup and development. * feat(cli): refactor command structure for core CLI - Replaced the existing CLI command structure with a new design using `clap` for better organization and extensibility. - Introduced a `CoreCli` struct with subcommands for various operations including server management, health checks, and configuration settings. - Updated command handling to support new subcommands for settings and accessibility operations, enhancing the CLI's functionality. - Modified the core process handling to reflect the new command structure, ensuring compatibility with the updated CLI design. * chore(eslint): add 'app/**' to ignored paths in ESLint configuration - Updated the ESLint configuration to include the 'app/**' directory in the list of ignored paths, ensuring that files in this directory are not linted during the development process. * chore(prettier): add 'app' directory to .prettierignore - Updated the .prettierignore file to include the 'app' directory, preventing formatting checks on files within this path during development. * feat(cli): integrate clap_complete for shell command completions - Added support for generating shell completion scripts using `clap_complete`, enhancing the CLI's usability. - Introduced new subcommands for generating completions and updated command structures to accommodate this feature. - Implemented a new `capture_image_ref` command in the accessibility module for direct image reference capture. - Enhanced the `Tools` command structure to include screenshot functionalities, improving CLI tool management.
@@ -1,6 +1,7 @@
|
||||
node_modules
|
||||
dist
|
||||
coverage
|
||||
app
|
||||
src-tauri
|
||||
rust-core
|
||||
skills
|
||||
|
||||
@@ -1169,6 +1169,15 @@ dependencies = [
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
@@ -5441,6 +5450,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"console",
|
||||
"cron",
|
||||
"dialoguer",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,7 @@
|
||||
# Tauri + React + Typescript
|
||||
|
||||
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri + React + Typescript</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "openhuman",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.4",
|
||||
"@tauri-apps/cli": "^2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "openhuman"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "openhuman_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 974 B |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 903 B |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,14 @@
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![greet])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
openhuman_lib::run()
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "openhuman",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.tinyhumansai.openhuman",
|
||||
"build": {
|
||||
"beforeDevCommand": "yarn dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "yarn build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "openhuman",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
.logo.vite:hover {
|
||||
filter: drop-shadow(0 0 2em #747bff);
|
||||
}
|
||||
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafb);
|
||||
}
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
background-color: #f6f6f6;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
padding-top: 10vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: 0.75s;
|
||||
}
|
||||
|
||||
.logo.tauri:hover {
|
||||
filter: drop-shadow(0 0 2em #24c8db);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: #0f0f0f;
|
||||
background-color: #ffffff;
|
||||
transition: border-color 0.25s;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #396cd8;
|
||||
}
|
||||
button:active {
|
||||
border-color: #396cd8;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#greet-input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color: #f6f6f6;
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #24c8db;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
color: #ffffff;
|
||||
background-color: #0f0f0f98;
|
||||
}
|
||||
button:active {
|
||||
background-color: #0f0f0f69;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useState } from "react";
|
||||
import reactLogo from "./assets/react.svg";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
const [greetMsg, setGreetMsg] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
|
||||
async function greet() {
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
setGreetMsg(await invoke("greet", { name }));
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="container">
|
||||
<h1>Welcome to Tauri + React</h1>
|
||||
|
||||
<div className="row">
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src="/vite.svg" className="logo vite" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://tauri.app" target="_blank">
|
||||
<img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
</div>
|
||||
<p>Click on the Tauri, Vite, and React logos to learn more.</p>
|
||||
|
||||
<form
|
||||
className="row"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
greet();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
id="greet-input"
|
||||
onChange={(e) => setName(e.currentTarget.value)}
|
||||
placeholder="Enter a name..."
|
||||
/>
|
||||
<button type="submit">Greet</button>
|
||||
</form>
|
||||
<p>{greetMsg}</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [react()],
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
// 1. prevent Vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host
|
||||
? {
|
||||
protocol: "ws",
|
||||
host,
|
||||
port: 1421,
|
||||
}
|
||||
: undefined,
|
||||
watch: {
|
||||
// 3. tell Vite to ignore watching `src-tauri`
|
||||
ignored: ["**/src-tauri/**"],
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -26,6 +26,7 @@ export default [
|
||||
'**/target/**',
|
||||
'dist/**',
|
||||
'coverage/**',
|
||||
'app/**',
|
||||
'src-tauri/**',
|
||||
'rust-core/**',
|
||||
'skills/**',
|
||||
|
||||
@@ -62,6 +62,7 @@ rustls-pki-types = "1.14.0"
|
||||
tokio-rustls = "0.26.4"
|
||||
webpki-roots = "1.0.6"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
clap_complete = "4.5"
|
||||
lettre = { version = "0.11.19", default-features = false, features = ["builder", "smtp-transport", "rustls-tls"] }
|
||||
mail-parser = "0.11.2"
|
||||
async-imap = { version = "0.11", features = ["runtime-tokio"], default-features = false }
|
||||
|
||||
@@ -1,334 +1,7 @@
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "openhuman")]
|
||||
#[command(about = "OpenHuman core CLI")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Command {
|
||||
/// Run JSON-RPC server
|
||||
Serve {
|
||||
#[arg(long)]
|
||||
port: Option<u16>,
|
||||
},
|
||||
/// Check core health
|
||||
Ping,
|
||||
/// Print core version
|
||||
Version,
|
||||
/// Get health snapshot
|
||||
Health,
|
||||
/// Get runtime flags
|
||||
RuntimeFlags,
|
||||
/// Get security policy info
|
||||
SecurityPolicy,
|
||||
/// Generic JSON-RPC style method call
|
||||
Call {
|
||||
#[arg(long)]
|
||||
method: String,
|
||||
#[arg(long, default_value = "{}")]
|
||||
params: String,
|
||||
},
|
||||
/// Config operations
|
||||
Config {
|
||||
#[command(subcommand)]
|
||||
command: ConfigCommand,
|
||||
},
|
||||
/// Service operations
|
||||
Service {
|
||||
#[command(subcommand)]
|
||||
command: ServiceCommand,
|
||||
},
|
||||
/// Doctor operations
|
||||
Doctor {
|
||||
#[command(subcommand)]
|
||||
command: DoctorCommand,
|
||||
},
|
||||
/// Integrations operations
|
||||
Integrations {
|
||||
#[command(subcommand)]
|
||||
command: IntegrationsCommand,
|
||||
},
|
||||
/// Send one-shot agent message
|
||||
AgentChat(AgentChatArgs),
|
||||
/// Hardware operations
|
||||
Hardware {
|
||||
#[command(subcommand)]
|
||||
command: HardwareCommand,
|
||||
},
|
||||
/// Encrypt a secret
|
||||
Encrypt { plaintext: String },
|
||||
/// Decrypt a secret
|
||||
Decrypt { ciphertext: String },
|
||||
/// Toggle browser allow-all runtime flag
|
||||
BrowserAllowAll {
|
||||
#[arg(long)]
|
||||
enabled: bool,
|
||||
},
|
||||
/// Refresh model catalog
|
||||
ModelsRefresh {
|
||||
#[arg(long)]
|
||||
provider: Option<String>,
|
||||
#[arg(long, default_value_t = false)]
|
||||
force: bool,
|
||||
},
|
||||
/// Migrate OpenClaw memory
|
||||
MigrateOpenclaw {
|
||||
#[arg(long)]
|
||||
source_workspace: Option<String>,
|
||||
#[arg(long, default_value_t = true)]
|
||||
dry_run: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum ConfigCommand {
|
||||
/// Get full config snapshot
|
||||
Get,
|
||||
/// Update model settings with a JSON object
|
||||
UpdateModel {
|
||||
#[arg(long)]
|
||||
json: String,
|
||||
},
|
||||
/// Update memory settings with a JSON object
|
||||
UpdateMemory {
|
||||
#[arg(long)]
|
||||
json: String,
|
||||
},
|
||||
/// Update gateway settings with a JSON object
|
||||
UpdateGateway {
|
||||
#[arg(long)]
|
||||
json: String,
|
||||
},
|
||||
/// Update runtime settings with a JSON object
|
||||
UpdateRuntime {
|
||||
#[arg(long)]
|
||||
json: String,
|
||||
},
|
||||
/// Update browser settings with a JSON object
|
||||
UpdateBrowser {
|
||||
#[arg(long)]
|
||||
json: String,
|
||||
},
|
||||
/// Replace tunnel settings with a JSON object
|
||||
UpdateTunnel {
|
||||
#[arg(long)]
|
||||
json: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum ServiceCommand {
|
||||
Install,
|
||||
Start,
|
||||
Stop,
|
||||
Status,
|
||||
Reinstall,
|
||||
Uninstall,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum DoctorCommand {
|
||||
/// Run doctor checks
|
||||
Report,
|
||||
/// Probe model catalog
|
||||
Models {
|
||||
#[arg(long)]
|
||||
provider: Option<String>,
|
||||
#[arg(long, default_value_t = true)]
|
||||
use_cache: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum IntegrationsCommand {
|
||||
/// List integrations
|
||||
List,
|
||||
/// Get one integration info
|
||||
Info {
|
||||
#[arg(long)]
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum HardwareCommand {
|
||||
/// Discover connected hardware
|
||||
Discover,
|
||||
/// Introspect one device path
|
||||
Introspect {
|
||||
#[arg(long)]
|
||||
path: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct AgentChatArgs {
|
||||
message: String,
|
||||
#[arg(long)]
|
||||
provider: Option<String>,
|
||||
#[arg(long)]
|
||||
model: Option<String>,
|
||||
#[arg(long)]
|
||||
temperature: Option<f64>,
|
||||
}
|
||||
|
||||
fn parse_json_arg(raw: &str) -> Result<serde_json::Value, String> {
|
||||
serde_json::from_str(raw).map_err(|e| format!("invalid JSON for --json/--params: {e}"))
|
||||
}
|
||||
|
||||
async fn call_local(method: &str, params: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
openhuman_core::core_server::call_method(method, params).await
|
||||
}
|
||||
|
||||
async fn execute(cli: Cli) -> Result<serde_json::Value, String> {
|
||||
match cli.command {
|
||||
Command::Serve { port } => openhuman_core::core_server::run_server(port)
|
||||
.await
|
||||
.map(|_| serde_json::Value::Null)
|
||||
.map_err(|e| format!("serve failed: {e}")),
|
||||
Command::Ping => call_local("core.ping", json!({})).await,
|
||||
Command::Version => call_local("core.version", json!({})).await,
|
||||
Command::Health => call_local("openhuman.health_snapshot", json!({})).await,
|
||||
Command::RuntimeFlags => call_local("openhuman.get_runtime_flags", json!({})).await,
|
||||
Command::SecurityPolicy => call_local("openhuman.security_policy_info", json!({})).await,
|
||||
Command::Call { method, params } => call_local(&method, parse_json_arg(¶ms)?).await,
|
||||
Command::Config { command } => match command {
|
||||
ConfigCommand::Get => call_local("openhuman.get_config", json!({})).await,
|
||||
ConfigCommand::UpdateModel { json } => {
|
||||
call_local("openhuman.update_model_settings", parse_json_arg(&json)?).await
|
||||
}
|
||||
ConfigCommand::UpdateMemory { json } => {
|
||||
call_local("openhuman.update_memory_settings", parse_json_arg(&json)?).await
|
||||
}
|
||||
ConfigCommand::UpdateGateway { json } => {
|
||||
call_local("openhuman.update_gateway_settings", parse_json_arg(&json)?).await
|
||||
}
|
||||
ConfigCommand::UpdateRuntime { json } => {
|
||||
call_local("openhuman.update_runtime_settings", parse_json_arg(&json)?).await
|
||||
}
|
||||
ConfigCommand::UpdateBrowser { json } => {
|
||||
call_local("openhuman.update_browser_settings", parse_json_arg(&json)?).await
|
||||
}
|
||||
ConfigCommand::UpdateTunnel { json } => {
|
||||
call_local("openhuman.update_tunnel_settings", parse_json_arg(&json)?).await
|
||||
}
|
||||
},
|
||||
Command::Service { command } => match command {
|
||||
ServiceCommand::Install => call_local("openhuman.service_install", json!({})).await,
|
||||
ServiceCommand::Start => call_local("openhuman.service_start", json!({})).await,
|
||||
ServiceCommand::Stop => call_local("openhuman.service_stop", json!({})).await,
|
||||
ServiceCommand::Status => call_local("openhuman.service_status", json!({})).await,
|
||||
ServiceCommand::Reinstall => {
|
||||
call_local("openhuman.service_uninstall", json!({})).await?;
|
||||
call_local("openhuman.service_install", json!({})).await
|
||||
}
|
||||
ServiceCommand::Uninstall => call_local("openhuman.service_uninstall", json!({})).await,
|
||||
},
|
||||
Command::Doctor { command } => match command {
|
||||
DoctorCommand::Report => call_local("openhuman.doctor_report", json!({})).await,
|
||||
DoctorCommand::Models {
|
||||
provider,
|
||||
use_cache,
|
||||
} => {
|
||||
call_local(
|
||||
"openhuman.doctor_models",
|
||||
json!({
|
||||
"provider_override": provider,
|
||||
"use_cache": use_cache,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
Command::Integrations { command } => match command {
|
||||
IntegrationsCommand::List => call_local("openhuman.list_integrations", json!({})).await,
|
||||
IntegrationsCommand::Info { name } => {
|
||||
call_local("openhuman.get_integration_info", json!({ "name": name })).await
|
||||
}
|
||||
},
|
||||
Command::AgentChat(args) => {
|
||||
call_local(
|
||||
"openhuman.agent_chat",
|
||||
json!({
|
||||
"message": args.message,
|
||||
"provider_override": args.provider,
|
||||
"model_override": args.model,
|
||||
"temperature": args.temperature,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
}
|
||||
Command::Hardware { command } => match command {
|
||||
HardwareCommand::Discover => call_local("openhuman.hardware_discover", json!({})).await,
|
||||
HardwareCommand::Introspect { path } => {
|
||||
call_local("openhuman.hardware_introspect", json!({ "path": path })).await
|
||||
}
|
||||
},
|
||||
Command::Encrypt { plaintext } => {
|
||||
call_local(
|
||||
"openhuman.encrypt_secret",
|
||||
json!({ "plaintext": plaintext }),
|
||||
)
|
||||
.await
|
||||
}
|
||||
Command::Decrypt { ciphertext } => {
|
||||
call_local(
|
||||
"openhuman.decrypt_secret",
|
||||
json!({ "ciphertext": ciphertext }),
|
||||
)
|
||||
.await
|
||||
}
|
||||
Command::BrowserAllowAll { enabled } => {
|
||||
call_local(
|
||||
"openhuman.set_browser_allow_all",
|
||||
json!({ "enabled": enabled }),
|
||||
)
|
||||
.await
|
||||
}
|
||||
Command::ModelsRefresh { provider, force } => {
|
||||
call_local(
|
||||
"openhuman.models_refresh",
|
||||
json!({
|
||||
"provider_override": provider,
|
||||
"force": force,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
}
|
||||
Command::MigrateOpenclaw {
|
||||
source_workspace,
|
||||
dry_run,
|
||||
} => {
|
||||
call_local(
|
||||
"openhuman.migrate_openclaw",
|
||||
json!({
|
||||
"source_workspace": source_workspace,
|
||||
"dry_run": dry_run,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
match execute(cli).await {
|
||||
Ok(value) => {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&value).unwrap_or_else(|_| "null".to_string())
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
if let Err(err) = openhuman_core::run_core_from_args(&args) {
|
||||
eprintln!("{err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,15 @@ use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
||||
use clap::{Args, CommandFactory, Parser, Subcommand};
|
||||
use clap_complete::{generate, Shell};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::openhuman::config::Config;
|
||||
use crate::openhuman::cron;
|
||||
@@ -16,6 +22,7 @@ use crate::openhuman::local_ai::{
|
||||
Suggestion,
|
||||
};
|
||||
use crate::openhuman::security::{SecretStore, SecurityPolicy};
|
||||
use crate::openhuman::tools::{ScreenshotTool, Tool};
|
||||
use crate::openhuman::{
|
||||
accessibility, doctor, hardware, integrations, migration, onboard, service,
|
||||
};
|
||||
@@ -23,9 +30,10 @@ use chrono::Utc;
|
||||
|
||||
pub use crate::openhuman::accessibility::{
|
||||
AccessibilityStatus, AutocompleteCommitParams, AutocompleteCommitResult,
|
||||
AutocompleteSuggestParams, AutocompleteSuggestResult, CaptureNowResult, InputActionParams,
|
||||
InputActionResult, PermissionRequestParams, PermissionStatus, SessionStatus,
|
||||
StartSessionParams, StopSessionParams, VisionFlushResult, VisionRecentResult,
|
||||
AutocompleteSuggestParams, AutocompleteSuggestResult, CaptureImageRefResult, CaptureNowResult,
|
||||
InputActionParams, InputActionResult, PermissionRequestParams, PermissionState,
|
||||
PermissionStatus, SessionStatus, StartSessionParams, StopSessionParams, VisionFlushResult,
|
||||
VisionRecentResult,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -1067,6 +1075,15 @@ async fn dispatch(
|
||||
))
|
||||
}
|
||||
|
||||
"openhuman.accessibility_capture_image_ref" => {
|
||||
let result: CaptureImageRefResult =
|
||||
accessibility::global_engine().capture_image_ref_test().await;
|
||||
to_json_value(command_response(
|
||||
result,
|
||||
vec!["accessibility direct image_ref capture requested".to_string()],
|
||||
))
|
||||
}
|
||||
|
||||
"openhuman.accessibility_input_action" => {
|
||||
let payload: InputActionParams = parse_params(params)?;
|
||||
let result = accessibility::global_engine().input_action(payload).await?;
|
||||
@@ -1211,29 +1228,905 @@ pub async fn run_server(port: Option<u16>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_from_cli_args(args: &[String]) -> Result<()> {
|
||||
let mut port: Option<u16> = None;
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "openhuman-core")]
|
||||
#[command(about = "OpenHuman core CLI")]
|
||||
#[command(arg_required_else_help = true)]
|
||||
struct CoreCli {
|
||||
#[command(subcommand)]
|
||||
command: CoreCommand,
|
||||
}
|
||||
|
||||
let mut idx = 0;
|
||||
while idx < args.len() {
|
||||
match args[idx].as_str() {
|
||||
"serve" => {}
|
||||
"--port" => {
|
||||
let value = args
|
||||
.get(idx + 1)
|
||||
.ok_or_else(|| anyhow::anyhow!("missing value for --port"))?;
|
||||
port = Some(value.parse::<u16>()?);
|
||||
idx += 1;
|
||||
}
|
||||
_ => {}
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum CoreCommand {
|
||||
/// Run JSON-RPC server
|
||||
#[command(alias = "serve")]
|
||||
Run {
|
||||
#[arg(long)]
|
||||
port: Option<u16>,
|
||||
},
|
||||
/// Check core health
|
||||
Ping,
|
||||
/// Print core version
|
||||
Version,
|
||||
/// Get health snapshot
|
||||
Health,
|
||||
/// Get runtime flags
|
||||
RuntimeFlags,
|
||||
/// Get security policy info
|
||||
SecurityPolicy,
|
||||
/// Generic JSON-RPC style method call
|
||||
Call {
|
||||
#[arg(long)]
|
||||
method: String,
|
||||
#[arg(long, default_value = "{}")]
|
||||
params: String,
|
||||
},
|
||||
/// Generate shell completion scripts
|
||||
Completions {
|
||||
#[arg(value_enum)]
|
||||
shell: Shell,
|
||||
},
|
||||
/// Settings style commands mirroring app settings sections
|
||||
Settings {
|
||||
#[command(subcommand)]
|
||||
command: SettingsCommand,
|
||||
},
|
||||
/// Accessibility automation commands
|
||||
Accessibility {
|
||||
#[command(subcommand)]
|
||||
command: AccessibilityCommand,
|
||||
},
|
||||
/// Tool wrappers for local CLI testing
|
||||
Tools {
|
||||
#[command(subcommand)]
|
||||
command: ToolsCommand,
|
||||
},
|
||||
/// Legacy config operations
|
||||
Config {
|
||||
#[command(subcommand)]
|
||||
command: ConfigCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum SettingsCommand {
|
||||
Model {
|
||||
#[command(subcommand)]
|
||||
command: ModelSettingsCommand,
|
||||
},
|
||||
Memory {
|
||||
#[command(subcommand)]
|
||||
command: MemorySettingsCommand,
|
||||
},
|
||||
Gateway {
|
||||
#[command(subcommand)]
|
||||
command: GatewaySettingsCommand,
|
||||
},
|
||||
Tunnel {
|
||||
#[command(subcommand)]
|
||||
command: TunnelSettingsCommand,
|
||||
},
|
||||
Runtime {
|
||||
#[command(subcommand)]
|
||||
command: RuntimeSettingsCommand,
|
||||
},
|
||||
Browser {
|
||||
#[command(subcommand)]
|
||||
command: BrowserSettingsCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum ModelSettingsCommand {
|
||||
Get,
|
||||
Set(ModelSetArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct ModelSetArgs {
|
||||
#[arg(long)]
|
||||
api_key: Option<String>,
|
||||
#[arg(long)]
|
||||
api_url: Option<String>,
|
||||
#[arg(long)]
|
||||
provider: Option<String>,
|
||||
#[arg(long)]
|
||||
model: Option<String>,
|
||||
#[arg(long)]
|
||||
temperature: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum MemorySettingsCommand {
|
||||
Get,
|
||||
Set(MemorySetArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct MemorySetArgs {
|
||||
#[arg(long)]
|
||||
backend: Option<String>,
|
||||
#[arg(long)]
|
||||
auto_save: Option<bool>,
|
||||
#[arg(long)]
|
||||
embedding_provider: Option<String>,
|
||||
#[arg(long)]
|
||||
embedding_model: Option<String>,
|
||||
#[arg(long)]
|
||||
embedding_dimensions: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum GatewaySettingsCommand {
|
||||
Get,
|
||||
Set(GatewaySetArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct GatewaySetArgs {
|
||||
#[arg(long)]
|
||||
host: Option<String>,
|
||||
#[arg(long)]
|
||||
port: Option<u16>,
|
||||
#[arg(long)]
|
||||
require_pairing: Option<bool>,
|
||||
#[arg(long)]
|
||||
allow_public_bind: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum TunnelSettingsCommand {
|
||||
Get,
|
||||
/// Replace tunnel settings with full JSON payload
|
||||
Set(TunnelSetArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct TunnelSetArgs {
|
||||
#[arg(long)]
|
||||
json: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum RuntimeSettingsCommand {
|
||||
Get,
|
||||
Set(RuntimeSetArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct RuntimeSetArgs {
|
||||
#[arg(long)]
|
||||
kind: Option<String>,
|
||||
#[arg(long)]
|
||||
reasoning_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum BrowserSettingsCommand {
|
||||
Get,
|
||||
Set(BrowserSetArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum AccessibilityCommand {
|
||||
/// Read current accessibility automation status
|
||||
Status,
|
||||
/// Diagnose accessibility permission readiness with actionable fixes
|
||||
Doctor,
|
||||
/// Request all accessibility-related permissions
|
||||
RequestPermissions,
|
||||
/// Request a specific permission kind
|
||||
RequestPermission(RequestPermissionArgs),
|
||||
/// Start a bounded accessibility session
|
||||
StartSession(StartSessionCliArgs),
|
||||
/// Stop the active accessibility session
|
||||
StopSession(StopSessionCliArgs),
|
||||
/// Force an immediate capture sample
|
||||
CaptureNow,
|
||||
/// Directly trigger capture_screen_image_ref (no active session required)
|
||||
CaptureImageRef,
|
||||
/// Fetch recent vision summaries
|
||||
VisionRecent(VisionRecentCliArgs),
|
||||
/// Flush immediate vision summary from latest frame
|
||||
VisionFlush,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct RequestPermissionArgs {
|
||||
/// One of: screen_recording, accessibility, input_monitoring
|
||||
#[arg(long)]
|
||||
permission: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct StartSessionCliArgs {
|
||||
/// Explicit consent required to start
|
||||
#[arg(long, default_value_t = false)]
|
||||
consent: bool,
|
||||
/// Optional session TTL in seconds (bounded server-side)
|
||||
#[arg(long)]
|
||||
ttl_secs: Option<u64>,
|
||||
/// Optional override for screen monitoring
|
||||
#[arg(long)]
|
||||
screen_monitoring: Option<bool>,
|
||||
/// Optional override for device control
|
||||
#[arg(long)]
|
||||
device_control: Option<bool>,
|
||||
/// Optional override for predictive input
|
||||
#[arg(long)]
|
||||
predictive_input: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct StopSessionCliArgs {
|
||||
#[arg(long)]
|
||||
reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct VisionRecentCliArgs {
|
||||
#[arg(long)]
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct BrowserSetArgs {
|
||||
#[arg(long)]
|
||||
enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum ConfigCommand {
|
||||
/// Get full config snapshot
|
||||
Get,
|
||||
/// Update model settings with a JSON object
|
||||
UpdateModel {
|
||||
#[arg(long)]
|
||||
json: String,
|
||||
},
|
||||
/// Update memory settings with a JSON object
|
||||
UpdateMemory {
|
||||
#[arg(long)]
|
||||
json: String,
|
||||
},
|
||||
/// Update gateway settings with a JSON object
|
||||
UpdateGateway {
|
||||
#[arg(long)]
|
||||
json: String,
|
||||
},
|
||||
/// Update runtime settings with a JSON object
|
||||
UpdateRuntime {
|
||||
#[arg(long)]
|
||||
json: String,
|
||||
},
|
||||
/// Update browser settings with a JSON object
|
||||
UpdateBrowser {
|
||||
#[arg(long)]
|
||||
json: String,
|
||||
},
|
||||
/// Replace tunnel settings with a JSON object
|
||||
UpdateTunnel {
|
||||
#[arg(long)]
|
||||
json: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum ToolsCommand {
|
||||
/// List tool wrappers exposed by this CLI
|
||||
List,
|
||||
/// Capture a screenshot using the screenshot tool
|
||||
Screenshot(ToolsScreenshotArgs),
|
||||
/// Capture image ref directly from accessibility engine
|
||||
ScreenshotRef(ToolsScreenshotRefArgs),
|
||||
/// Generic wrapper for available tool commands
|
||||
Run(ToolsRunArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct ToolsScreenshotArgs {
|
||||
/// Optional filename saved under workspace
|
||||
#[arg(long)]
|
||||
filename: Option<String>,
|
||||
/// Optional region for macOS: selection | window
|
||||
#[arg(long)]
|
||||
region: Option<String>,
|
||||
/// Optional output file path (copies or writes PNG to this path)
|
||||
#[arg(long)]
|
||||
output: Option<PathBuf>,
|
||||
/// Include full data URL in JSON output
|
||||
#[arg(long, default_value_t = false)]
|
||||
print_data_url: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct ToolsScreenshotRefArgs {
|
||||
/// Optional output file path (writes PNG to this path)
|
||||
#[arg(long)]
|
||||
output: Option<PathBuf>,
|
||||
/// Include full data URL in JSON output
|
||||
#[arg(long, default_value_t = false)]
|
||||
print_data_url: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct ToolsRunArgs {
|
||||
/// Tool wrapper name: screenshot | screenshot-ref
|
||||
#[arg(long)]
|
||||
name: String,
|
||||
/// JSON arguments payload for selected wrapper
|
||||
#[arg(long, default_value = "{}")]
|
||||
args: String,
|
||||
}
|
||||
|
||||
fn parse_json_arg(raw: &str) -> Result<serde_json::Value, String> {
|
||||
serde_json::from_str(raw).map_err(|e| format!("invalid JSON for --json/--params: {e}"))
|
||||
}
|
||||
|
||||
fn ensure_non_empty_payload(payload: &serde_json::Map<String, serde_json::Value>) -> Result<()> {
|
||||
if payload.is_empty() {
|
||||
return Err(anyhow::anyhow!("no fields provided for set operation"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_data_url(raw: &str) -> Option<String> {
|
||||
raw.lines()
|
||||
.find_map(|line| {
|
||||
let trimmed = line.trim();
|
||||
trimmed
|
||||
.starts_with("data:image/")
|
||||
.then(|| trimmed.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_saved_path(raw: &str) -> Option<PathBuf> {
|
||||
const PREFIX: &str = "Screenshot saved to: ";
|
||||
raw.lines()
|
||||
.find_map(|line| line.strip_prefix(PREFIX).map(PathBuf::from))
|
||||
}
|
||||
|
||||
fn decode_data_url_bytes(data_url: &str) -> Result<Vec<u8>, String> {
|
||||
let (meta, payload) = data_url
|
||||
.split_once(',')
|
||||
.ok_or_else(|| "invalid data URL: missing comma separator".to_string())?;
|
||||
if !meta.starts_with("data:image/") || !meta.ends_with(";base64") {
|
||||
return Err("invalid data URL: expected data:image/*;base64,...".to_string());
|
||||
}
|
||||
BASE64_STANDARD
|
||||
.decode(payload)
|
||||
.map_err(|e| format!("failed to decode base64 image payload: {e}"))
|
||||
}
|
||||
|
||||
fn write_bytes_to_path(path: &Path, bytes: &[u8]) -> Result<(), String> {
|
||||
if let Some(parent) = path.parent() {
|
||||
if !parent.as_os_str().is_empty() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("failed to create output directory: {e}"))?;
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
std::fs::write(path, bytes).map_err(|e| format!("failed to write output file: {e}"))
|
||||
}
|
||||
|
||||
async fn execute_tools_screenshot(args: ToolsScreenshotArgs) -> Result<serde_json::Value, String> {
|
||||
let config = load_openhuman_config().await?;
|
||||
let security = Arc::new(SecurityPolicy::from_config(
|
||||
&config.autonomy,
|
||||
&config.workspace_dir,
|
||||
));
|
||||
let tool = ScreenshotTool::new(security);
|
||||
|
||||
let mut payload = serde_json::Map::new();
|
||||
if let Some(filename) = args.filename {
|
||||
payload.insert("filename".to_string(), json!(filename));
|
||||
}
|
||||
if let Some(region) = args.region {
|
||||
payload.insert("region".to_string(), json!(region));
|
||||
}
|
||||
|
||||
let tool_result = tool
|
||||
.execute(serde_json::Value::Object(payload))
|
||||
.await
|
||||
.map_err(|e| format!("screenshot tool failed to execute: {e}"))?;
|
||||
|
||||
let mut logs = vec!["tools.screenshot executed".to_string()];
|
||||
|
||||
if let Some(output_path) = args.output.as_ref() {
|
||||
if let Some(saved_path) = extract_saved_path(&tool_result.output) {
|
||||
std::fs::copy(&saved_path, output_path).map_err(|e| {
|
||||
format!(
|
||||
"failed to copy screenshot from {} to {}: {e}",
|
||||
saved_path.display(),
|
||||
output_path.display()
|
||||
)
|
||||
})?;
|
||||
logs.push(format!("copied screenshot to {}", output_path.display()));
|
||||
} else if let Some(data_url) = extract_data_url(&tool_result.output) {
|
||||
let bytes = decode_data_url_bytes(&data_url)?;
|
||||
write_bytes_to_path(output_path, &bytes)?;
|
||||
logs.push(format!(
|
||||
"decoded data URL and wrote {} bytes to {}",
|
||||
bytes.len(),
|
||||
output_path.display()
|
||||
));
|
||||
} else {
|
||||
return Err(
|
||||
"screenshot tool response did not contain a saved path or image data URL"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let data_url = extract_data_url(&tool_result.output);
|
||||
let response = json!({
|
||||
"result": {
|
||||
"success": tool_result.success,
|
||||
"error": tool_result.error,
|
||||
"output_path": args.output.as_ref().map(|p| p.display().to_string()),
|
||||
"tool_output": tool_result.output,
|
||||
"data_url": if args.print_data_url { data_url } else { None::<String> },
|
||||
},
|
||||
"logs": logs
|
||||
});
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn execute_tools_screenshot_ref(
|
||||
args: ToolsScreenshotRefArgs,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let raw = call_method("openhuman.accessibility_capture_image_ref", json!({})).await?;
|
||||
let payload: CommandResponse<CaptureImageRefResult> = serde_json::from_value(raw)
|
||||
.map_err(|e| format!("failed to decode accessibility capture_image_ref response: {e}"))?;
|
||||
|
||||
let mut logs = payload.logs;
|
||||
logs.push("tools.screenshot-ref executed".to_string());
|
||||
|
||||
if let Some(output_path) = args.output.as_ref() {
|
||||
if let Some(data_url) = payload.result.image_ref.as_deref() {
|
||||
let bytes = decode_data_url_bytes(data_url)?;
|
||||
write_bytes_to_path(output_path, &bytes)?;
|
||||
logs.push(format!(
|
||||
"decoded image_ref and wrote {} bytes to {}",
|
||||
bytes.len(),
|
||||
output_path.display()
|
||||
));
|
||||
} else {
|
||||
return Err("accessibility capture_image_ref did not return image_ref".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"result": {
|
||||
"ok": payload.result.ok,
|
||||
"mime_type": payload.result.mime_type,
|
||||
"bytes_estimate": payload.result.bytes_estimate,
|
||||
"message": payload.result.message,
|
||||
"output_path": args.output.as_ref().map(|p| p.display().to_string()),
|
||||
"image_ref": if args.print_data_url { payload.result.image_ref } else { None::<String> },
|
||||
},
|
||||
"logs": logs
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_config_snapshot() -> Result<CommandResponse<ConfigSnapshot>, String> {
|
||||
let value = call_method("openhuman.get_config", json!({})).await?;
|
||||
serde_json::from_value::<CommandResponse<ConfigSnapshot>>(value)
|
||||
.map_err(|e| format!("failed to decode config snapshot: {e}"))
|
||||
}
|
||||
|
||||
fn settings_view_response(
|
||||
section: &'static str,
|
||||
snapshot: CommandResponse<ConfigSnapshot>,
|
||||
) -> CommandResponse<serde_json::Value> {
|
||||
let cfg = &snapshot.result.config;
|
||||
let settings = match section {
|
||||
"model" => json!({
|
||||
"api_key": cfg.get("api_key"),
|
||||
"api_url": cfg.get("api_url"),
|
||||
"default_provider": cfg.get("default_provider"),
|
||||
"default_model": cfg.get("default_model"),
|
||||
"default_temperature": cfg.get("default_temperature"),
|
||||
}),
|
||||
"memory" => cfg.get("memory").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"gateway" => cfg.get("gateway").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"tunnel" => cfg.get("tunnel").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"runtime" => cfg.get("runtime").cloned().unwrap_or(serde_json::Value::Null),
|
||||
"browser" => cfg.get("browser").cloned().unwrap_or(serde_json::Value::Null),
|
||||
_ => serde_json::Value::Null,
|
||||
};
|
||||
|
||||
command_response(
|
||||
json!({
|
||||
"section": section,
|
||||
"settings": settings,
|
||||
"workspace_dir": snapshot.result.workspace_dir,
|
||||
"config_path": snapshot.result.config_path,
|
||||
}),
|
||||
snapshot.logs,
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute_core_cli(cli: CoreCli) -> Result<serde_json::Value, String> {
|
||||
match cli.command {
|
||||
CoreCommand::Run { port } => run_server(port)
|
||||
.await
|
||||
.map(|_| serde_json::Value::Null)
|
||||
.map_err(|e| format!("run failed: {e}")),
|
||||
CoreCommand::Ping => call_method("core.ping", json!({})).await,
|
||||
CoreCommand::Version => call_method("core.version", json!({})).await,
|
||||
CoreCommand::Health => call_method("openhuman.health_snapshot", json!({})).await,
|
||||
CoreCommand::RuntimeFlags => call_method("openhuman.get_runtime_flags", json!({})).await,
|
||||
CoreCommand::SecurityPolicy => {
|
||||
call_method("openhuman.security_policy_info", json!({})).await
|
||||
}
|
||||
CoreCommand::Call { method, params } => call_method(&method, parse_json_arg(¶ms)?).await,
|
||||
CoreCommand::Completions { shell } => {
|
||||
let mut cmd = CoreCli::command();
|
||||
let bin_name = cmd.get_name().to_string();
|
||||
generate(shell, &mut cmd, bin_name, &mut io::stdout());
|
||||
Ok(serde_json::Value::Null)
|
||||
}
|
||||
CoreCommand::Settings { command } => match command {
|
||||
SettingsCommand::Model { command } => match command {
|
||||
ModelSettingsCommand::Get => {
|
||||
let snapshot = get_config_snapshot().await?;
|
||||
serde_json::to_value(settings_view_response("model", snapshot))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
ModelSettingsCommand::Set(args) => {
|
||||
let mut payload = serde_json::Map::new();
|
||||
if let Some(v) = args.api_key {
|
||||
payload.insert("api_key".to_string(), json!(v));
|
||||
}
|
||||
if let Some(v) = args.api_url {
|
||||
payload.insert("api_url".to_string(), json!(v));
|
||||
}
|
||||
if let Some(v) = args.provider {
|
||||
payload.insert("default_provider".to_string(), json!(v));
|
||||
}
|
||||
if let Some(v) = args.model {
|
||||
payload.insert("default_model".to_string(), json!(v));
|
||||
}
|
||||
if let Some(v) = args.temperature {
|
||||
payload.insert("default_temperature".to_string(), json!(v));
|
||||
}
|
||||
ensure_non_empty_payload(&payload).map_err(|e| e.to_string())?;
|
||||
call_method("openhuman.update_model_settings", serde_json::Value::Object(payload))
|
||||
.await
|
||||
}
|
||||
},
|
||||
SettingsCommand::Memory { command } => match command {
|
||||
MemorySettingsCommand::Get => {
|
||||
let snapshot = get_config_snapshot().await?;
|
||||
serde_json::to_value(settings_view_response("memory", snapshot))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
MemorySettingsCommand::Set(args) => {
|
||||
let mut payload = serde_json::Map::new();
|
||||
if let Some(v) = args.backend {
|
||||
payload.insert("backend".to_string(), json!(v));
|
||||
}
|
||||
if let Some(v) = args.auto_save {
|
||||
payload.insert("auto_save".to_string(), json!(v));
|
||||
}
|
||||
if let Some(v) = args.embedding_provider {
|
||||
payload.insert("embedding_provider".to_string(), json!(v));
|
||||
}
|
||||
if let Some(v) = args.embedding_model {
|
||||
payload.insert("embedding_model".to_string(), json!(v));
|
||||
}
|
||||
if let Some(v) = args.embedding_dimensions {
|
||||
payload.insert("embedding_dimensions".to_string(), json!(v));
|
||||
}
|
||||
ensure_non_empty_payload(&payload).map_err(|e| e.to_string())?;
|
||||
call_method(
|
||||
"openhuman.update_memory_settings",
|
||||
serde_json::Value::Object(payload),
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
SettingsCommand::Gateway { command } => match command {
|
||||
GatewaySettingsCommand::Get => {
|
||||
let snapshot = get_config_snapshot().await?;
|
||||
serde_json::to_value(settings_view_response("gateway", snapshot))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
GatewaySettingsCommand::Set(args) => {
|
||||
let mut payload = serde_json::Map::new();
|
||||
if let Some(v) = args.host {
|
||||
payload.insert("host".to_string(), json!(v));
|
||||
}
|
||||
if let Some(v) = args.port {
|
||||
payload.insert("port".to_string(), json!(v));
|
||||
}
|
||||
if let Some(v) = args.require_pairing {
|
||||
payload.insert("require_pairing".to_string(), json!(v));
|
||||
}
|
||||
if let Some(v) = args.allow_public_bind {
|
||||
payload.insert("allow_public_bind".to_string(), json!(v));
|
||||
}
|
||||
ensure_non_empty_payload(&payload).map_err(|e| e.to_string())?;
|
||||
call_method(
|
||||
"openhuman.update_gateway_settings",
|
||||
serde_json::Value::Object(payload),
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
SettingsCommand::Tunnel { command } => match command {
|
||||
TunnelSettingsCommand::Get => {
|
||||
let snapshot = get_config_snapshot().await?;
|
||||
serde_json::to_value(settings_view_response("tunnel", snapshot))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
TunnelSettingsCommand::Set(args) => {
|
||||
call_method("openhuman.update_tunnel_settings", parse_json_arg(&args.json)?).await
|
||||
}
|
||||
},
|
||||
SettingsCommand::Runtime { command } => match command {
|
||||
RuntimeSettingsCommand::Get => {
|
||||
let snapshot = get_config_snapshot().await?;
|
||||
serde_json::to_value(settings_view_response("runtime", snapshot))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
RuntimeSettingsCommand::Set(args) => {
|
||||
let mut payload = serde_json::Map::new();
|
||||
if let Some(v) = args.kind {
|
||||
payload.insert("kind".to_string(), json!(v));
|
||||
}
|
||||
if let Some(v) = args.reasoning_enabled {
|
||||
payload.insert("reasoning_enabled".to_string(), json!(v));
|
||||
}
|
||||
ensure_non_empty_payload(&payload).map_err(|e| e.to_string())?;
|
||||
call_method(
|
||||
"openhuman.update_runtime_settings",
|
||||
serde_json::Value::Object(payload),
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
SettingsCommand::Browser { command } => match command {
|
||||
BrowserSettingsCommand::Get => {
|
||||
let snapshot = get_config_snapshot().await?;
|
||||
serde_json::to_value(settings_view_response("browser", snapshot))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
BrowserSettingsCommand::Set(args) => {
|
||||
let mut payload = serde_json::Map::new();
|
||||
if let Some(v) = args.enabled {
|
||||
payload.insert("enabled".to_string(), json!(v));
|
||||
}
|
||||
ensure_non_empty_payload(&payload).map_err(|e| e.to_string())?;
|
||||
call_method(
|
||||
"openhuman.update_browser_settings",
|
||||
serde_json::Value::Object(payload),
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
},
|
||||
CoreCommand::Accessibility { command } => match command {
|
||||
AccessibilityCommand::Status => {
|
||||
call_method("openhuman.accessibility_status", json!({})).await
|
||||
}
|
||||
AccessibilityCommand::Doctor => {
|
||||
let raw = call_method("openhuman.accessibility_status", json!({})).await?;
|
||||
let payload: CommandResponse<AccessibilityStatus> = serde_json::from_value(raw)
|
||||
.map_err(|e| format!("failed to decode accessibility status: {e}"))?;
|
||||
let permissions = &payload.result.permissions;
|
||||
|
||||
let screen_ready = permissions.screen_recording == PermissionState::Granted;
|
||||
let control_ready = permissions.accessibility == PermissionState::Granted;
|
||||
let monitoring_ready = permissions.input_monitoring == PermissionState::Granted;
|
||||
let overall_ready = payload.result.platform_supported && screen_ready && control_ready;
|
||||
|
||||
let mut recommendations: Vec<String> = Vec::new();
|
||||
if !payload.result.platform_supported {
|
||||
recommendations.push(
|
||||
"Accessibility automation is macOS-only in this build/runtime."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if permissions.screen_recording != PermissionState::Granted {
|
||||
recommendations.push(
|
||||
"Grant Screen Recording in System Settings -> Privacy & Security -> Screen Recording."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if permissions.accessibility != PermissionState::Granted {
|
||||
recommendations.push(
|
||||
"Grant Accessibility in System Settings -> Privacy & Security -> Accessibility."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if permissions.input_monitoring != PermissionState::Granted {
|
||||
recommendations.push(
|
||||
"Grant Input Monitoring in System Settings -> Privacy & Security -> Input Monitoring (optional but recommended)."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if recommendations.is_empty() {
|
||||
recommendations.push("No action required. Accessibility automation is ready.".to_string());
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"result": {
|
||||
"summary": {
|
||||
"overall_ready": overall_ready,
|
||||
"platform_supported": payload.result.platform_supported,
|
||||
"session_active": payload.result.session.active,
|
||||
"screen_capture_ready": screen_ready,
|
||||
"device_control_ready": control_ready,
|
||||
"input_monitoring_ready": monitoring_ready
|
||||
},
|
||||
"permissions": permissions,
|
||||
"features": payload.result.features,
|
||||
"recommendations": recommendations
|
||||
},
|
||||
"logs": payload.logs
|
||||
}))
|
||||
}
|
||||
AccessibilityCommand::RequestPermissions => {
|
||||
call_method("openhuman.accessibility_request_permissions", json!({})).await
|
||||
}
|
||||
AccessibilityCommand::RequestPermission(args) => {
|
||||
call_method(
|
||||
"openhuman.accessibility_request_permission",
|
||||
json!({ "permission": args.permission }),
|
||||
)
|
||||
.await
|
||||
}
|
||||
AccessibilityCommand::StartSession(args) => {
|
||||
call_method(
|
||||
"openhuman.accessibility_start_session",
|
||||
json!({
|
||||
"consent": args.consent,
|
||||
"ttl_secs": args.ttl_secs,
|
||||
"screen_monitoring": args.screen_monitoring,
|
||||
"device_control": args.device_control,
|
||||
"predictive_input": args.predictive_input,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
}
|
||||
AccessibilityCommand::StopSession(args) => {
|
||||
call_method(
|
||||
"openhuman.accessibility_stop_session",
|
||||
json!({ "reason": args.reason }),
|
||||
)
|
||||
.await
|
||||
}
|
||||
AccessibilityCommand::CaptureNow => {
|
||||
call_method("openhuman.accessibility_capture_now", json!({})).await
|
||||
}
|
||||
AccessibilityCommand::CaptureImageRef => {
|
||||
call_method("openhuman.accessibility_capture_image_ref", json!({})).await
|
||||
}
|
||||
AccessibilityCommand::VisionRecent(args) => {
|
||||
call_method(
|
||||
"openhuman.accessibility_vision_recent",
|
||||
json!({ "limit": args.limit }),
|
||||
)
|
||||
.await
|
||||
}
|
||||
AccessibilityCommand::VisionFlush => {
|
||||
call_method("openhuman.accessibility_vision_flush", json!({})).await
|
||||
}
|
||||
},
|
||||
CoreCommand::Tools { command } => match command {
|
||||
ToolsCommand::List => Ok(json!({
|
||||
"result": {
|
||||
"wrappers": [
|
||||
{
|
||||
"name": "screenshot",
|
||||
"description": "Capture a screenshot with screenshot tool wrapper."
|
||||
},
|
||||
{
|
||||
"name": "screenshot-ref",
|
||||
"description": "Capture data URL from accessibility capture_image_ref."
|
||||
}
|
||||
]
|
||||
},
|
||||
"logs": ["tools wrappers listed"]
|
||||
})),
|
||||
ToolsCommand::Screenshot(args) => execute_tools_screenshot(args).await,
|
||||
ToolsCommand::ScreenshotRef(args) => execute_tools_screenshot_ref(args).await,
|
||||
ToolsCommand::Run(args) => {
|
||||
let parsed = parse_json_arg(&args.args)?;
|
||||
match args.name.as_str() {
|
||||
"screenshot" => {
|
||||
let payload = parsed.as_object().cloned().unwrap_or_default();
|
||||
let wrapped = ToolsScreenshotArgs {
|
||||
filename: payload
|
||||
.get("filename")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::to_string),
|
||||
region: payload
|
||||
.get("region")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::to_string),
|
||||
output: payload
|
||||
.get("output")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(PathBuf::from),
|
||||
print_data_url: payload
|
||||
.get("print_data_url")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
};
|
||||
execute_tools_screenshot(wrapped).await
|
||||
}
|
||||
"screenshot-ref" | "screenshot_ref" => {
|
||||
let payload = parsed.as_object().cloned().unwrap_or_default();
|
||||
let wrapped = ToolsScreenshotRefArgs {
|
||||
output: payload
|
||||
.get("output")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(PathBuf::from),
|
||||
print_data_url: payload
|
||||
.get("print_data_url")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
};
|
||||
execute_tools_screenshot_ref(wrapped).await
|
||||
}
|
||||
other => Err(format!(
|
||||
"unsupported tool wrapper '{other}'. available: screenshot, screenshot-ref"
|
||||
)),
|
||||
}
|
||||
}
|
||||
},
|
||||
CoreCommand::Config { command } => match command {
|
||||
ConfigCommand::Get => call_method("openhuman.get_config", json!({})).await,
|
||||
ConfigCommand::UpdateModel { json } => {
|
||||
call_method("openhuman.update_model_settings", parse_json_arg(&json)?).await
|
||||
}
|
||||
ConfigCommand::UpdateMemory { json } => {
|
||||
call_method("openhuman.update_memory_settings", parse_json_arg(&json)?).await
|
||||
}
|
||||
ConfigCommand::UpdateGateway { json } => {
|
||||
call_method("openhuman.update_gateway_settings", parse_json_arg(&json)?).await
|
||||
}
|
||||
ConfigCommand::UpdateRuntime { json } => {
|
||||
call_method("openhuman.update_runtime_settings", parse_json_arg(&json)?).await
|
||||
}
|
||||
ConfigCommand::UpdateBrowser { json } => {
|
||||
call_method("openhuman.update_browser_settings", parse_json_arg(&json)?).await
|
||||
}
|
||||
ConfigCommand::UpdateTunnel { json } => {
|
||||
call_method("openhuman.update_tunnel_settings", parse_json_arg(&json)?).await
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_from_cli_args(args: &[String]) -> Result<()> {
|
||||
let mut argv = Vec::with_capacity(args.len() + 1);
|
||||
argv.push("openhuman-core".to_string());
|
||||
argv.extend(args.iter().cloned());
|
||||
let cli =
|
||||
CoreCli::try_parse_from(argv).map_err(|e| anyhow::anyhow!(e.render().to_string()))?;
|
||||
|
||||
let thread_stack_size = std::env::var("OPENHUMAN_CORE_THREAD_STACK_SIZE")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<usize>().ok())
|
||||
.unwrap_or(8 * 1024 * 1024);
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.thread_stack_size(thread_stack_size)
|
||||
.enable_all()
|
||||
.build()?;
|
||||
runtime.block_on(run_server(port))
|
||||
let output = runtime.block_on(execute_core_cli(cli)).map_err(anyhow::Error::msg)?;
|
||||
if !output.is_null() {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&output).unwrap_or_else(|_| "null".to_string())
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -122,6 +122,15 @@ pub struct CaptureNowResult {
|
||||
pub frame: Option<CaptureFrame>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CaptureImageRefResult {
|
||||
pub ok: bool,
|
||||
pub image_ref: Option<String>,
|
||||
pub mime_type: String,
|
||||
pub bytes_estimate: Option<usize>,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VisionSummary {
|
||||
pub id: String,
|
||||
@@ -535,6 +544,30 @@ impl AccessibilityEngine {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn capture_image_ref_test(&self) -> CaptureImageRefResult {
|
||||
match capture_screen_image_ref() {
|
||||
Ok(image_ref) => {
|
||||
let bytes_estimate = image_ref
|
||||
.strip_prefix("data:image/png;base64,")
|
||||
.map(|payload| payload.len() * 3 / 4);
|
||||
CaptureImageRefResult {
|
||||
ok: true,
|
||||
image_ref: Some(image_ref),
|
||||
mime_type: "image/png".to_string(),
|
||||
bytes_estimate,
|
||||
message: "screen capture completed".to_string(),
|
||||
}
|
||||
}
|
||||
Err(err) => CaptureImageRefResult {
|
||||
ok: false,
|
||||
image_ref: None,
|
||||
mime_type: "image/png".to_string(),
|
||||
bytes_estimate: None,
|
||||
message: err,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn input_action(
|
||||
&self,
|
||||
action: InputActionParams,
|
||||
|
||||
@@ -60,15 +60,15 @@ fn default_provider() -> String {
|
||||
}
|
||||
|
||||
fn default_model_id() -> String {
|
||||
"qwen2.5:1.5b".to_string()
|
||||
"gemma3:4b-it-qat".to_string()
|
||||
}
|
||||
|
||||
fn default_chat_model_id() -> String {
|
||||
"qwen2.5:1.5b".to_string()
|
||||
"gemma3:4b-it-qat".to_string()
|
||||
}
|
||||
|
||||
fn default_vision_model_id() -> String {
|
||||
"qwen3-vl:2b".to_string()
|
||||
"gemma3:4b-it-qat".to_string()
|
||||
}
|
||||
|
||||
fn default_embedding_model_id() -> String {
|
||||
|
||||
@@ -7,19 +7,79 @@ use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
const SERVICE_LABEL: &str = "com.openhuman.daemon";
|
||||
const LEGACY_SERVICE_LABEL: &str = "com.openhuman.app";
|
||||
const WINDOWS_TASK_NAME: &str = "OpenHuman Daemon";
|
||||
const SERVICE_LABEL: &str = "com.openhuman.core";
|
||||
const LEGACY_SERVICE_LABEL: &str = "com.openhuman.daemon";
|
||||
const LEGACY_APP_LABEL: &str = "com.openhuman.app";
|
||||
const WINDOWS_TASK_NAME: &str = "OpenHuman Core";
|
||||
|
||||
fn windows_task_name() -> &'static str {
|
||||
WINDOWS_TASK_NAME
|
||||
}
|
||||
|
||||
fn resolve_daemon_executable() -> Result<PathBuf> {
|
||||
if let Ok(path) = std::env::var("OPENHUMAN_CORE_BIN") {
|
||||
let candidate = PathBuf::from(path);
|
||||
if candidate.exists() {
|
||||
return Ok(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
let exe = std::env::current_exe().context("Failed to resolve current executable")?;
|
||||
let exe_dir = exe
|
||||
.parent()
|
||||
.map(PathBuf::from)
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to resolve executable directory"))?;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut search_dirs = vec![
|
||||
exe_dir.clone(),
|
||||
exe_dir
|
||||
.parent()
|
||||
.map(|p| p.join("Resources"))
|
||||
.unwrap_or_else(|| exe_dir.clone()),
|
||||
];
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let search_dirs = vec![exe_dir.clone()];
|
||||
|
||||
for dir in search_dirs.drain(..) {
|
||||
let Ok(entries) = fs::read_dir(&dir) else {
|
||||
continue;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_file() || is_current_executable(&path) {
|
||||
continue;
|
||||
}
|
||||
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
let matches = name.starts_with("openhuman-")
|
||||
|| name.starts_with("openhuman-core-")
|
||||
|| name.eq_ignore_ascii_case("openhuman.exe")
|
||||
|| name.eq_ignore_ascii_case("openhuman-core.exe");
|
||||
#[cfg(not(windows))]
|
||||
let matches = name.starts_with("openhuman-")
|
||||
|| name.starts_with("openhuman-core-")
|
||||
|| name == "openhuman"
|
||||
|| name == "openhuman-core";
|
||||
|
||||
if matches {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(exe)
|
||||
}
|
||||
|
||||
fn daemon_program_args(exe: &std::path::Path) -> Vec<String> {
|
||||
let raw_file_name = exe.file_name().and_then(|n| n.to_str()).unwrap_or_default();
|
||||
let file_name = raw_file_name.to_ascii_lowercase();
|
||||
let standalone_core_binary = !is_current_executable(exe)
|
||||
&& (file_name.contains("openhuman-core")
|
||||
|| file_name.starts_with("openhuman-")
|
||||
|| file_name.starts_with("openhuman-core-")
|
||||
|| raw_file_name == "openhuman"
|
||||
|| raw_file_name == "openhuman.exe");
|
||||
@@ -95,9 +155,25 @@ pub fn start(config: &Config) -> Result<ServiceStatus> {
|
||||
let domain = macos_gui_domain()?;
|
||||
let primary_target = macos_target(SERVICE_LABEL)?;
|
||||
|
||||
if !plist.exists() {
|
||||
log::info!(
|
||||
"[service] LaunchAgent plist missing, installing it before start: {}",
|
||||
plist.display()
|
||||
);
|
||||
install_macos(config)?;
|
||||
}
|
||||
|
||||
validate_macos_plist(&plist)?;
|
||||
|
||||
// Prefer modern launchctl lifecycle commands on macOS.
|
||||
if !is_service_loaded_macos()? {
|
||||
log::info!("[service] Loading macOS LaunchAgent service");
|
||||
run_best_effort(
|
||||
Command::new("launchctl")
|
||||
.arg("bootout")
|
||||
.arg(&domain)
|
||||
.arg(&primary_target),
|
||||
);
|
||||
let bootstrap_ok = run_checked(
|
||||
Command::new("launchctl")
|
||||
.arg("bootstrap")
|
||||
@@ -205,6 +281,8 @@ pub fn stop(config: &Config) -> Result<ServiceStatus> {
|
||||
|
||||
let legacy_plist = macos_service_file_for(LEGACY_SERVICE_LABEL)?;
|
||||
let legacy_target = macos_target(LEGACY_SERVICE_LABEL)?;
|
||||
let legacy_app_plist = macos_service_file_for(LEGACY_APP_LABEL)?;
|
||||
let legacy_app_target = macos_target(LEGACY_APP_LABEL)?;
|
||||
|
||||
// Modern lifecycle path first.
|
||||
run_best_effort(
|
||||
@@ -231,6 +309,18 @@ pub fn stop(config: &Config) -> Result<ServiceStatus> {
|
||||
.arg(&domain)
|
||||
.arg(&legacy_plist),
|
||||
);
|
||||
run_best_effort(
|
||||
Command::new("launchctl")
|
||||
.arg("bootout")
|
||||
.arg(&domain)
|
||||
.arg(&legacy_app_target),
|
||||
);
|
||||
run_best_effort(
|
||||
Command::new("launchctl")
|
||||
.arg("bootout")
|
||||
.arg(&domain)
|
||||
.arg(&legacy_app_plist),
|
||||
);
|
||||
|
||||
// Compatibility fallback.
|
||||
run_best_effort(Command::new("launchctl").arg("stop").arg(SERVICE_LABEL));
|
||||
@@ -239,6 +329,7 @@ pub fn stop(config: &Config) -> Result<ServiceStatus> {
|
||||
.arg("stop")
|
||||
.arg(LEGACY_SERVICE_LABEL),
|
||||
);
|
||||
run_best_effort(Command::new("launchctl").arg("stop").arg(LEGACY_APP_LABEL));
|
||||
return status(config);
|
||||
}
|
||||
|
||||
@@ -261,9 +352,11 @@ pub fn stop(config: &Config) -> Result<ServiceStatus> {
|
||||
pub fn status(config: &Config) -> Result<ServiceStatus> {
|
||||
if std::env::consts::OS == "macos" {
|
||||
let out = run_capture(Command::new("launchctl").arg("list"))?;
|
||||
let running = out
|
||||
.lines()
|
||||
.any(|line| line.contains(SERVICE_LABEL) || line.contains(LEGACY_SERVICE_LABEL));
|
||||
let running = out.lines().any(|line| {
|
||||
line.contains(SERVICE_LABEL)
|
||||
|| line.contains(LEGACY_SERVICE_LABEL)
|
||||
|| line.contains(LEGACY_APP_LABEL)
|
||||
});
|
||||
return Ok(ServiceStatus {
|
||||
state: if running {
|
||||
ServiceState::Running
|
||||
@@ -342,6 +435,10 @@ pub fn uninstall(config: &Config) -> Result<ServiceStatus> {
|
||||
if legacy_file.exists() {
|
||||
let _ = fs::remove_file(&legacy_file);
|
||||
}
|
||||
let legacy_app_file = macos_service_file_for(LEGACY_APP_LABEL)?;
|
||||
if legacy_app_file.exists() {
|
||||
let _ = fs::remove_file(&legacy_app_file);
|
||||
}
|
||||
return Ok(ServiceStatus {
|
||||
state: ServiceState::NotInstalled,
|
||||
unit_path: Some(file),
|
||||
@@ -395,7 +492,7 @@ fn install_macos(config: &Config) -> Result<()> {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let exe = std::env::current_exe().context("Failed to resolve current executable")?;
|
||||
let exe = resolve_daemon_executable()?;
|
||||
let logs_dir = config
|
||||
.config_path
|
||||
.parent()
|
||||
@@ -412,9 +509,9 @@ fn install_macos(config: &Config) -> Result<()> {
|
||||
.collect::<String>();
|
||||
|
||||
let plist = format!(
|
||||
r#"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
||||
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
|
||||
<plist version=\"1.0\">
|
||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>{label}</string>
|
||||
@@ -465,7 +562,7 @@ fn install_linux(config: &Config) -> Result<()> {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let exe = std::env::current_exe().context("Failed to resolve current executable")?;
|
||||
let exe = resolve_daemon_executable()?;
|
||||
let logs_dir = config
|
||||
.config_path
|
||||
.parent()
|
||||
@@ -490,7 +587,7 @@ fn install_linux(config: &Config) -> Result<()> {
|
||||
}
|
||||
|
||||
fn install_windows(config: &Config) -> Result<()> {
|
||||
let exe = std::env::current_exe().context("Failed to resolve current executable")?;
|
||||
let exe = resolve_daemon_executable()?;
|
||||
let logs_dir = config
|
||||
.config_path
|
||||
.parent()
|
||||
@@ -555,6 +652,11 @@ fn macos_target(label: &str) -> Result<String> {
|
||||
Ok(format!("{}/{}", macos_gui_domain()?, label))
|
||||
}
|
||||
|
||||
fn validate_macos_plist(path: &std::path::Path) -> Result<()> {
|
||||
run_checked(Command::new("plutil").arg("-lint").arg(path))
|
||||
.with_context(|| format!("Invalid launch agent plist: {}", path.display()))
|
||||
}
|
||||
|
||||
fn linux_service_file(config: &Config) -> Result<PathBuf> {
|
||||
let config_dir = config
|
||||
.config_path
|
||||
@@ -598,24 +700,35 @@ fn run_best_effort(cmd: &mut Command) {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_check_silent(cmd: &mut Command) -> bool {
|
||||
cmd.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.map(|status| status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Check if the macOS LaunchAgent service is loaded (regardless of running state)
|
||||
fn is_service_loaded_macos() -> Result<bool> {
|
||||
if run_checked(
|
||||
if run_check_silent(
|
||||
Command::new("launchctl")
|
||||
.arg("print")
|
||||
.arg(macos_target(SERVICE_LABEL)?),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
) {
|
||||
return Ok(true);
|
||||
}
|
||||
if run_checked(
|
||||
if run_check_silent(
|
||||
Command::new("launchctl")
|
||||
.arg("print")
|
||||
.arg(macos_target(LEGACY_SERVICE_LABEL)?),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
) {
|
||||
return Ok(true);
|
||||
}
|
||||
if run_check_silent(
|
||||
Command::new("launchctl")
|
||||
.arg("print")
|
||||
.arg(macos_target(LEGACY_APP_LABEL)?),
|
||||
) {
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
|
||||
@@ -76,6 +76,32 @@ async fn call_core<T: DeserializeOwned>(
|
||||
crate::core_rpc::call(method, params).await
|
||||
}
|
||||
|
||||
async fn ensure_service_managed_core_running() -> Result<(), String> {
|
||||
let config = load_config_local().await?;
|
||||
let _ = service::install(&config);
|
||||
let _ = service::start(&config);
|
||||
|
||||
for _ in 0..40 {
|
||||
if crate::core_rpc::ping().await {
|
||||
return Ok(());
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
||||
}
|
||||
|
||||
Err(
|
||||
"OpenHuman Core daemon did not become ready. Confirm the background service is running."
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn call_core_service_managed<T: DeserializeOwned>(
|
||||
method: &str,
|
||||
params: serde_json::Value,
|
||||
) -> Result<T, String> {
|
||||
ensure_service_managed_core_running().await?;
|
||||
crate::core_rpc::call(method, params).await
|
||||
}
|
||||
|
||||
async fn load_config_local() -> Result<openhuman_core::openhuman::config::Config, String> {
|
||||
let timeout_duration = std::time::Duration::from_secs(30);
|
||||
match tokio::time::timeout(
|
||||
@@ -96,6 +122,12 @@ pub struct AgentServerStatus {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DaemonHostConfigPayload {
|
||||
pub show_tray: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LocalAiStatus {
|
||||
pub state: String,
|
||||
@@ -438,7 +470,8 @@ pub async fn openhuman_agent_chat(
|
||||
pub async fn openhuman_accessibility_status(
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<CommandResponse<AccessibilityStatus>, String> {
|
||||
call_core(&app, "openhuman.accessibility_status", params_none()).await
|
||||
let _ = app;
|
||||
call_core_service_managed("openhuman.accessibility_status", params_none()).await
|
||||
}
|
||||
|
||||
/// Request accessibility-related permissions on macOS.
|
||||
@@ -446,12 +479,9 @@ pub async fn openhuman_accessibility_status(
|
||||
pub async fn openhuman_accessibility_request_permissions(
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<CommandResponse<PermissionStatus>, String> {
|
||||
let response: CommandResponse<PermissionStatus> = call_core(
|
||||
&app,
|
||||
"openhuman.accessibility_request_permissions",
|
||||
params_none(),
|
||||
)
|
||||
.await?;
|
||||
let response: CommandResponse<PermissionStatus> =
|
||||
call_core_service_managed("openhuman.accessibility_request_permissions", params_none())
|
||||
.await?;
|
||||
emit_accessibility_event(
|
||||
&app,
|
||||
"permissions_requested",
|
||||
@@ -466,8 +496,7 @@ pub async fn openhuman_accessibility_request_permission(
|
||||
app: tauri::AppHandle,
|
||||
params: PermissionRequestParams,
|
||||
) -> Result<CommandResponse<PermissionStatus>, String> {
|
||||
let response: CommandResponse<PermissionStatus> = call_core(
|
||||
&app,
|
||||
let response: CommandResponse<PermissionStatus> = call_core_service_managed(
|
||||
"openhuman.accessibility_request_permission",
|
||||
serde_json::json!(params),
|
||||
)
|
||||
@@ -486,8 +515,7 @@ pub async fn openhuman_accessibility_start_session(
|
||||
app: tauri::AppHandle,
|
||||
params: StartSessionParams,
|
||||
) -> Result<CommandResponse<SessionStatus>, String> {
|
||||
let response: CommandResponse<SessionStatus> = call_core(
|
||||
&app,
|
||||
let response: CommandResponse<SessionStatus> = call_core_service_managed(
|
||||
"openhuman.accessibility_start_session",
|
||||
serde_json::json!(params),
|
||||
)
|
||||
@@ -502,8 +530,7 @@ pub async fn openhuman_accessibility_stop_session(
|
||||
app: tauri::AppHandle,
|
||||
params: Option<StopSessionParams>,
|
||||
) -> Result<CommandResponse<SessionStatus>, String> {
|
||||
let response: CommandResponse<SessionStatus> = call_core(
|
||||
&app,
|
||||
let response: CommandResponse<SessionStatus> = call_core_service_managed(
|
||||
"openhuman.accessibility_stop_session",
|
||||
serde_json::json!(params.unwrap_or(StopSessionParams { reason: None })),
|
||||
)
|
||||
@@ -517,7 +544,8 @@ pub async fn openhuman_accessibility_stop_session(
|
||||
pub async fn openhuman_accessibility_capture_now(
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<CommandResponse<CaptureNowResult>, String> {
|
||||
call_core(&app, "openhuman.accessibility_capture_now", params_none()).await
|
||||
let _ = app;
|
||||
call_core_service_managed("openhuman.accessibility_capture_now", params_none()).await
|
||||
}
|
||||
|
||||
/// Execute a validated input action in an active accessibility session.
|
||||
@@ -526,8 +554,7 @@ pub async fn openhuman_accessibility_input_action(
|
||||
app: tauri::AppHandle,
|
||||
params: InputActionParams,
|
||||
) -> Result<CommandResponse<InputActionResult>, String> {
|
||||
let response: CommandResponse<InputActionResult> = call_core(
|
||||
&app,
|
||||
let response: CommandResponse<InputActionResult> = call_core_service_managed(
|
||||
"openhuman.accessibility_input_action",
|
||||
serde_json::json!(params),
|
||||
)
|
||||
@@ -544,8 +571,8 @@ pub async fn openhuman_accessibility_autocomplete_suggest(
|
||||
app: tauri::AppHandle,
|
||||
params: Option<AutocompleteSuggestParams>,
|
||||
) -> Result<CommandResponse<AutocompleteSuggestResult>, String> {
|
||||
call_core(
|
||||
&app,
|
||||
let _ = app;
|
||||
call_core_service_managed(
|
||||
"openhuman.accessibility_autocomplete_suggest",
|
||||
serde_json::json!(params.unwrap_or(AutocompleteSuggestParams {
|
||||
context: None,
|
||||
@@ -561,8 +588,8 @@ pub async fn openhuman_accessibility_autocomplete_commit(
|
||||
app: tauri::AppHandle,
|
||||
params: AutocompleteCommitParams,
|
||||
) -> Result<CommandResponse<AutocompleteCommitResult>, String> {
|
||||
call_core(
|
||||
&app,
|
||||
let _ = app;
|
||||
call_core_service_managed(
|
||||
"openhuman.accessibility_autocomplete_commit",
|
||||
serde_json::json!(params),
|
||||
)
|
||||
@@ -574,8 +601,8 @@ pub async fn openhuman_accessibility_vision_recent(
|
||||
app: tauri::AppHandle,
|
||||
limit: Option<usize>,
|
||||
) -> Result<CommandResponse<VisionRecentResult>, String> {
|
||||
call_core(
|
||||
&app,
|
||||
let _ = app;
|
||||
call_core_service_managed(
|
||||
"openhuman.accessibility_vision_recent",
|
||||
serde_json::json!({ "limit": limit }),
|
||||
)
|
||||
@@ -586,7 +613,8 @@ pub async fn openhuman_accessibility_vision_recent(
|
||||
pub async fn openhuman_accessibility_vision_flush(
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<CommandResponse<VisionFlushResult>, String> {
|
||||
call_core(&app, "openhuman.accessibility_vision_flush", params_none()).await
|
||||
let _ = app;
|
||||
call_core_service_managed("openhuman.accessibility_vision_flush", params_none()).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -922,6 +950,42 @@ pub async fn openhuman_agent_server_status() -> Result<CommandResponse<AgentServ
|
||||
})
|
||||
}
|
||||
|
||||
/// Read local daemon-host settings (not synced, machine-local).
|
||||
#[tauri::command]
|
||||
pub async fn openhuman_get_daemon_host_config(
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<CommandResponse<DaemonHostConfigPayload>, String> {
|
||||
let cfg = crate::daemon_host_config::load(&app).await;
|
||||
Ok(CommandResponse {
|
||||
result: DaemonHostConfigPayload {
|
||||
show_tray: cfg.show_tray,
|
||||
},
|
||||
logs: vec!["daemon host config loaded".to_string()],
|
||||
})
|
||||
}
|
||||
|
||||
/// Update local daemon-host settings (not synced, machine-local).
|
||||
#[tauri::command]
|
||||
pub async fn openhuman_set_daemon_host_config(
|
||||
app: tauri::AppHandle,
|
||||
show_tray: Option<bool>,
|
||||
) -> Result<CommandResponse<DaemonHostConfigPayload>, String> {
|
||||
let mut cfg = crate::daemon_host_config::load(&app).await;
|
||||
if let Some(value) = show_tray {
|
||||
cfg.show_tray = value;
|
||||
}
|
||||
crate::daemon_host_config::save(&app, &cfg).await?;
|
||||
Ok(CommandResponse {
|
||||
result: DaemonHostConfigPayload {
|
||||
show_tray: cfg.show_tray,
|
||||
},
|
||||
logs: vec![
|
||||
"daemon host config saved".to_string(),
|
||||
"restart daemon host process to apply tray visibility changes".to_string(),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
/// Install the OpenHuman daemon service.
|
||||
#[tauri::command]
|
||||
pub async fn openhuman_service_install(
|
||||
|
||||
@@ -71,9 +71,9 @@ impl CoreProcessHandle {
|
||||
// explicit subcommand path so we don't accidentally relaunch clients.
|
||||
cmd.arg("core");
|
||||
}
|
||||
cmd.arg("serve").arg("--port").arg(self.port.to_string());
|
||||
cmd.arg("run").arg("--port").arg(self.port.to_string());
|
||||
log::info!(
|
||||
"[core] spawning dedicated core binary: {:?} serve --port {}",
|
||||
"[core] spawning dedicated core binary: {:?} run --port {}",
|
||||
cmd.as_std().get_program(),
|
||||
self.port
|
||||
);
|
||||
@@ -83,7 +83,7 @@ impl CoreProcessHandle {
|
||||
.map_err(|e| format!("failed to resolve current executable: {e}"))?;
|
||||
let mut cmd = Command::new(exe);
|
||||
cmd.arg("core")
|
||||
.arg("serve")
|
||||
.arg("run")
|
||||
.arg("--port")
|
||||
.arg(self.port.to_string());
|
||||
log::warn!(
|
||||
@@ -218,7 +218,7 @@ pub fn default_core_bin() -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
// Dev ergonomics: in debug builds, prefer spawning this same executable with
|
||||
// `core serve` so Cargo recompiles core logic changes as part of tauri dev.
|
||||
// `core run` so Cargo recompiles core logic changes as part of tauri dev.
|
||||
// Sidecar discovery remains enabled for packaged/release builds.
|
||||
if cfg!(debug_assertions) {
|
||||
return None;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
const DAEMON_HOST_CONFIG_FILE: &str = "daemon_host_config.json";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DaemonHostConfig {
|
||||
pub show_tray: bool,
|
||||
}
|
||||
|
||||
impl Default for DaemonHostConfig {
|
||||
fn default() -> Self {
|
||||
Self { show_tray: true }
|
||||
}
|
||||
}
|
||||
|
||||
fn config_path(app: &AppHandle) -> PathBuf {
|
||||
app.path()
|
||||
.app_data_dir()
|
||||
.unwrap_or_else(|_| {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".openhuman")
|
||||
})
|
||||
.join(DAEMON_HOST_CONFIG_FILE)
|
||||
}
|
||||
|
||||
pub async fn load(app: &AppHandle) -> DaemonHostConfig {
|
||||
let path = config_path(app);
|
||||
let Ok(contents) = tokio::fs::read_to_string(path).await else {
|
||||
return DaemonHostConfig::default();
|
||||
};
|
||||
serde_json::from_str::<DaemonHostConfig>(&contents).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub async fn save(app: &AppHandle, config: &DaemonHostConfig) -> Result<(), String> {
|
||||
let path = config_path(app);
|
||||
if let Some(parent) = path.parent() {
|
||||
tokio::fs::create_dir_all(parent)
|
||||
.await
|
||||
.map_err(|e| format!("failed to create daemon host config directory: {e}"))?;
|
||||
}
|
||||
let bytes = serde_json::to_vec_pretty(config)
|
||||
.map_err(|e| format!("failed to serialize daemon host config: {e}"))?;
|
||||
tokio::fs::write(path, bytes)
|
||||
.await
|
||||
.map_err(|e| format!("failed to write daemon host config: {e}"))
|
||||
}
|
||||
@@ -14,6 +14,7 @@ compile_error!("src-tauri host is desktop-only. Non-desktop targets are not supp
|
||||
mod commands;
|
||||
mod core_process;
|
||||
mod core_rpc;
|
||||
mod daemon_host_config;
|
||||
pub mod memory;
|
||||
mod models;
|
||||
mod openhuman_daemon;
|
||||
@@ -727,7 +728,15 @@ pub fn run() {
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
if daemon_mode {
|
||||
setup_tray(app.handle())?;
|
||||
let daemon_host_cfg =
|
||||
tauri::async_runtime::block_on(daemon_host_config::load(app.handle()));
|
||||
if daemon_host_cfg.show_tray {
|
||||
setup_tray(app.handle())?;
|
||||
} else {
|
||||
log::info!(
|
||||
"[app] Daemon host tray disabled by local config (show_tray=false)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1117,6 +1126,8 @@ pub fn run() {
|
||||
openhuman_service_status,
|
||||
openhuman_service_uninstall,
|
||||
openhuman_agent_server_status,
|
||||
openhuman_get_daemon_host_config,
|
||||
openhuman_set_daemon_host_config,
|
||||
// Unified skill registry commands
|
||||
unified_list_skills,
|
||||
unified_execute_skill,
|
||||
@@ -1168,13 +1179,6 @@ pub fn run() {
|
||||
log::info!("[openhuman] Daemon shutdown signalled");
|
||||
}
|
||||
|
||||
if let Some(core) = app_handle.try_state::<core_process::CoreProcessHandle>() {
|
||||
let core_handle: core_process::CoreProcessHandle = (*core).clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
core_handle.shutdown().await;
|
||||
});
|
||||
}
|
||||
|
||||
let _ = app_handle;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,16 @@ import {
|
||||
XCircleIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { formatRelativeTime, useDaemonHealth } from '../../hooks/useDaemonHealth';
|
||||
import type { ComponentHealth, DaemonStatus } from '../../store/daemonSlice';
|
||||
import { IS_DEV } from '../../utils/config';
|
||||
import {
|
||||
isTauri,
|
||||
openhumanGetDaemonHostConfig,
|
||||
openhumanSetDaemonHostConfig,
|
||||
} from '../../utils/tauriCommands';
|
||||
|
||||
interface Props {
|
||||
userId?: string;
|
||||
@@ -28,6 +33,8 @@ interface Props {
|
||||
const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
|
||||
const daemonHealth = useDaemonHealth(userId);
|
||||
const [operationLoading, setOperationLoading] = useState<string | null>(null);
|
||||
const [showTray, setShowTray] = useState<boolean>(true);
|
||||
const [trayLoaded, setTrayLoaded] = useState<boolean>(false);
|
||||
|
||||
// Handle agent operations with loading states
|
||||
const handleOperation = async (operation: () => Promise<unknown>, operationName: string) => {
|
||||
@@ -83,6 +90,39 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
|
||||
const statusStyling = getStatusStyling(daemonHealth.status);
|
||||
const StatusIcon = statusStyling.icon;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauri()) {
|
||||
return;
|
||||
}
|
||||
const loadTray = async () => {
|
||||
try {
|
||||
const result = await openhumanGetDaemonHostConfig();
|
||||
setShowTray(result.result.show_tray);
|
||||
setTrayLoaded(true);
|
||||
} catch (error) {
|
||||
console.error('[AgentHealthPanel] Failed to load daemon host config:', error);
|
||||
}
|
||||
};
|
||||
void loadTray();
|
||||
}, []);
|
||||
|
||||
const updateTraySetting = async (enabled: boolean) => {
|
||||
if (!isTauri()) {
|
||||
return;
|
||||
}
|
||||
const previous = showTray;
|
||||
setShowTray(enabled);
|
||||
setOperationLoading('tray');
|
||||
try {
|
||||
await openhumanSetDaemonHostConfig(enabled);
|
||||
} catch (error) {
|
||||
setShowTray(previous);
|
||||
console.error('[AgentHealthPanel] Failed to save daemon host config:', error);
|
||||
} finally {
|
||||
setOperationLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`bg-stone-900 rounded-lg border border-stone-700 p-6 space-y-6 ${className}`}>
|
||||
{/* Header */}
|
||||
@@ -186,6 +226,26 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-stone-800/40 border border-stone-700/60">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-300">Show Daemon Tray</div>
|
||||
<div className="text-xs text-gray-500">Display tray icon in daemon host mode</div>
|
||||
<div className="text-xs text-amber-400 mt-1">Requires daemon host restart to apply</div>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
checked={showTray}
|
||||
disabled={!trayLoaded || operationLoading !== null}
|
||||
onChange={e => {
|
||||
void updateTraySetting(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-disabled:opacity-60 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Control Actions */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
openhumanDoctorReport,
|
||||
openhumanEncryptSecret,
|
||||
openhumanGetConfig,
|
||||
openhumanGetDaemonHostConfig,
|
||||
openhumanGetIntegrationInfo,
|
||||
openhumanHardwareDiscover,
|
||||
openhumanHardwareIntrospect,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
openhumanServiceInstall,
|
||||
openhumanServiceStatus,
|
||||
openhumanServiceUninstall,
|
||||
openhumanSetDaemonHostConfig,
|
||||
openhumanUpdateGatewaySettings,
|
||||
openhumanUpdateMemorySettings,
|
||||
openhumanUpdateModelSettings,
|
||||
@@ -104,6 +106,8 @@ const TauriCommandsPanel = () => {
|
||||
const [chatModel, setChatModel] = useState<string>('');
|
||||
const [chatTemperature, setChatTemperature] = useState<string>('0.7');
|
||||
const [chatLog, setChatLog] = useState<Array<{ role: 'user' | 'agent'; text: string }>>([]);
|
||||
const [daemonShowTray, setDaemonShowTray] = useState<boolean>(true);
|
||||
const [daemonShowTrayLoaded, setDaemonShowTrayLoaded] = useState<boolean>(false);
|
||||
|
||||
// Loading states
|
||||
const [operationLoading, setOperationLoading] = useState<string>('');
|
||||
@@ -413,6 +417,45 @@ const TauriCommandsPanel = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadDaemonHostConfig = useCallback(async () => {
|
||||
if (!tauriAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await openhumanGetDaemonHostConfig();
|
||||
setDaemonShowTray(result.result.show_tray);
|
||||
setDaemonShowTrayLoaded(true);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message);
|
||||
}
|
||||
}, [tauriAvailable]);
|
||||
|
||||
const saveDaemonHostTraySetting = useCallback(
|
||||
async (showTray: boolean) => {
|
||||
if (!tauriAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = daemonShowTray;
|
||||
setDaemonShowTray(showTray);
|
||||
setOperationLoading('saveDaemonHostConfig');
|
||||
setError('');
|
||||
try {
|
||||
const result = await openhumanSetDaemonHostConfig(showTray);
|
||||
setOutput(formatJson(result));
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setDaemonShowTray(previous);
|
||||
setError(message);
|
||||
} finally {
|
||||
setOperationLoading('');
|
||||
}
|
||||
},
|
||||
[daemonShowTray, tauriAvailable]
|
||||
);
|
||||
|
||||
const buildTunnelConfig = (): TunnelConfig => {
|
||||
if (tunnelProvider === 'cloudflare') {
|
||||
return { provider: 'cloudflare', cloudflare: { token: cloudflareToken } };
|
||||
@@ -654,6 +697,10 @@ const TauriCommandsPanel = () => {
|
||||
return true; // Always show all sections
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadDaemonHostConfig();
|
||||
}, [loadDaemonHostConfig]);
|
||||
|
||||
const isCollapsed = (sectionId: string) => {
|
||||
return !expandedSections.has(sectionId);
|
||||
};
|
||||
@@ -1090,6 +1137,33 @@ const TauriCommandsPanel = () => {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Tray Toggle */}
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-stone-800/40 border border-stone-700/60">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-300">Show Daemon Tray</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Keep OpenHuman Core tray icon visible in daemon host mode
|
||||
</div>
|
||||
<div className="text-xs text-amber-400 mt-1">
|
||||
Requires daemon host restart to fully apply
|
||||
</div>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
checked={daemonShowTray}
|
||||
disabled={
|
||||
!daemonShowTrayLoaded || operationLoading === 'saveDaemonHostConfig'
|
||||
}
|
||||
onChange={event => {
|
||||
void saveDaemonHostTraySetting(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 disabled:opacity-60 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Connection Info */}
|
||||
{daemonHealth.connectionAttempts > 0 && (
|
||||
<div className="p-3 rounded-lg bg-yellow-900/20 border border-yellow-500/30">
|
||||
|
||||
@@ -142,7 +142,9 @@ const Home = () => {
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center justify-between text-xs">
|
||||
<span className="text-stone-300">{localAiStatus?.model_id ?? 'qwen3-1.7b'}</span>
|
||||
<span className="text-stone-300">
|
||||
{localAiStatus?.model_id ?? 'gemma3:4b-it-qat'}
|
||||
</span>
|
||||
<span className="text-stone-200 capitalize">
|
||||
{localAiStatus?.state ?? 'starting'}
|
||||
</span>
|
||||
|
||||
@@ -427,6 +427,10 @@ export interface AgentServerStatus {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface DaemonHostConfig {
|
||||
show_tray: boolean;
|
||||
}
|
||||
|
||||
export type AccessibilityPermissionState = 'granted' | 'denied' | 'unknown' | 'unsupported';
|
||||
export type AccessibilityPermissionKind = 'screen_recording' | 'accessibility' | 'input_monitoring';
|
||||
|
||||
@@ -1187,6 +1191,22 @@ export async function openhumanAgentServerStatus(): Promise<CommandResponse<Agen
|
||||
return await invoke('openhuman_agent_server_status');
|
||||
}
|
||||
|
||||
export async function openhumanGetDaemonHostConfig(): Promise<CommandResponse<DaemonHostConfig>> {
|
||||
if (!isTauri()) {
|
||||
throw new Error('Not running in Tauri');
|
||||
}
|
||||
return await invoke('openhuman_get_daemon_host_config');
|
||||
}
|
||||
|
||||
export async function openhumanSetDaemonHostConfig(
|
||||
showTray?: boolean
|
||||
): Promise<CommandResponse<DaemonHostConfig>> {
|
||||
if (!isTauri()) {
|
||||
throw new Error('Not running in Tauri');
|
||||
}
|
||||
return await invoke('openhuman_set_daemon_host_config', { showTray });
|
||||
}
|
||||
|
||||
export async function openhumanAccessibilityStatus(): Promise<
|
||||
CommandResponse<AccessibilityStatus>
|
||||
> {
|
||||
|
||||