mo commited on
Commit
fce80d1
·
1 Parent(s): 44de75e

feat: enterprise capability expansion — app control, browser control, background execution

Browse files

New capabilities, so every real-world request works end to end:

- tools/apps: launch and quit desktop applications, multi-OS
(macOS open/osascript, Linux gtk-launch/pkill, Windows start/taskkill)
- tools/browser: open URLs and run searches (YouTube, Google, web) in the
default browser, multi-OS; watch mode attempts autoplay via Safari
automation with a graceful fallback
- tools/shell: background mode — GUI apps, servers and tkinter windows
detach instead of dying with the 15s timeout
- agent prompt: explicit guidance for background execution and tkinter
windows, plus the never-give-up rule
- tests: 34 passing (8 new cases for URL building and validation)

Polish:

- professional tone across all text: emojis removed from docs, CLI,
installer and logs
- dashboard template: emoji-free labels, text action buttons
(Start / Stop / Delete instead of glyphs)

.github/ISSUE_TEMPLATE/config.yml CHANGED
@@ -1,11 +1,11 @@
1
  blank_issues_enabled: false
2
  contact_links:
3
- - name: 📖 Documentation
4
  url: https://github.com/MONAEXPERT/agent/tree/main/docs
5
  about: Getting started, architecture, tools reference and FAQ
6
- - name: 🌐 mona.expert
7
  url: https://agent.mona.expert
8
  about: The cloud dashboard — create an account and get your device key
9
- - name: 🔒 Security issue?
10
  url: https://github.com/MONAEXPERT/agent/blob/main/SECURITY.md
11
  about: Report vulnerabilities privately (do NOT open a public issue)
 
1
  blank_issues_enabled: false
2
  contact_links:
3
+ - name: Documentation
4
  url: https://github.com/MONAEXPERT/agent/tree/main/docs
5
  about: Getting started, architecture, tools reference and FAQ
6
+ - name: mona.expert
7
  url: https://agent.mona.expert
8
  about: The cloud dashboard — create an account and get your device key
9
+ - name: Security issue?
10
  url: https://github.com/MONAEXPERT/agent/blob/main/SECURITY.md
11
  about: Report vulnerabilities privately (do NOT open a public issue)
README.md CHANGED
@@ -1,4 +1,4 @@
1
- # mona-agent — your computer, with a brain in the cloud
2
 
3
  <p align="center">
4
  <strong>The open-source AI agent that lives on your machine.<br/>
@@ -44,21 +44,21 @@ to do. The other just gets it done.
44
 
45
  ## What it can do
46
 
47
- - 🖥 **Operate your computer from anywhere** — ask from your phone: *"how's
48
  the disk looking?"*, *"restart nginx"*, *"open Safari"*
49
- - 🔄 **Thinkactobservedeliver** — a real agentic loop: the cloud
50
  brain plans, the device executes local tools, results flow back, and the
51
  answer lands in your chat — up to 8 tool steps per task
52
- - 🛡 **Fail is never allowed** — transient errors retry automatically,
53
  failed commands trigger self-diagnosis and a smarter second attempt,
54
  and every task ends with an answer
55
- - 🔐 **Zero secrets on the device** — no AI provider keys are stored
56
  locally; they live in the encrypted cloud vault. One key. One brain.
57
- - 📊 **Live device monitoring** — CPU, memory, disk, load and uptime
58
  streamed to your dashboard every 10 seconds, with history sparklines
59
- -**Terminal-native** — a fast TUI with live logs, or a fully headless
60
  daemon; one command installs it, one dependency powers it
61
- - 🆓 **Free and open source** — MIT licensed, forever
62
 
63
  ## Quickstart — 60 seconds
64
 
@@ -111,10 +111,10 @@ Full reference: **[docs/TOOLS.md](docs/TOOLS.md)**
111
 
112
  | | Typical AI chat | mona-agent |
113
  |---|---|---|
114
- | Can it see your machine? ||live metrics & files |
115
- | Can it execute? ||sandboxed shell & tools |
116
- | Does it retry when things fail? ||auto-debug + retry loop |
117
- | Where do your API keys live? | on your disk | 🔐 encrypted cloud vault |
118
  | Install | app + account | one command, one dependency |
119
 
120
  ## FAQ
@@ -166,13 +166,13 @@ boxes, Raspberry Pis, home servers. See
166
 
167
  ## Community
168
 
169
- **Star the repo** if mona-agent is useful to you — it genuinely helps
170
  others discover the project.
171
 
172
- - 💬 [GitHub Discussions](https://github.com/MONAEXPERT/agent/discussions) — ideas, questions, show & tell
173
- - 🐛 [Issues](https://github.com/MONAEXPERT/agent/issues) — bugs and feature requests
174
- - 🌐 [agent.mona.expert](https://agent.mona.expert) — the cloud dashboard
175
- - 🔒 [SECURITY.md](SECURITY.md) — responsible disclosure
176
 
177
  ## Development
178
 
@@ -194,4 +194,4 @@ MIT — see [LICENSE](LICENSE). Free forever.
194
  ---
195
 
196
  **[mona.expert](https://agent.mona.expert)** — one key, one brain, any
197
- device.
 
1
+ # mona-agent — your computer, with a brain in the cloud
2
 
3
  <p align="center">
4
  <strong>The open-source AI agent that lives on your machine.<br/>
 
44
 
45
  ## What it can do
46
 
47
+ - **Operate your computer from anywhere** — ask from your phone: *"how's
48
  the disk looking?"*, *"restart nginx"*, *"open Safari"*
49
+ - **Think act observe deliver** — a real agentic loop: the cloud
50
  brain plans, the device executes local tools, results flow back, and the
51
  answer lands in your chat — up to 8 tool steps per task
52
+ - **Fail is never allowed** — transient errors retry automatically,
53
  failed commands trigger self-diagnosis and a smarter second attempt,
54
  and every task ends with an answer
55
+ - **Zero secrets on the device** — no AI provider keys are stored
56
  locally; they live in the encrypted cloud vault. One key. One brain.
57
+ - **Live device monitoring** — CPU, memory, disk, load and uptime
58
  streamed to your dashboard every 10 seconds, with history sparklines
59
+ - **Terminal-native** — a fast TUI with live logs, or a fully headless
60
  daemon; one command installs it, one dependency powers it
61
+ - **Free and open source** — MIT licensed, forever
62
 
63
  ## Quickstart — 60 seconds
64
 
 
111
 
112
  | | Typical AI chat | mona-agent |
113
  |---|---|---|
114
+ | Can it see your machine? | | live metrics & files |
115
+ | Can it execute? | | sandboxed shell & tools |
116
+ | Does it retry when things fail? | | auto-debug + retry loop |
117
+ | Where do your API keys live? | on your disk | encrypted cloud vault |
118
  | Install | app + account | one command, one dependency |
119
 
120
  ## FAQ
 
166
 
167
  ## Community
168
 
169
+ **Star the repo** if mona-agent is useful to you — it genuinely helps
170
  others discover the project.
171
 
172
+ - [GitHub Discussions](https://github.com/MONAEXPERT/agent/discussions) — ideas, questions, show & tell
173
+ - [Issues](https://github.com/MONAEXPERT/agent/issues) — bugs and feature requests
174
+ - [agent.mona.expert](https://agent.mona.expert) — the cloud dashboard
175
+ - [SECURITY.md](SECURITY.md) — responsible disclosure
176
 
177
  ## Development
178
 
 
194
  ---
195
 
196
  **[mona.expert](https://agent.mona.expert)** — one key, one brain, any
197
+ device.
SECURITY.md CHANGED
@@ -7,8 +7,8 @@ the client-side security model and how to report vulnerabilities.
7
 
8
  | Version | Supported |
9
  |---|---|
10
- | 2.x (current) ||
11
- | < 2.0 ||
12
 
13
  ## Security model
14
 
 
7
 
8
  | Version | Supported |
9
  |---|---|
10
+ | 2.x (current) | |
11
+ | < 2.0 | |
12
 
13
  ## Security model
14
 
apps/desktop/bin/mona-agent.js CHANGED
@@ -66,7 +66,7 @@ async function login() {
66
  console.log(` ${CYAN}mona-agent gui${RESET} ${DIM}# terminal dashboard${RESET}`);
67
  console.log(` ${CYAN}mona-agent start${RESET} ${DIM}# headless daemon${RESET}`);
68
  console.log(` ${CYAN}mona-agent connect${RESET} ${DIM}# test connection${RESET}`);
69
- console.log(`\n ${DIM}Enjoying it?Star the repo:${RESET} https://github.com/MONAEXPERT/agent`);
70
  console.log();
71
  rl.close();
72
  } catch (e) {
@@ -313,10 +313,10 @@ async function gui() {
313
  async function start() {
314
  // Fail is never allowed: the daemon logs and survives unexpected errors.
315
  process.on('uncaughtException', (err) => {
316
- process.stderr.write(` ${YELLOW} uncaught: ${err?.message || err}${RESET}\n`);
317
  });
318
  process.on('unhandledRejection', (err) => {
319
- process.stderr.write(` ${YELLOW} unhandled rejection: ${String(err?.message || err)}${RESET}\n`);
320
  });
321
 
322
  const creds = requireCreds();
@@ -324,7 +324,7 @@ async function start() {
324
  console.log(`\n ${BOLD}${CYAN}mona-agent${RESET} ${DIM}v${DEFAULTS.version}${RESET}`);
325
  console.log(` ${DIM}Headless daemon — controlled from ${CLOUD.base}${RESET}`);
326
  console.log(` ${DIM}Log level: ${log.level || 'info'}${RESET}`);
327
- console.log(` ${DIM} ${RESET}${DIM}Star on GitHub:${RESET} https://github.com/MONAEXPERT/agent`);
328
  console.log();
329
 
330
  const daemon = new AgentDaemon(creds);
@@ -338,17 +338,17 @@ async function start() {
338
  });
339
 
340
  daemon.on('auth-failed', () => {
341
- process.stderr.write(` ${RED} Authentication rejected by ${CLOUD.base}${RESET}\n`);
342
  process.stderr.write(` ${YELLOW} Run ${CYAN}mona-agent login${RESET}${YELLOW} with a valid key, then retry.${RESET}\n`);
343
  process.exit(1);
344
  });
345
 
346
  daemon.on('task:done', (result) => {
347
- process.stderr.write(` ${GREEN} Done (${result.tokens} tokens)${RESET}\n`);
348
  });
349
 
350
  daemon.on('error', (err) => {
351
- process.stderr.write(` ${RED} ${err.message}${RESET}\n`);
352
  });
353
 
354
  const stop = () => {
 
66
  console.log(` ${CYAN}mona-agent gui${RESET} ${DIM}# terminal dashboard${RESET}`);
67
  console.log(` ${CYAN}mona-agent start${RESET} ${DIM}# headless daemon${RESET}`);
68
  console.log(` ${CYAN}mona-agent connect${RESET} ${DIM}# test connection${RESET}`);
69
+ console.log(`\n ${DIM}Enjoying it? Star the repo:${RESET} https://github.com/MONAEXPERT/agent`);
70
  console.log();
71
  rl.close();
72
  } catch (e) {
 
313
  async function start() {
314
  // Fail is never allowed: the daemon logs and survives unexpected errors.
315
  process.on('uncaughtException', (err) => {
316
+ process.stderr.write(` ${YELLOW} uncaught: ${err?.message || err}${RESET}\n`);
317
  });
318
  process.on('unhandledRejection', (err) => {
319
+ process.stderr.write(` ${YELLOW} unhandled rejection: ${String(err?.message || err)}${RESET}\n`);
320
  });
321
 
322
  const creds = requireCreds();
 
324
  console.log(`\n ${BOLD}${CYAN}mona-agent${RESET} ${DIM}v${DEFAULTS.version}${RESET}`);
325
  console.log(` ${DIM}Headless daemon — controlled from ${CLOUD.base}${RESET}`);
326
  console.log(` ${DIM}Log level: ${log.level || 'info'}${RESET}`);
327
+ console.log(` ${DIM} ${RESET}${DIM}Star on GitHub:${RESET} https://github.com/MONAEXPERT/agent`);
328
  console.log();
329
 
330
  const daemon = new AgentDaemon(creds);
 
338
  });
339
 
340
  daemon.on('auth-failed', () => {
341
+ process.stderr.write(` ${RED} Authentication rejected by ${CLOUD.base}${RESET}\n`);
342
  process.stderr.write(` ${YELLOW} Run ${CYAN}mona-agent login${RESET}${YELLOW} with a valid key, then retry.${RESET}\n`);
343
  process.exit(1);
344
  });
345
 
346
  daemon.on('task:done', (result) => {
347
+ process.stderr.write(` ${GREEN} Done (${result.tokens} tokens)${RESET}\n`);
348
  });
349
 
350
  daemon.on('error', (err) => {
351
+ process.stderr.write(` ${RED} ${err.message}${RESET}\n`);
352
  });
353
 
354
  const stop = () => {
apps/desktop/install.sh CHANGED
@@ -19,7 +19,7 @@ RED='\033[31m'
19
  RESET='\033[0m'
20
 
21
  echo ""
22
- echo -e " ${BOLD}${CYAN} mona-agent${RESET} installer"
23
  echo -e " ───────────────────────"
24
  echo ""
25
 
@@ -36,25 +36,25 @@ echo -e " Platform: ${BOLD}$PLATFORM${RESET} ($ARCH)"
36
 
37
  # ── Prerequisites ───────────────────────────────────────────────
38
  command -v node >/dev/null 2>&1 || {
39
- echo -e " ${RED}${RESET} Node.js 20+ required — install from https://nodejs.org"
40
  exit 1
41
  }
42
  NODE_MAJOR="$(node -p "process.versions.node.split('.')[0]")"
43
  if [ "$NODE_MAJOR" -lt 20 ]; then
44
- echo -e " ${RED}${RESET} Node.js 20+ required (found $(node -v))"
45
  exit 1
46
  fi
47
- echo -e " ${GREEN}${RESET} Node.js $(node -v) | npm $(npm -v)"
48
 
49
  # ── Download ────────────────────────────────────────────────────
50
- echo -e " ${DIM} Downloading ${REPO}@${BRANCH} from GitHub${RESET}"
51
  TMP_DIR="$(mktemp -d)"
52
  trap 'rm -rf "$TMP_DIR"' EXIT
53
  curl -fsSL "https://github.com/$REPO/archive/refs/heads/$BRANCH.tar.gz" \
54
  | tar xz -C "$TMP_DIR" --strip-components=1
55
 
56
  # ── Dependencies ────────────────────────────────────────────────
57
- echo -e " ${DIM} Installing dependencies${RESET}"
58
  ( cd "$TMP_DIR" && npm install --omit=dev --no-audit --no-fund --silent )
59
 
60
  # ── Copy into place (clean replace; config lives outside agent/) ─
@@ -98,9 +98,9 @@ if [ -z "$rc_touched" ] && [ ! -f "$HOME/.zshrc" ] && [ ! -f "$HOME/.bashrc" ] &
98
  fi
99
 
100
  echo ""
101
- echo -e " ${GREEN} mona-agent installed!${RESET}"
102
  echo ""
103
- echo -e " ${BOLD}Enjoying mona-agent?${RESET}Star us on GitHub:"
104
  echo -e " ${CYAN}https://github.com/MONAEXPERT/agent${RESET}"
105
  echo ""
106
  echo -e " ${BOLD}Next steps:${RESET}"
 
19
  RESET='\033[0m'
20
 
21
  echo ""
22
+ echo -e " ${BOLD}${CYAN} mona-agent${RESET} installer"
23
  echo -e " ───────────────────────"
24
  echo ""
25
 
 
36
 
37
  # ── Prerequisites ───────────────────────────────────────────────
38
  command -v node >/dev/null 2>&1 || {
39
+ echo -e " ${RED}${RESET} Node.js 20+ required — install from https://nodejs.org"
40
  exit 1
41
  }
42
  NODE_MAJOR="$(node -p "process.versions.node.split('.')[0]")"
43
  if [ "$NODE_MAJOR" -lt 20 ]; then
44
+ echo -e " ${RED}${RESET} Node.js 20+ required (found $(node -v))"
45
  exit 1
46
  fi
47
+ echo -e " ${GREEN}${RESET} Node.js $(node -v) | npm $(npm -v)"
48
 
49
  # ── Download ────────────────────────────────────────────────────
50
+ echo -e " ${DIM} Downloading ${REPO}@${BRANCH} from GitHub${RESET}"
51
  TMP_DIR="$(mktemp -d)"
52
  trap 'rm -rf "$TMP_DIR"' EXIT
53
  curl -fsSL "https://github.com/$REPO/archive/refs/heads/$BRANCH.tar.gz" \
54
  | tar xz -C "$TMP_DIR" --strip-components=1
55
 
56
  # ── Dependencies ────────────────────────────────────────────────
57
+ echo -e " ${DIM} Installing dependencies${RESET}"
58
  ( cd "$TMP_DIR" && npm install --omit=dev --no-audit --no-fund --silent )
59
 
60
  # ── Copy into place (clean replace; config lives outside agent/) ─
 
98
  fi
99
 
100
  echo ""
101
+ echo -e " ${GREEN} mona-agent installed!${RESET}"
102
  echo ""
103
+ echo -e " ${BOLD}Enjoying mona-agent?${RESET} Star us on GitHub:"
104
  echo -e " ${CYAN}https://github.com/MONAEXPERT/agent${RESET}"
105
  echo ""
106
  echo -e " ${BOLD}Next steps:${RESET}"
apps/desktop/src/agent.js CHANGED
@@ -264,7 +264,7 @@ export class AgentDaemon extends EventEmitter {
264
  const hasText = answer && answer.trim();
265
  const looksLikeAttempt = hasText && /"tool"\s*:/.test(answer);
266
  if (hasText && !looksLikeAttempt) { final = answer; break; }
267
- // empty or malformedcorrective nudge (auto-reasoning)
268
  if (corrections >= MAX_CORRECTIONS) {
269
  final = hasText ? answer : 'The brain produced no usable reply. Check the activity feed for the trace.';
270
  break;
@@ -382,7 +382,11 @@ ${rows}
382
  How to use a tool — reply with ONLY one JSON object, nothing else:
383
  {"tool":"<tool name>","args":{...}}
384
 
385
- After each tool use you receive a TOOL RESULT message. Use another tool if needed, then answer in plain text. Never invent data you can read with a tool. Keep answers short and direct.`;
 
 
 
 
386
  }
387
 
388
  // ── Lifecycle ───────────────────────────────────────────────────
 
264
  const hasText = answer && answer.trim();
265
  const looksLikeAttempt = hasText && /"tool"\s*:/.test(answer);
266
  if (hasText && !looksLikeAttempt) { final = answer; break; }
267
+ // empty or malformed corrective nudge (auto-reasoning)
268
  if (corrections >= MAX_CORRECTIONS) {
269
  final = hasText ? answer : 'The brain produced no usable reply. Check the activity feed for the trace.';
270
  break;
 
382
  How to use a tool — reply with ONLY one JSON object, nothing else:
383
  {"tool":"<tool name>","args":{...}}
384
 
385
+ Rules:
386
+ - GUI apps, servers, and long-running programs (e.g. a Python tkinter window) MUST use the shell tool with "background":true so they keep running.
387
+ - To create a Python GUI window, generate a tkinter script and run it with "python3 -c '...'" in the background.
388
+ - Never invent data you can read with a tool. Keep answers short and direct.
389
+ - If a command fails, diagnose and retry differently — never give up.`;
390
  }
391
 
392
  // ── Lifecycle ───────────────────────────────────────────────────
apps/desktop/src/control.js CHANGED
@@ -120,7 +120,7 @@ export class ControlChannel extends EventEmitter {
120
  } else if (msg.type === 'command') {
121
  this.emit('command', msg);
122
  } else if (CLOUD.platform === 'docker' && msg.type === 'chat') {
123
- // Docker dashboard chatsame run flow as a command.
124
  this.emit('command', { action: 'run', runId: msg.requestId, payload: { task: msg.message } });
125
  } else if (msg.type === 'ping') {
126
  this.#send('pong', {});
 
120
  } else if (msg.type === 'command') {
121
  this.emit('command', msg);
122
  } else if (CLOUD.platform === 'docker' && msg.type === 'chat') {
123
+ // Docker dashboard chat same run flow as a command.
124
  this.emit('command', { action: 'run', runId: msg.requestId, payload: { task: msg.message } });
125
  } else if (msg.type === 'ping') {
126
  this.#send('pong', {});
apps/desktop/src/log.js CHANGED
@@ -8,7 +8,7 @@ import { EventEmitter } from 'node:events';
8
  const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
9
  const COLORS = { debug: '\x1b[90m', info: '\x1b[36m', warn: '\x1b[33m', error: '\x1b[31m' };
10
  const RESET = '\x1b[0m';
11
- const ICONS = { debug: '·', info: '●', warn: '', error: '' };
12
 
13
  class Logger extends EventEmitter {
14
  #level = LEVELS.info;
 
8
  const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
9
  const COLORS = { debug: '\x1b[90m', info: '\x1b[36m', warn: '\x1b[33m', error: '\x1b[31m' };
10
  const RESET = '\x1b[0m';
11
+ const ICONS = { debug: '·', info: '●', warn: '', error: '' };
12
 
13
  class Logger extends EventEmitter {
14
  #level = LEVELS.info;
apps/desktop/src/tools/apps.js ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Application control — launch and quit desktop applications.
2
+ // Multi-OS: macOS (open / osascript), Linux (xdg-open / pkill), Windows (start / taskkill).
3
+
4
+ import { exec } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import os from 'node:os';
7
+
8
+ const pexec = promisify(exec);
9
+ const PLATFORM = os.platform();
10
+
11
+ function safeName(name) {
12
+ // Allow letters, digits, spaces and a small set of safe characters.
13
+ return String(name || '').replace(/[^a-zA-Z0-9 ._\-]/g, '').trim();
14
+ }
15
+
16
+ async function runCmd(cmd, timeoutMs = 8000) {
17
+ try {
18
+ const { stdout, stderr } = await pexec(cmd, { timeout: timeoutMs, maxBuffer: 1 << 20 });
19
+ return { exitCode: 0, stdout: stdout.slice(0, 2000), stderr: stderr.slice(0, 1000) };
20
+ } catch (err) {
21
+ return {
22
+ exitCode: err.code ?? 1,
23
+ stdout: (err.stdout || '').slice(0, 2000),
24
+ stderr: (err.stderr || '').slice(0, 1000),
25
+ error: err.message,
26
+ };
27
+ }
28
+ }
29
+
30
+ export const apps = {
31
+ name: 'apps',
32
+ description: `Launch or quit desktop applications (${PLATFORM}).`,
33
+ args: {
34
+ action: 'string — open | quit',
35
+ app: 'string — application name (e.g. Safari, Calculator, Spotify, Terminal)',
36
+ },
37
+ platform: PLATFORM,
38
+
39
+ async run(args) {
40
+ const action = String(args.action || 'open').toLowerCase();
41
+ const app = safeName(args.app);
42
+ if (!app) return { error: 'app name required' };
43
+
44
+ if (action === 'open') {
45
+ if (PLATFORM === 'darwin') return runCmd(`open -a "${app}"`);
46
+ if (PLATFORM === 'linux') return runCmd(`gtk-launch "${app}" 2>/dev/null || xdg-open "app:${app}" 2>/dev/null || echo "unknown app: ${app}"`);
47
+ if (PLATFORM === 'win32') return runCmd(`start "" "${app}"`);
48
+ return { error: `Unsupported platform: ${PLATFORM}` };
49
+ }
50
+
51
+ if (action === 'quit') {
52
+ if (PLATFORM === 'darwin') return runCmd(`osascript -e 'quit app "${app}"'`);
53
+ if (PLATFORM === 'linux') return runCmd(`pkill -x "${app}" 2>/dev/null; echo done`);
54
+ if (PLATFORM === 'win32') return runCmd(`taskkill /IM "${app}.exe" /F 2>nul || echo "not running"`);
55
+ return { error: `Unsupported platform: ${PLATFORM}` };
56
+ }
57
+
58
+ return { error: `Unknown action: ${action} (use open | quit)` };
59
+ },
60
+ };
apps/desktop/src/tools/browser.js ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Browser control — open URLs and run web searches in the default browser.
2
+ // Multi-OS: macOS (open + osascript), Linux (xdg-open), Windows (start).
3
+
4
+ import { exec } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import os from 'node:os';
7
+
8
+ const pexec = promisify(exec);
9
+ const PLATFORM = os.platform();
10
+
11
+ /** Build a search URL for the requested site. Exported for tests. */
12
+ export function searchUrl(query, site = 'web') {
13
+ const q = encodeURIComponent(String(query || '').slice(0, 300));
14
+ switch (String(site).toLowerCase()) {
15
+ case 'youtube':
16
+ return `https://www.youtube.com/results?search_query=${q}`;
17
+ case 'google':
18
+ return `https://www.google.com/search?q=${q}`;
19
+ case 'web':
20
+ default:
21
+ return `https://www.bing.com/search?q=${q}`;
22
+ }
23
+ }
24
+
25
+ /** Validate a URL for the open action. Exported for tests. */
26
+ export function validateUrl(url) {
27
+ try {
28
+ const u = new URL(String(url));
29
+ if (!['http:', 'https:'].includes(u.protocol)) return null;
30
+ return u.toString();
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ async function openWith(cmd, timeoutMs = 8000) {
37
+ try {
38
+ await pexec(cmd, { timeout: timeoutMs, maxBuffer: 1 << 20 });
39
+ return { exitCode: 0, opened: true };
40
+ } catch (err) {
41
+ return { exitCode: err.code ?? 1, error: err.message, opened: false };
42
+ }
43
+ }
44
+
45
+ export const browser = {
46
+ name: 'browser',
47
+ description: `Open URLs or run web searches (YouTube, Google, web) in the default browser (${PLATFORM}).`,
48
+ args: {
49
+ action: 'string — open | search | watch',
50
+ url: 'string — full URL (for open)',
51
+ query: 'string — search terms (for search/watch)',
52
+ site: 'string — youtube | google | web (default web)',
53
+ },
54
+ platform: PLATFORM,
55
+
56
+ async run(args) {
57
+ const action = String(args.action || 'open').toLowerCase();
58
+
59
+ let url = null;
60
+ if (action === 'open') {
61
+ url = validateUrl(args.url);
62
+ if (!url) return { error: 'A valid http(s) URL is required (url=...)' };
63
+ } else if (action === 'search' || action === 'watch') {
64
+ const site = args.site || (action === 'watch' ? 'youtube' : 'web');
65
+ url = searchUrl(args.query || '', site);
66
+ if (!String(args.query || '').trim()) return { error: 'query required' };
67
+ } else {
68
+ return { error: `Unknown action: ${action} (use open | search | watch)` };
69
+ }
70
+
71
+ if (PLATFORM === 'darwin') {
72
+ const res = await openWith(`open "${url}"`);
73
+ if (action === 'watch') {
74
+ // Best effort: ask Safari to play the first video result. Requires
75
+ // "Allow JavaScript from Apple Events" in Safari — falls back to the
76
+ // results page if the setting is off.
77
+ try {
78
+ await pexec(
79
+ `osascript -e 'tell application "Safari" to activate' -e 'delay 2' -e 'tell application "Safari" to do JavaScript "document.querySelector(\\"ytd-video-renderer a#video-title\\")?.click()" in front document'`,
80
+ { timeout: 12000 }
81
+ );
82
+ res.autoplayAttempted = true;
83
+ } catch {
84
+ res.autoplayAttempted = false;
85
+ res.note = 'Opened the results page. Click the first result to play (Safari automation requires "Allow JavaScript from Apple Events").';
86
+ }
87
+ }
88
+ return { ...res, url };
89
+ }
90
+ if (PLATFORM === 'linux') {
91
+ const res = await openWith(`xdg-open "${url}"`);
92
+ return { ...res, url, note: action === 'watch' ? 'Opened the search results page — click the first result to play.' : undefined };
93
+ }
94
+ if (PLATFORM === 'win32') {
95
+ const res = await openWith(`start "" "${url}"`);
96
+ return { ...res, url, note: action === 'watch' ? 'Opened the search results page — click the first result to play.' : undefined };
97
+ }
98
+ return { error: `Unsupported platform: ${PLATFORM}` };
99
+ },
100
+ };
apps/desktop/src/tools/index.js CHANGED
@@ -7,8 +7,10 @@ import { sysinfo } from './sysinfo.js';
7
  import { shell } from './shell.js';
8
  import { files } from './files.js';
9
  import { net } from './net.js';
 
 
10
 
11
- const BUILTIN = [sysinfo, shell, files, net];
12
  const TIMEOUT_MS = 30_000;
13
 
14
  class ToolRegistry {
 
7
  import { shell } from './shell.js';
8
  import { files } from './files.js';
9
  import { net } from './net.js';
10
+ import { apps } from './apps.js';
11
+ import { browser } from './browser.js';
12
 
13
+ const BUILTIN = [sysinfo, shell, files, net, apps, browser];
14
  const TIMEOUT_MS = 30_000;
15
 
16
  class ToolRegistry {
apps/desktop/src/tools/shell.js CHANGED
@@ -3,7 +3,7 @@
3
  // Only allowlisted commands run by default.
4
  // Set MONA_ALLOW_CMDS to extend. Use MONA_SHELL_UNSAFE=1 to allow anything.
5
 
6
- import { exec } from 'node:child_process';
7
  import { promisify } from 'node:util';
8
  import os from 'node:os';
9
 
@@ -50,7 +50,7 @@ export const security = {
50
  platform: PLATFORM,
51
  };
52
 
53
- // ── Per-OS command mapping (translate common unixwindows) ──────
54
  const CMD_MAP_WIN32 = {
55
  ls: 'dir',
56
  cat: 'type',
@@ -98,8 +98,8 @@ const EXEC_OPTS = {
98
  // ── Tool definition ───────────────────────────────────────────────
99
  export const shell = {
100
  name: 'shell',
101
- description: `Execute a shell command (${PLATFORM}; allowlisted by default; max 15s timeout)`,
102
- args: { cmd: 'string — the command to run' },
103
  platform: PLATFORM,
104
 
105
  async run(args) {
@@ -134,6 +134,24 @@ export const shell = {
134
  }
135
 
136
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  const { stdout, stderr } = await pexec(cmd, EXEC_OPTS);
138
  return {
139
  exitCode: 0,
 
3
  // Only allowlisted commands run by default.
4
  // Set MONA_ALLOW_CMDS to extend. Use MONA_SHELL_UNSAFE=1 to allow anything.
5
 
6
+ import { exec, spawn } from 'node:child_process';
7
  import { promisify } from 'node:util';
8
  import os from 'node:os';
9
 
 
50
  platform: PLATFORM,
51
  };
52
 
53
+ // ── Per-OS command mapping (translate common unix windows) ──────
54
  const CMD_MAP_WIN32 = {
55
  ls: 'dir',
56
  cat: 'type',
 
98
  // ── Tool definition ───────────────────────────────────────────────
99
  export const shell = {
100
  name: 'shell',
101
+ description: `Execute a shell command (${PLATFORM}; allowlisted by default; max 15s timeout; background:true for GUI/long-running processes)`,
102
+ args: { cmd: 'string — the command to run', background: 'bool — optional, detach and return immediately (for GUI apps, servers, tkinter windows)' },
103
  platform: PLATFORM,
104
 
105
  async run(args) {
 
134
  }
135
 
136
  try {
137
+ // Background mode: GUI apps / long-running processes (e.g. tkinter
138
+ // windows) must not block the task or die with the 15s timeout.
139
+ if (args.background) {
140
+ const child = spawn(cfg.shell, ['-c', cmd], {
141
+ detached: true,
142
+ stdio: 'ignore',
143
+ env: { ...process.env, PATH: cfg.path || process.env.PATH },
144
+ });
145
+ child.unref();
146
+ return {
147
+ exitCode: null,
148
+ pid: child.pid,
149
+ background: true,
150
+ note: 'Process started in background and detached from the agent.',
151
+ platform: PLATFORM,
152
+ };
153
+ }
154
+
155
  const { stdout, stderr } = await pexec(cmd, EXEC_OPTS);
156
  return {
157
  exitCode: 0,
apps/desktop/src/tui.js CHANGED
@@ -15,7 +15,7 @@
15
  // r Force reconnect
16
  // c Clear activity log
17
  // d Toggle debug info
18
- ///Scroll log
19
  // g / End Jump to end of log (auto-follow on)
20
  // h / ? Show help + connect instructions
21
 
@@ -84,16 +84,16 @@ const ICON = {
84
  connected: '●',
85
  disconnected: '○',
86
  thinking: '◈',
87
- done: '',
88
- error: '',
89
- tool: '',
90
- arrow: '',
91
  task: '▸',
92
  token: '·',
93
  debug: '…',
94
  };
95
 
96
- const PLATFORM_ICON = { darwin: '🍎', linux: '🐧', win32: '🪟' };
97
  const PLATFORM_LABEL = { darwin: 'macOS', linux: 'Linux', win32: 'Windows' };
98
 
99
  // ── Connection states ─────────────────────────────────────────────
@@ -574,7 +574,7 @@ export class Dashboard {
574
  ` ${ansi.fg.bGreen}c${ansi.reset} Clear activity log`,
575
  ` ${ansi.fg.bGreen}d${ansi.reset} Toggle debug info bar`,
576
  ` ${ansi.fg.bGreen}h${ansi.reset} / ${ansi.fg.bGreen}?${ansi.reset} Show this help (any key to dismiss)`,
577
- ` ${ansi.fg.bGreen} / ${ansi.reset} Scroll activity log`,
578
  '',
579
  ` ${ansi.bold}Environment${ansi.reset}`,
580
  ` ${ansi.dim}Cloud:${ansi.reset} ${CLOUD.base}`,
@@ -683,7 +683,7 @@ export class Dashboard {
683
  #logIcon(type) {
684
  switch (type) {
685
  case 'info': return `${ansi.fg.bCyan}${ICON.connected}${ansi.reset}`;
686
- case 'warn': return `${ansi.fg.bYellow}${ansi.reset}`;
687
  case 'error': return `${ansi.fg.bRed}${ICON.error}${ansi.reset}`;
688
  case 'task': return `${ansi.fg.bMagenta}${ICON.task}${ansi.reset}`;
689
  case 'done': return `${ansi.fg.bGreen}${ICON.done}${ansi.reset}`;
 
15
  // r Force reconnect
16
  // c Clear activity log
17
  // d Toggle debug info
18
+ // / Scroll log
19
  // g / End Jump to end of log (auto-follow on)
20
  // h / ? Show help + connect instructions
21
 
 
84
  connected: '●',
85
  disconnected: '○',
86
  thinking: '◈',
87
+ done: '',
88
+ error: '',
89
+ tool: '',
90
+ arrow: '',
91
  task: '▸',
92
  token: '·',
93
  debug: '…',
94
  };
95
 
96
+ const PLATFORM_ICON = { darwin: '', linux: '', win32: '' };
97
  const PLATFORM_LABEL = { darwin: 'macOS', linux: 'Linux', win32: 'Windows' };
98
 
99
  // ── Connection states ─────────────────────────────────────────────
 
574
  ` ${ansi.fg.bGreen}c${ansi.reset} Clear activity log`,
575
  ` ${ansi.fg.bGreen}d${ansi.reset} Toggle debug info bar`,
576
  ` ${ansi.fg.bGreen}h${ansi.reset} / ${ansi.fg.bGreen}?${ansi.reset} Show this help (any key to dismiss)`,
577
+ ` ${ansi.fg.bGreen} / ${ansi.reset} Scroll activity log`,
578
  '',
579
  ` ${ansi.bold}Environment${ansi.reset}`,
580
  ` ${ansi.dim}Cloud:${ansi.reset} ${CLOUD.base}`,
 
683
  #logIcon(type) {
684
  switch (type) {
685
  case 'info': return `${ansi.fg.bCyan}${ICON.connected}${ansi.reset}`;
686
+ case 'warn': return `${ansi.fg.bYellow}${ansi.reset}`;
687
  case 'error': return `${ansi.fg.bRed}${ICON.error}${ansi.reset}`;
688
  case 'task': return `${ansi.fg.bMagenta}${ICON.task}${ansi.reset}`;
689
  case 'done': return `${ansi.fg.bGreen}${ICON.done}${ansi.reset}`;
apps/desktop/test/control.test.mjs CHANGED
@@ -114,7 +114,7 @@ describe('control channel', () => {
114
  let flushed = false;
115
  quiet(ch);
116
  ch.connect();
117
- ch.step('test', { n: 1 }); // sent before openqueued
118
 
119
  await new Promise((resolve) => {
120
  const timer = setInterval(() => {
 
114
  let flushed = false;
115
  quiet(ch);
116
  ch.connect();
117
+ ch.step('test', { n: 1 }); // sent before open queued
118
 
119
  await new Promise((resolve) => {
120
  const timer = setInterval(() => {
apps/desktop/test/tools-new.test.mjs ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Tests for the newer tools: pure functions only (no real launches).
2
+
3
+ import { describe, it } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { searchUrl, validateUrl } from '../src/tools/browser.js';
6
+
7
+ describe('browser searchUrl', () => {
8
+ it('builds a YouTube search URL', () => {
9
+ assert.equal(
10
+ searchUrl('avicii wake me up', 'youtube'),
11
+ 'https://www.youtube.com/results?search_query=avicii%20wake%20me%20up'
12
+ );
13
+ });
14
+
15
+ it('builds a Google search URL', () => {
16
+ assert.equal(searchUrl('mona agent', 'google'), 'https://www.google.com/search?q=mona%20agent');
17
+ });
18
+
19
+ it('defaults to web search', () => {
20
+ assert.match(searchUrl('test'), /^https:\/\/www\.bing\.com\/search\?q=test$/);
21
+ });
22
+
23
+ it('encodes special characters', () => {
24
+ assert.equal(searchUrl('a & b', 'youtube'), 'https://www.youtube.com/results?search_query=a%20%26%20b');
25
+ });
26
+ });
27
+
28
+ describe('browser validateUrl', () => {
29
+ it('accepts https URLs', () => {
30
+ assert.equal(validateUrl('https://example.com/x'), 'https://example.com/x');
31
+ });
32
+
33
+ it('accepts http URLs', () => {
34
+ assert.equal(validateUrl('http://example.com'), 'http://example.com/');
35
+ });
36
+
37
+ it('rejects non-http(s) schemes', () => {
38
+ assert.equal(validateUrl('file:///etc/passwd'), null);
39
+ assert.equal(validateUrl('javascript:alert(1)'), null);
40
+ });
41
+
42
+ it('rejects malformed URLs', () => {
43
+ assert.equal(validateUrl('not a url'), null);
44
+ });
45
+ });
apps/desktop/test/tui-scroll.test.mjs CHANGED
@@ -41,7 +41,7 @@ describe('dashboard scroll', () => {
41
  text = await render(out);
42
  assert.ok(!text.includes('fill29 done'), 'scrolled away from the newest entry');
43
 
44
- // New activity arrivesthe view must snap back to the bottom.
45
  agent.emit('tool:done', 'fresh', {});
46
  text = await render(out);
47
  assert.ok(text.includes('fresh done'), 'auto-follow pulled the view back to the newest entry');
 
41
  text = await render(out);
42
  assert.ok(!text.includes('fill29 done'), 'scrolled away from the newest entry');
43
 
44
+ // New activity arrives the view must snap back to the bottom.
45
  agent.emit('tool:done', 'fresh', {});
46
  text = await render(out);
47
  assert.ok(text.includes('fresh done'), 'auto-follow pulled the view back to the newest entry');
docs/ARCHITECTURE.md CHANGED
@@ -21,8 +21,8 @@ mona-agent is a **headless Node.js daemon** with two jobs:
21
  │ │ ControlChannel│◄─►│ tools/ │ │ │ /api/v1/agent/stats │
22
  │ │ (HTTPS + WS) │ │ files │ │ │ /api/v1/agent/chat … │
23
  │ │ │ │ shell │ │ │ │
24
- │ │metrics │ │ net │ │ │ AI engine (the brain) │
25
- │ │commands │ │ sysinfo │ │ │ Dashboard + device overview │
26
  │ └──────────────┘ └────────────┘ │ │ Key vault (AES-256) │
27
  │ │ │ │ Audit log │
28
  │ ▼ │ └──────────────────────────────┘
@@ -71,7 +71,7 @@ CLI, or the cloud queue):
71
  ```
72
  ┌───────────────────────────────────────────────────┐
73
  │ mona.expert brain │
74
- │ reasonanswer in text OR emit one tool call │
75
  └───────────────┬───────────────────────▲──────────┘
76
  task (HTTPS) │ │ tool result
77
  ┌───────────────▼───────────────────────┴──────────┐
 
21
  │ │ ControlChannel│◄─►│ tools/ │ │ │ /api/v1/agent/stats │
22
  │ │ (HTTPS + WS) │ │ files │ │ │ /api/v1/agent/chat … │
23
  │ │ │ │ shell │ │ │ │
24
+ │ │ metrics │ │ net │ │ │ AI engine (the brain) │
25
+ │ │ commands │ │ sysinfo │ │ │ Dashboard + device overview │
26
  │ └──────────────┘ └────────────┘ │ │ Key vault (AES-256) │
27
  │ │ │ │ Audit log │
28
  │ ▼ │ └──────────────────────────────┘
 
71
  ```
72
  ┌───────────────────────────────────────────────────┐
73
  │ mona.expert brain │
74
+ │ reason answer in text OR emit one tool call │
75
  └───────────────┬───────────────────────▲──────────┘
76
  task (HTTPS) │ │ tool result
77
  ┌───────────────▼───────────────────────┴──────────┐
docs/CHANGELOG.md CHANGED
@@ -11,7 +11,7 @@ Format: [Keep a Changelog](https://keepachangelog.com), versioning:
11
  - **Agentic execution loop** — the device is no longer a listener; it's an
12
  operator. Tasks from the dashboard flow into a cloud task queue, the
13
  device claims them within seconds, and the mona.expert brain plans the
14
- work: thinkactobservedeliver. Up to 8 tool steps per task, with
15
  every step streamed to the dashboard activity feed in real time.
16
  - **Cloud task queue (WS-free command channel)** — devices poll for work
17
  every 2 s over HTTPS, so command execution works on every hosting
 
11
  - **Agentic execution loop** — the device is no longer a listener; it's an
12
  operator. Tasks from the dashboard flow into a cloud task queue, the
13
  device claims them within seconds, and the mona.expert brain plans the
14
+ work: think act observe deliver. Up to 8 tool steps per task, with
15
  every step streamed to the dashboard activity feed in real time.
16
  - **Cloud task queue (WS-free command channel)** — devices poll for work
17
  every 2 s over HTTPS, so command execution works on every hosting
docs/FAQ.md CHANGED
@@ -14,7 +14,7 @@ cloud has a free tier.
14
  ## Does mona-agent need an API key?
15
 
16
  It needs **one key**: your mona.expert device token (created in the
17
- dashboardSettings). AI provider keys (OpenAI, Anthropic, …) live only in
18
  the cloud vault — never on your device.
19
 
20
  ## Where is my key stored?
 
14
  ## Does mona-agent need an API key?
15
 
16
  It needs **one key**: your mona.expert device token (created in the
17
+ dashboard Settings). AI provider keys (OpenAI, Anthropic, …) live only in
18
  the cloud vault — never on your device.
19
 
20
  ## Where is my key stored?
docs/GETTING-STARTED.md CHANGED
@@ -9,7 +9,7 @@ have your machine connected to the cloud in under a minute.
9
  - macOS: `brew install node`
10
  - Ubuntu/Debian: `curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt install nodejs`
11
  - Windows: use [WSL2](https://learn.microsoft.com/windows/wsl/install) (native Windows Git Bash works too)
12
- - An account + **API key** at [agent.mona.expert](https://agent.mona.expert/dashboard)Settings.
13
 
14
  ## 2. Install
15
 
@@ -54,7 +54,7 @@ CPU, memory, disk and load — and a chat window connected to the cloud brain.
54
  | `mona-agent: command not found` | Re-open your terminal, or run `export PATH="$HOME/.local/bin:$PATH"` |
55
  | `Node.js 20+ required` | Upgrade Node (`brew upgrade node` / nodesource) |
56
  | Agent connects but dashboard shows no device | Confirm you ran `mona-agent login` with the key from your account |
57
- | Metrics stream, chat replies "No API key configured" | Add an AI provider key in the dashboardSettings (the cloud brain needs one) |
58
  | Firewall / corporate proxy | Set `MONA_CLOUD=https://agent.mona.expert` and check HTTPS egress |
59
 
60
  ## Uninstall
 
9
  - macOS: `brew install node`
10
  - Ubuntu/Debian: `curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt install nodejs`
11
  - Windows: use [WSL2](https://learn.microsoft.com/windows/wsl/install) (native Windows Git Bash works too)
12
+ - An account + **API key** at [agent.mona.expert](https://agent.mona.expert/dashboard) Settings.
13
 
14
  ## 2. Install
15
 
 
54
  | `mona-agent: command not found` | Re-open your terminal, or run `export PATH="$HOME/.local/bin:$PATH"` |
55
  | `Node.js 20+ required` | Upgrade Node (`brew upgrade node` / nodesource) |
56
  | Agent connects but dashboard shows no device | Confirm you ran `mona-agent login` with the key from your account |
57
+ | Metrics stream, chat replies "No API key configured" | Add an AI provider key in the dashboard Settings (the cloud brain needs one) |
58
  | Firewall / corporate proxy | Set `MONA_CLOUD=https://agent.mona.expert` and check HTTPS egress |
59
 
60
  ## Uninstall
docs/_config.yml CHANGED
@@ -1,5 +1,5 @@
1
  # GitHub Pages config for the docs site (optional)
2
- # Enable in repo SettingsPagesSource: main /docs to publish
3
  # at https://monaexpert.github.io/agent/
4
  title: mona-agent — cloud-brained AI agent for your device
5
  description: >-
 
1
  # GitHub Pages config for the docs site (optional)
2
+ # Enable in repo Settings Pages Source: main /docs to publish
3
  # at https://monaexpert.github.io/agent/
4
  title: mona-agent — cloud-brained AI agent for your device
5
  description: >-
package.json CHANGED
@@ -16,7 +16,7 @@
16
  "start": "node apps/desktop/bin/mona-agent.js start",
17
  "gui": "node apps/desktop/bin/mona-agent.js gui",
18
  "login": "node apps/desktop/bin/mona-agent.js login",
19
- "test": "node --test apps/desktop/test/agent.test.mjs apps/desktop/test/control.test.mjs apps/desktop/test/docker-control.test.mjs apps/desktop/test/tui-scroll.test.mjs"
20
  },
21
  "engines": {
22
  "node": ">=20"
 
16
  "start": "node apps/desktop/bin/mona-agent.js start",
17
  "gui": "node apps/desktop/bin/mona-agent.js gui",
18
  "login": "node apps/desktop/bin/mona-agent.js login",
19
+ "test": "node --test apps/desktop/test/agent.test.mjs apps/desktop/test/control.test.mjs apps/desktop/test/docker-control.test.mjs apps/desktop/test/tui-scroll.test.mjs apps/desktop/test/tools-new.test.mjs"
20
  },
21
  "engines": {
22
  "node": ">=20"
packages/engine/src/engine.mjs CHANGED
@@ -34,7 +34,7 @@ export class EngineClient {
34
 
35
  async authenticate(keyOverride) {
36
  const key = keyOverride || this.key;
37
- if (!key) return { ok: false, detail: 'No mona.expert key is stored. Add it under SettingsEngine key.' };
38
  try {
39
  await this.request('/v1/health', { key, timeoutMs: 10000 });
40
  return { ok: true, detail: 'Engine reachable.' };
 
34
 
35
  async authenticate(keyOverride) {
36
  const key = keyOverride || this.key;
37
+ if (!key) return { ok: false, detail: 'No mona.expert key is stored. Add it under Settings Engine key.' };
38
  try {
39
  await this.request('/v1/health', { key, timeoutMs: 10000 });
40
  return { ok: true, detail: 'Engine reachable.' };
packages/engine/src/simulated.mjs CHANGED
@@ -5,8 +5,8 @@ import { estimateTokens } from './sse.mjs';
5
  * (assertProductionReady refuses SIMULATED_ENGINE_ENABLED=true in prod).
6
  *
7
  * It is NOT a language model. It exercises the full engine loop offline:
8
- * gateway authconversation historyengine calltool_callscommand
9
- * devicetool resultengine continuationstreamingusageaudit.
10
  *
11
  * Tool-call trigger: a user message containing `tool: <name>` (e.g. `tool: sysinfo`)
12
  * issues one tool_call for the named device tool; the tool result is echoed into
@@ -34,7 +34,7 @@ function compose({ messages, system, agentName }) {
34
  lines.push(`The mona.expert engine would answer here and pick the model itself.`);
35
  lines.push(`Echo for verification: *${words.slice(0, 12).join(' ')}${words.length > 12 ? '…' : ''}*`);
36
  if (/hi|hello|hey/i.test(last)) {
37
- lines.push('', 'Connected end to end: devicegatewayengineback. Try `tool: sysinfo` to run a real command on the device.');
38
  }
39
  return lines.join('\n');
40
  }
 
5
  * (assertProductionReady refuses SIMULATED_ENGINE_ENABLED=true in prod).
6
  *
7
  * It is NOT a language model. It exercises the full engine loop offline:
8
+ * gateway auth conversation history engine call tool_calls command
9
+ * device tool result engine continuation streaming usage audit.
10
  *
11
  * Tool-call trigger: a user message containing `tool: <name>` (e.g. `tool: sysinfo`)
12
  * issues one tool_call for the named device tool; the tool result is echoed into
 
34
  lines.push(`The mona.expert engine would answer here and pick the model itself.`);
35
  lines.push(`Echo for verification: *${words.slice(0, 12).join(' ')}${words.length > 12 ? '…' : ''}*`);
36
  if (/hi|hello|hey/i.test(last)) {
37
+ lines.push('', 'Connected end to end: device gateway engine back. Try `tool: sysinfo` to run a real command on the device.');
38
  }
39
  return lines.join('\n');
40
  }
packages/protocol/src/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
  /**
2
- * mona.expert wire contract — the single source of truth for the devicegateway
3
  * WebSocket protocol. The daemon (apps/desktop) and the gateway (apps/gateway)
4
  * implement this; `test/protocol.test.mjs` fails if they drift apart.
5
  *
@@ -20,22 +20,22 @@ export const CLOSE_CODES = Object.freeze({
20
  export const isTerminalClose = (code) =>
21
  code === CLOSE_CODES.UNAUTHORIZED || code === CLOSE_CODES.REVOKED || code === CLOSE_CODES.PROTOCOL;
22
 
23
- /** Message types. `device ` frames are sent by the daemon; `gateway ` by the control plane. */
24
  export const TYPES = Object.freeze({
25
- /* handshake (device ) */
26
  HELLO: 'hello',
27
- HELLO_OK: 'hello.ok', // gateway{ heartbeatIntervalMs, permissions }
28
  /* registration */
29
- REGISTER: 'register', // devicedevice fingerprint / name
30
- /* commands (gatewaydevicegateway) */
31
- COMMAND: 'command', // gateway{ id, tool, args, timeoutMs, requestId }
32
- COMMAND_RESULT: 'command.result', // device{ id, ok, output, error, durationMs }
33
- COMMAND_ERROR: 'command.error', // device{ id, error }
34
  /* reasoning stream (device reports steps the engine asked it to take) */
35
- AGENT_STEP: 'agent.step', // device{ runId, name, detail }
36
- AGENT_TOKEN: 'agent.token', // device{ runId, text } (kept for TUI streaming)
37
- AGENT_RESULT: 'agent.result', // device{ runId, ok, text, usage, error }
38
- /* telemetry (device ) */
39
  DEVICE_METRICS: 'device.metrics',
40
  /* liveness */
41
  PING: 'ping',
 
1
  /**
2
+ * mona.expert wire contract — the single source of truth for the device gateway
3
  * WebSocket protocol. The daemon (apps/desktop) and the gateway (apps/gateway)
4
  * implement this; `test/protocol.test.mjs` fails if they drift apart.
5
  *
 
20
  export const isTerminalClose = (code) =>
21
  code === CLOSE_CODES.UNAUTHORIZED || code === CLOSE_CODES.REVOKED || code === CLOSE_CODES.PROTOCOL;
22
 
23
+ /** Message types. `device ` frames are sent by the daemon; `gateway ` by the control plane. */
24
  export const TYPES = Object.freeze({
25
+ /* handshake (device ) */
26
  HELLO: 'hello',
27
+ HELLO_OK: 'hello.ok', // gateway { heartbeatIntervalMs, permissions }
28
  /* registration */
29
+ REGISTER: 'register', // device device fingerprint / name
30
+ /* commands (gateway device gateway) */
31
+ COMMAND: 'command', // gateway { id, tool, args, timeoutMs, requestId }
32
+ COMMAND_RESULT: 'command.result', // device { id, ok, output, error, durationMs }
33
+ COMMAND_ERROR: 'command.error', // device { id, error }
34
  /* reasoning stream (device reports steps the engine asked it to take) */
35
+ AGENT_STEP: 'agent.step', // device { runId, name, detail }
36
+ AGENT_TOKEN: 'agent.token', // device { runId, text } (kept for TUI streaming)
37
+ AGENT_RESULT: 'agent.result', // device { runId, ok, text, usage, error }
38
+ /* telemetry (device ) */
39
  DEVICE_METRICS: 'device.metrics',
40
  /* liveness */
41
  PING: 'ping',