Mona commited on
Commit
17d402e
·
1 Parent(s): 5b137c2

feat: add CLI with login, gui, start, status, and help commands

Browse files

Command-line interface with five commands:
- login: interactive API key verification and secure storage
- gui: full terminal dashboard with live metrics
- start: headless daemon mode with console logging
- status: display connection state and config paths
- help: usage guide with environment variables

Auto-detection: GUI if TTY, headless otherwise when no command
is specified. Graceful shutdown on SIGINT/SIGTERM.

Removed deprecated GUI server and telemetry files from v0.1.0.

Files changed (6) hide show
  1. README.md +155 -37
  2. bin/mona-agent.js +150 -36
  3. install.sh +49 -22
  4. src/gui/index.html +0 -146
  5. src/gui/server.js +0 -61
  6. src/telemetry.js +0 -67
README.md CHANGED
@@ -1,63 +1,181 @@
1
- # Mona Agent
2
 
3
- A lightweight, OpenClaw-style device agent with a **cloud brain**.
 
 
4
 
5
- - 🔑 Requires an **agent.mona.expert** API key — nothing else.
6
- - 🚫 **No LLM provider keys stored locally.** All reasoning, thinking, and planning happen online at `agent.mona.expert`.
7
- - 📡 Streams **device monitoring, agent steps, and tokens** live to `agent.mona.expert`.
8
- - 🖥️ Clean local **GUI dashboard**.
9
- - ⚡ One-line install.
 
10
 
11
- ## Install
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  ```bash
 
14
  curl -fsSL https://agent.mona.expert/install.sh | bash
 
 
 
 
 
 
 
15
  ```
16
 
17
- Then:
18
 
19
- ```bash
20
- mona-agent login # paste your agent.mona.expert API key
21
- mona-agent run "check disk usage and summarize"
 
 
 
 
 
 
 
 
 
 
 
22
  ```
23
 
24
- Launch the GUI:
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  ```bash
27
- npm run gui # → http://localhost:4319
28
  ```
29
 
30
- ## How it works
 
 
31
 
32
  ```
33
- ┌────────────┐ prompts + steps ┌───────────────────────┐
34
- │ Your │ ─────────────────────▶ │ agent.mona.expert │
35
- │ device │ streamed reasoning │ (LLM brain, keys, │
36
- │ (this app)│ ◀───────────────────── │ policy, memory) │
37
- └────────────┘ └───────────────────────┘
38
- │ device metrics + agent steps (WebSocket) ▲
39
- └────────────────────────────────────────────┘
40
  ```
41
 
42
- - **`src/config.js`** — stores only your `agent.mona.expert` API key (`~/.mona-agent/credentials.json`, chmod 600).
43
- - **`src/cloud.js`** — sends messages to the cloud brain and streams reasoning back.
44
- - **`src/telemetry.js`** — WebSocket stream of device metrics + agent steps to the domain.
45
- - **`src/agent.js`** — the local run loop (remote reasoning, local execution + telemetry).
46
- - **`src/gui/`** — the dashboard.
47
 
48
- ## Commands
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
- | Command | Description |
51
- |---|---|
52
- | `mona-agent login` | Save your API key |
53
- | `mona-agent status` | Show login state |
54
- | `mona-agent run "<task>"` | Run a task (reasoning in the cloud) |
55
- | `npm run gui` | Start the GUI at `http://localhost:4319` |
56
 
57
- ## Privacy
58
 
59
- The only secret on your device is the `agent.mona.expert` API key. Model provider keys, prompts policy, and long-term memory live server-side.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
  ## License
62
 
63
- MIT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mona-agent — Cloud-Brained AI Agent for Your Device
2
 
3
+ <p align="center">
4
+ <strong>Open-source device agent. Free forever. MIT licensed.</strong>
5
+ </p>
6
 
7
+ <p align="center">
8
+ <a href="https://github.com/MONAEXPERT/agent/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
9
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg" alt="Node.js"></a>
10
+ <img src="https://img.shields.io/badge/dependencies-1-lightgrey" alt="1 dependency">
11
+ <img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20WSL2-informational" alt="Platforms">
12
+ </p>
13
 
14
+ ---
15
+
16
+ ## What is this?
17
+
18
+ **mona-agent** is a headless daemon that runs on any device. It connects to a control plane, receives commands, executes local tools, and streams results back.
19
+
20
+ No LLM provider keys (OpenAI, Anthropic, Google, etc.) are stored on your device. Reasoning happens in the cloud — the device is a smart terminal.
21
+
22
+ ```
23
+ Your Device Control Plane (SaaS or self-hosted)
24
+ +-------------------+ +----------------------------+
25
+ | mona-agent |--- WSS ------+ | Dashboard / Website |
26
+ | | | Auth / User Management |
27
+ | * Terminal GUI |<-- commands -- | API Key Vault (AES-256) |
28
+ | * Local tools |-- telemetry -+ | LLM Proxy (5 providers) |
29
+ | * File sandbox |-- tokens ----+ | Agent Orchestration |
30
+ | * Shell guard |-- metrics ---+ | Audit Log |
31
+ | | | |
32
+ | <- NO API KEYS ->| | <-- YOUR KEYS STAY HERE --> |
33
+ +-------------------+ +----------------------------+
34
+ ```
35
+
36
+ ## Quick Start
37
 
38
  ```bash
39
+ # Install
40
  curl -fsSL https://agent.mona.expert/install.sh | bash
41
+
42
+ # Login to your control plane
43
+ mona-agent login # paste your API key
44
+
45
+ # Run
46
+ mona-agent gui # terminal dashboard with live metrics
47
+ mona-agent start # headless daemon (no UI)
48
  ```
49
 
50
+ ## Terminal Dashboard
51
 
52
+ Built-in TUI — zero extra dependencies. Pure ANSI escape codes.
53
+
54
+ ```
55
+ +-- mona-agent v1.0.0 ------------------------- * connected ---+
56
+ | +-- System -------------------+ +-- Activity ---------------+ |
57
+ | | Host MacBook-Pro | | 22:54 * Connected | |
58
+ | | OS darwin arm64 | | 22:55 > Task: "sys info" | |
59
+ | | CPUs 10 cores | | 22:55 ~ Thinking... | |
60
+ | | Mem [#### ] 62% | | 22:56 + Done (142 tok) | |
61
+ | +-- Task ---------------------+ | | |
62
+ | | + Idle | | | |
63
+ | +-----------------------------+ +---------------------------+ |
64
+ | q quit &middot; c clear &middot; arrows scroll |
65
+ +----------------------------------------------------------------+
66
  ```
67
 
68
+ ## Tools
69
+
70
+ The agent ships with four sandboxed tool modules:
71
+
72
+ | Tool | Capabilities | Security |
73
+ |------|-------------|----------|
74
+ | `sysinfo` | OS, CPU, memory, load, network, uptime | Read-only |
75
+ | `shell` | Command execution | Allowlist + blocked patterns |
76
+ | `files` | Read/write/list/delete/stat | Path sandboxed |
77
+ | `net` | HTTP fetch/check, connectivity | HTTP(S)-only |
78
+
79
+ ### Shell Security
80
+
81
+ By default only safe commands are allowed. Extend via environment:
82
 
83
  ```bash
84
+ MONA_ALLOW_CMDS="df,uptime,uname,git,npm,docker" mona-agent start
85
  ```
86
 
87
+ Destructive patterns (`rm -rf /`, `mkfs`, fork bombs) are always blocked.
88
+
89
+ ## Commands
90
 
91
  ```
92
+ mona-agent gui Terminal dashboard with live metrics
93
+ mona-agent start Headless daemon — no UI, log to stderr
94
+ mona-agent login Save your control-plane API key
95
+ mona-agent status Show login state and config paths
96
+ mona-agent help Show all commands and environment vars
 
 
97
  ```
98
 
99
+ Without arguments, auto-detects: GUI if terminal, headless otherwise.
 
 
 
 
100
 
101
+ ## Environment Variables
102
+
103
+ | Variable | Default | Purpose |
104
+ |----------|---------|---------|
105
+ | `MONA_CLOUD` | `https://agent.mona.expert` | Control plane URL |
106
+ | `MONA_CLOUD_WS` | Auto-derived | WebSocket URL override |
107
+ | `MONA_ALLOW_CMDS` | `df,uptime,uname,...` | Shell command allowlist |
108
+ | `MONA_SHELL_UNSAFE` | — | Set to `1` for unrestricted shell |
109
+ | `MONA_WORKSPACE` | `~/.mona-agent/workspace` | File tool sandbox directory |
110
+
111
+ ## Project Structure
112
+
113
+ ```
114
+ agent/
115
+ +-- bin/mona-agent.js CLI entry point
116
+ +-- src/
117
+ | +-- agent.js Agent daemon (reasoning loop)
118
+ | +-- cloud.js Cloud API client (SSE streaming)
119
+ | +-- config.js Configuration & credential management
120
+ | +-- control.js WebSocket control channel (reconnect, metrics)
121
+ | +-- log.js Structured event-driven logger
122
+ | +-- tui.js Terminal dashboard (pure ANSI)
123
+ | +-- tools/
124
+ | +-- index.js Tool registry & dispatcher
125
+ | +-- sysinfo.js System information
126
+ | +-- shell.js Sandboxed shell execution
127
+ | +-- files.js File system operations
128
+ | +-- net.js Network operations
129
+ +-- test/
130
+ | +-- agent.test.mjs 14 unit tests
131
+ +-- install.sh One-line install script
132
+ +-- README.md
133
+ +-- LICENSE
134
+ +-- package.json
135
+ ```
136
+
137
+ ## Architecture
138
+
139
+ **Cloud-brained**: Reasoning happens on the control plane where API keys live. The device executes tools and streams results. The device never touches LLM provider credentials.
140
+
141
+ - **Commands** (`run`, `tool`, `ping`) flow: control plane -> cloud -> device
142
+ - **Telemetry** (`metrics`, `steps`, `tokens`, `results`) flow: device -> cloud -> control plane
143
+ - **API keys** never leave the control plane
144
 
145
+ The device stores **only** the control-plane API key in `~/.mona-agent/credentials.json` (mode `0600`).
 
 
 
 
 
146
 
147
+ ## Open Source & SaaS
148
 
149
+ | What | Where | Price |
150
+ |------|-------|-------|
151
+ | **Device agent** | This repo — install, modify, fork | Free & open source (MIT) |
152
+ | **Control plane** | Self-host or SaaS at [agent.mona.expert](https://agent.mona.expert) | Free tier available |
153
+
154
+ The agent is MIT. Use it with any control plane. The cloud platform at agent.mona.expert provides the managed experience with premium plans for teams.
155
+
156
+ ## Development
157
+
158
+ ```bash
159
+ git clone https://github.com/MONAEXPERT/agent.git
160
+ cd agent
161
+ npm install
162
+ npm test # 14 tests, all passing
163
+ ```
164
 
165
  ## License
166
 
167
+ MIT — free forever.
168
+
169
+ ---
170
+
171
+ <p align="center">
172
+ <sub>
173
+ mona-agent &middot; <a href="https://agent.mona.expert">agent.mona.expert</a> &middot;
174
+ <a href="https://github.com/MONAEXPERT/agent">GitHub</a> &middot;
175
+ <a href="https://github.com/MONAEXPERT/agent/issues">Issues</a>
176
+ </sub>
177
+ </p>
178
+
179
+ <p align="center">
180
+ <sub>Open-source device agent. No LLM keys on your device. Ever.</sub>
181
+ </p>
bin/mona-agent.js CHANGED
@@ -1,68 +1,182 @@
1
  #!/usr/bin/env node
 
 
 
 
 
 
 
 
2
  import { createInterface } from 'node:readline/promises';
3
  import { stdin, stdout } from 'node:process';
4
- import { loadCreds, saveCreds, requireKey, CRED_PATH } from '../src/config.js';
5
  import { verifyKey } from '../src/cloud.js';
6
- import { Agent } from '../src/agent.js';
 
 
7
 
8
- const [cmd, ...rest] = process.argv.slice(2);
9
 
 
10
  async function login() {
11
  const rl = createInterface({ input: stdin, output: stdout });
12
  const existing = loadCreds();
13
- if (existing?.apiKey) console.log('An API key is already saved. This will replace it.');
14
- const apiKey = (await rl.question('agent.mona.expert API key: ')).trim();
 
 
 
 
 
 
15
  rl.close();
16
- if (!apiKey) { console.error('No key entered.'); process.exit(1); }
17
- process.stdout.write('Verifying with cloud... ');
 
 
 
 
 
 
18
  try {
19
  const info = await verifyKey(apiKey);
20
  const path = saveCreds({ apiKey, agentId: info.agentId });
21
- console.log('OK');
22
- console.log(`Linked agent: ${info.agentId || '(pending)'}`);
23
- console.log(`Saved to ${path} (chmod 600, no LLM keys stored).`);
 
 
 
 
 
 
24
  } catch (e) {
25
- console.error('FAILED\n' + e.message);
 
26
  process.exit(1);
27
  }
28
  }
29
 
30
- async function run() {
31
- const creds = requireKey();
32
- const task = rest.join(' ');
33
- if (!task) { console.error('Usage: mona-agent run "<task>"'); process.exit(1); }
34
- const agent = new Agent(creds);
35
- process.stdout.write('\n');
36
- await agent.run(task, (t) => process.stdout.write(t));
37
- process.stdout.write('\n');
38
- agent.close();
 
 
 
 
 
 
 
 
 
 
39
  }
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  function status() {
42
  const c = loadCreds();
43
- if (!c) { console.log('Not logged in. Run: mona-agent login'); return; }
44
- console.log(`Logged in. agentId=${c.agentId || '(pending)'} cred=${CRED_PATH}`);
 
 
 
 
 
 
 
 
 
 
45
  }
46
 
 
47
  function help() {
48
- console.log(`mona-agent — cloud-brained device agent
 
 
 
 
49
 
50
- Usage:
51
- mona-agent login Save your agent.mona.expert API key
52
- mona-agent status Show login status
53
- mona-agent run "<task>" Run a task (reasoning happens in the cloud)
54
- mona-agent gui Start the local GUI (npm run gui)
55
 
56
- No LLM provider keys are stored locally. All reasoning runs on agent.mona.expert.`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  }
58
 
 
 
 
 
 
 
 
 
 
59
  switch (cmd) {
60
- case 'login': await login(); break;
61
- case 'status': status(); break;
62
- case 'run': await run(); break;
 
63
  case undefined:
64
- case 'help':
65
- case '-h':
66
- case '--help': help(); break;
67
- default: console.error(`Unknown command: ${cmd}`); help(); process.exit(1);
 
 
 
 
 
 
68
  }
 
1
  #!/usr/bin/env node
2
+ // ── mona-agent CLI ────────────────────────────────────────────────
3
+ // Commands:
4
+ // gui Terminal dashboard (default when TTY)
5
+ // start Headless daemon
6
+ // login Save API key
7
+ // status Show connection info
8
+ // help Show usage
9
+
10
  import { createInterface } from 'node:readline/promises';
11
  import { stdin, stdout } from 'node:process';
12
+ import { loadCreds, saveCreds, requireCreds, CLOUD, DEFAULTS, PATHS } from '../src/config.js';
13
  import { verifyKey } from '../src/cloud.js';
14
+ import { AgentDaemon } from '../src/agent.js';
15
+ import { Dashboard } from '../src/tui.js';
16
+ import { log } from '../src/log.js';
17
 
18
+ const [cmd] = process.argv.slice(2);
19
 
20
+ // ── login ─────────────────────────────────────────────────────────
21
  async function login() {
22
  const rl = createInterface({ input: stdin, output: stdout });
23
  const existing = loadCreds();
24
+
25
+ console.log(`\n ${BOLD}mona-agent login${RESET}\n`);
26
+
27
+ if (existing?.apiKey) {
28
+ console.log(` ${DIM}An API key is already saved. This will replace it.${RESET}\n`);
29
+ }
30
+
31
+ const apiKey = (await rl.question(' agent.mona.expert API key: ')).trim();
32
  rl.close();
33
+
34
+ if (!apiKey) {
35
+ console.error('\n No key entered.\n');
36
+ process.exit(1);
37
+ }
38
+
39
+ process.stdout.write(`\n Verifying with ${CLOUD.base}... `);
40
+
41
  try {
42
  const info = await verifyKey(apiKey);
43
  const path = saveCreds({ apiKey, agentId: info.agentId });
44
+
45
+ console.log(`${GREEN}OK${RESET}`);
46
+ console.log(`\n ${DIM}Agent:${RESET} ${info.agentId || '(pending)'}`);
47
+ console.log(` ${DIM}Saved:${RESET} ${path}`);
48
+ console.log(` ${DIM}Cloud:${RESET} ${CLOUD.base}`);
49
+ console.log(`\n Start the daemon:\n`);
50
+ console.log(` ${CYAN}mona-agent gui${RESET} ${DIM}# terminal dashboard${RESET}`);
51
+ console.log(` ${CYAN}mona-agent start${RESET} ${DIM}# headless daemon${RESET}`);
52
+ console.log();
53
  } catch (e) {
54
+ console.log(`${RED}FAILED${RESET}`);
55
+ console.error(`\n ${e.message}\n`);
56
  process.exit(1);
57
  }
58
  }
59
 
60
+ // ── gui (terminal dashboard) ──────────────────────────────────────
61
+ async function gui() {
62
+ const creds = requireCreds();
63
+ log.quiet(true); // Suppress console output; TUI handles display
64
+
65
+ const daemon = new AgentDaemon(creds);
66
+ const dashboard = new Dashboard(daemon);
67
+
68
+ const stop = () => {
69
+ dashboard.stop();
70
+ daemon.close();
71
+ process.exit(0);
72
+ };
73
+
74
+ process.on('SIGINT', stop);
75
+ process.on('SIGTERM', stop);
76
+
77
+ daemon.start();
78
+ dashboard.start();
79
  }
80
 
81
+ // ── start (headless) ──────────────────────────────────────────────
82
+ async function start() {
83
+ const creds = requireCreds();
84
+
85
+ console.log(`\n ${BOLD}${CYAN}mona-agent${RESET} ${DIM}v${DEFAULTS.version}${RESET}`);
86
+ console.log(` ${DIM}Headless daemon — controlled from ${CLOUD.base}${RESET}\n`);
87
+
88
+ const daemon = new AgentDaemon(creds);
89
+
90
+ const stop = () => {
91
+ console.log(`\n ${DIM}Shutting down...${RESET}\n`);
92
+ daemon.close();
93
+ process.exit(0);
94
+ };
95
+
96
+ process.on('SIGINT', stop);
97
+ process.on('SIGTERM', stop);
98
+
99
+ daemon.start();
100
+
101
+ // Keep process alive; all work is driven by inbound commands.
102
+ setInterval(() => {}, 1 << 30);
103
+ }
104
+
105
+ // ── status ────────────────────────────────────────────────────────
106
  function status() {
107
  const c = loadCreds();
108
+ console.log(`\n ${BOLD}mona-agent status${RESET}\n`);
109
+
110
+ if (!c?.apiKey) {
111
+ console.log(` ${RED}Not logged in.${RESET} Run: ${CYAN}mona-agent login${RESET}\n`);
112
+ return;
113
+ }
114
+
115
+ console.log(` ${DIM}Agent:${RESET} ${c.agentId || '(pending)'}`);
116
+ console.log(` ${DIM}Cloud:${RESET} ${CLOUD.base}`);
117
+ console.log(` ${DIM}Creds:${RESET} ${PATHS.creds}`);
118
+ console.log(` ${DIM}Config:${RESET} ${PATHS.dir}`);
119
+ console.log();
120
  }
121
 
122
+ // ── help ──────────────────────────────────────────────────────────
123
  function help() {
124
+ console.log(`
125
+ ${BOLD}mona-agent${RESET} ${DIM}v${DEFAULTS.version}${RESET}
126
+ Cloud-brained device agent. No local LLM keys.
127
+
128
+ ${BOLD}USAGE${RESET}
129
 
130
+ mona-agent ${CYAN}<command>${RESET}
 
 
 
 
131
 
132
+ ${BOLD}COMMANDS${RESET}
133
+
134
+ ${CYAN}gui${RESET} Terminal dashboard with live metrics ${DIM}(default)${RESET}
135
+ ${CYAN}start${RESET} Headless daemon (no UI)
136
+ ${CYAN}login${RESET} Save your agent.mona.expert API key
137
+ ${CYAN}status${RESET} Show login and connection info
138
+ ${CYAN}help${RESET} Show this help
139
+
140
+ ${BOLD}ENVIRONMENT${RESET}
141
+
142
+ MONA_CLOUD Cloud base URL ${DIM}(default: ${CLOUD.base})${RESET}
143
+ MONA_CLOUD_WS WebSocket URL ${DIM}(auto-derived from MONA_CLOUD)${RESET}
144
+ MONA_ALLOW_CMDS Shell allowlist ${DIM}(comma-separated command names)${RESET}
145
+ MONA_SHELL_UNSAFE Allow all shell ${DIM}(set to 1 — NOT recommended)${RESET}
146
+ MONA_WORKSPACE File tool sandbox ${DIM}(default: ~/.mona-agent/workspace)${RESET}
147
+
148
+ ${BOLD}QUICK START${RESET}
149
+
150
+ ${DIM}$${RESET} mona-agent login ${DIM}# paste your API key${RESET}
151
+ ${DIM}$${RESET} mona-agent gui ${DIM}# terminal dashboard${RESET}
152
+
153
+ All reasoning runs on ${BOLD}${CLOUD.base}${RESET}. No LLM keys stored locally.
154
+ `);
155
  }
156
 
157
+ // ── ANSI constants ────────────────────────────────────────────────
158
+ const BOLD = '\x1b[1m';
159
+ const DIM = '\x1b[2m';
160
+ const RESET = '\x1b[0m';
161
+ const CYAN = '\x1b[36m';
162
+ const GREEN = '\x1b[32m';
163
+ const RED = '\x1b[31m';
164
+
165
+ // ── Dispatch ──────────────────────────────────────────────────────
166
  switch (cmd) {
167
+ case 'login': await login(); break;
168
+ case 'gui': await gui(); break;
169
+ case 'start': await start(); break;
170
+ case 'status': status(); break;
171
  case undefined:
172
+ // Default: GUI if TTY, headless otherwise
173
+ if (process.stdout.isTTY) await gui();
174
+ else await start();
175
+ break;
176
+ case 'help': case '-h': case '--help':
177
+ help();
178
+ break;
179
+ default:
180
+ console.error(`\n Unknown command: ${cmd}\n Run ${CYAN}mona-agent help${RESET} for usage.\n`);
181
+ process.exit(1);
182
  }
install.sh CHANGED
@@ -1,39 +1,66 @@
1
  #!/usr/bin/env bash
2
- # Easy install for Mona Agent.
3
- # Usage: curl -fsSL https://agent.mona.expert/install.sh | bash
4
  set -euo pipefail
5
 
6
- REPO="${MONA_REPO:-https://github.com/mona-expert/mona-agent.git}"
7
  DIR="${MONA_DIR:-$HOME/.mona-agent-app}"
 
 
 
 
 
 
8
 
9
- echo "→ Mona Agent installer"
 
 
 
 
 
 
 
 
10
 
11
- command -v node >/dev/null 2>&1 || { echo "Node.js 18+ required. Install from https://nodejs.org"; exit 1; }
12
  NODE_MAJOR=$(node -p "process.versions.node.split('.')[0]")
13
- [ "$NODE_MAJOR" -ge 18 ] || { echo "Node 18+ required (found $(node -v))"; exit 1; }
 
 
 
14
 
 
 
 
15
  if [ -d "$DIR/.git" ]; then
16
- echo "→ Updating existing install in $DIR"
17
- git -C "$DIR" pull --ff-only
18
  else
19
- echo "→ Cloning into $DIR"
20
- git clone --depth 1 "$REPO" "$DIR"
21
  fi
22
 
23
  cd "$DIR"
24
- echo "→ Installing dependencies"
25
- npm install --omit=dev --silent
26
 
27
- # Link the CLI globally if possible.
28
- if npm link >/dev/null 2>&1; then
29
- echo "→ Linked 'mona-agent' command globally"
 
 
 
 
30
  else
31
- echo "→ Could not link globally; run via: node $DIR/bin/mona-agent.js"
32
  fi
33
 
34
- echo
35
- echo "✓ Installed."
36
- echo " 1) Log in: mona-agent login"
37
- echo " 2) Launch GUI: cd $DIR && npm run gui → http://localhost:4319"
38
- echo
39
- echo "No LLM keys are stored locally. All reasoning runs on agent.mona.expert."
 
 
 
 
 
 
 
1
  #!/usr/bin/env bash
2
+ # ── mona-agent installer ──────────────────────────────────────────
3
+ # Usage: curl -fsSL https://agent.mona.expert/install.sh | bash
4
  set -euo pipefail
5
 
6
+ REPO="${MONA_REPO:-https://github.com/MONAEXPERT/mona-agent.git}"
7
  DIR="${MONA_DIR:-$HOME/.mona-agent-app}"
8
+ BOLD='\033[1m'
9
+ DIM='\033[2m'
10
+ CYAN='\033[36m'
11
+ GREEN='\033[32m'
12
+ RED='\033[31m'
13
+ RESET='\033[0m'
14
 
15
+ echo ""
16
+ echo -e " ${BOLD}${CYAN}mona-agent${RESET} installer"
17
+ echo ""
18
+
19
+ # ── Check prerequisites ──────────────────────────────────────────
20
+ command -v node >/dev/null 2>&1 || {
21
+ echo -e " ${RED}✗${RESET} Node.js 20+ required. Install from https://nodejs.org"
22
+ exit 1
23
+ }
24
 
 
25
  NODE_MAJOR=$(node -p "process.versions.node.split('.')[0]")
26
+ if [ "$NODE_MAJOR" -lt 20 ]; then
27
+ echo -e " ${RED}✗${RESET} Node.js 20+ required (found $(node -v))"
28
+ exit 1
29
+ fi
30
 
31
+ echo -e " ${GREEN}✓${RESET} Node.js $(node -v)"
32
+
33
+ # ── Clone or update ──────────────────────────────────────────────
34
  if [ -d "$DIR/.git" ]; then
35
+ echo -e " ${DIM}→ Updating existing install in $DIR${RESET}"
36
+ git -C "$DIR" pull --ff-only --quiet
37
  else
38
+ echo -e " ${DIM}→ Cloning into $DIR${RESET}"
39
+ git clone --depth 1 --quiet "$REPO" "$DIR"
40
  fi
41
 
42
  cd "$DIR"
 
 
43
 
44
+ # ── Install dependencies ────────────────────────────────────────
45
+ echo -e " ${DIM}→ Installing dependencies${RESET}"
46
+ npm install --omit=dev --silent 2>/dev/null
47
+
48
+ # ── Link CLI globally ───────────────────────────────────────────
49
+ if npm link --silent 2>/dev/null; then
50
+ echo -e " ${GREEN}✓${RESET} Linked ${BOLD}mona-agent${RESET} command globally"
51
  else
52
+ echo -e " ${DIM}→ Could not link globally; run via: node $DIR/bin/mona-agent.js${RESET}"
53
  fi
54
 
55
+ echo ""
56
+ echo -e " ${GREEN}✓${RESET} Installed."
57
+ echo ""
58
+ echo -e " ${BOLD}Quick start:${RESET}"
59
+ echo ""
60
+ echo -e " ${CYAN}mona-agent login${RESET} ${DIM}# connect to your control plane${RESET}"
61
+ echo -e " ${CYAN}mona-agent gui${RESET} ${DIM}# terminal dashboard${RESET}"
62
+ echo -e " ${CYAN}mona-agent start${RESET} ${DIM}# headless daemon${RESET}"
63
+ echo ""
64
+ echo -e " Free & open source (MIT). No LLM keys on your device."
65
+ echo -e " SaaS control plane at ${BOLD}agent.mona.expert${RESET} (free tier available)."
66
+ echo ""
src/gui/index.html DELETED
@@ -1,146 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Mona Agent</title>
7
- <style>
8
- :root {
9
- --bg: #0b0d12; --panel: #141821; --line: #232a37;
10
- --text: #e6e9ef; --muted: #8b94a7; --accent: #6ea8fe; --ok: #46d39a; --warn: #ffcc66;
11
- }
12
- * { box-sizing: border-box; }
13
- body {
14
- margin: 0; font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
15
- background: var(--bg); color: var(--text); height: 100vh; display: grid;
16
- grid-template-rows: auto 1fr;
17
- }
18
- header {
19
- display: flex; align-items: center; gap: 12px; padding: 14px 20px;
20
- border-bottom: 1px solid var(--line); background: linear-gradient(180deg,#11151d,#0b0d12);
21
- }
22
- header .logo { width: 28px; height: 28px; border-radius: 8px;
23
- background: radial-gradient(circle at 30% 30%, #6ea8fe, #3a6ff0); }
24
- header h1 { font-size: 15px; margin: 0; letter-spacing: .3px; }
25
- header .status { margin-left: auto; font-size: 12px; color: var(--muted); }
26
- header .dot { display:inline-block; width:8px; height:8px; border-radius:50%; background:var(--warn); margin-right:6px;}
27
- header .dot.on { background: var(--ok); }
28
- main { display: grid; grid-template-columns: 1.3fr 1fr; gap: 16px; padding: 16px; overflow: hidden; }
29
- .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
30
- display: flex; flex-direction: column; overflow: hidden; }
31
- .panel h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .6px; color: var(--muted);
32
- margin: 0; padding: 12px 16px; border-bottom: 1px solid var(--line); }
33
- .panel .body { padding: 14px 16px; overflow: auto; flex: 1; }
34
- .left { display: grid; grid-template-rows: auto 1fr; gap: 16px; }
35
- .metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
36
- .metric { background:#0f131b; border:1px solid var(--line); border-radius:10px; padding:12px; }
37
- .metric .k { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing:.5px;}
38
- .metric .v { font-size: 20px; font-weight: 600; margin-top: 4px; }
39
- .metric .v small { font-size: 12px; color: var(--muted); font-weight: 400; }
40
- .steps .item { padding: 8px 0; border-bottom: 1px dashed var(--line); }
41
- .steps .item:last-child { border-bottom: none; }
42
- .steps .name { color: var(--accent); font-weight: 600; }
43
- .steps .t { color: var(--muted); font-size: 11px; float: right; }
44
- #output { white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
45
- font-size: 13px; }
46
- .runbar { display: flex; gap: 10px; padding: 14px 16px; border-top: 1px solid var(--line); }
47
- .runbar input { flex: 1; background:#0f131b; border:1px solid var(--line); color: var(--text);
48
- border-radius: 10px; padding: 10px 12px; font-size: 14px; outline: none; }
49
- .runbar input:focus { border-color: var(--accent); }
50
- .runbar button { background: var(--accent); color:#06101f; border:none; border-radius:10px;
51
- padding: 0 18px; font-weight: 600; cursor: pointer; }
52
- .runbar button:disabled { opacity:.5; cursor: default; }
53
- .cloud-note { font-size: 11px; color: var(--muted); padding: 0 16px 12px; }
54
- </style>
55
- </head>
56
- <body>
57
- <header>
58
- <div class="logo"></div>
59
- <h1>Mona Agent</h1>
60
- <div class="status"><span class="dot" id="dot"></span><span id="statusText">connecting…</span></div>
61
- </header>
62
- <main>
63
- <div class="left">
64
- <div class="panel">
65
- <h2>Device Monitoring</h2>
66
- <div class="body metrics">
67
- <div class="metric"><div class="k">CPU load (1m)</div><div class="v" id="m-cpu">—</div></div>
68
- <div class="metric"><div class="k">Memory used</div><div class="v" id="m-mem">—</div></div>
69
- <div class="metric"><div class="k">Uptime</div><div class="v" id="m-up">—</div></div>
70
- <div class="metric"><div class="k">Platform</div><div class="v" id="m-plat">—</div></div>
71
- <div class="metric"><div class="k">Cores</div><div class="v" id="m-cores">—</div></div>
72
- <div class="metric"><div class="k">Streaming</div><div class="v" id="m-stream" style="color:var(--ok)">live</div></div>
73
- </div>
74
- </div>
75
- <div class="panel">
76
- <h2>Agent Output</h2>
77
- <div class="body"><div id="output">Ready. Type a task below.</div></div>
78
- <div class="runbar">
79
- <input id="task" placeholder="Ask the agent to do something…" />
80
- <button id="run">Run</button>
81
- </div>
82
- <div class="cloud-note">🔒 Reasoning runs on agent.mona.expert · no LLM keys stored on this device</div>
83
- </div>
84
- </div>
85
- <div class="panel">
86
- <h2>Live Steps &amp; Telemetry → agent.mona.expert</h2>
87
- <div class="body steps" id="steps"></div>
88
- </div>
89
- </main>
90
- <script>
91
- const $ = s => document.querySelector(s);
92
- const fmtBytes = b => (b/1073741824).toFixed(1) + ' GB';
93
- const fmtUp = s => { const h=Math.floor(s/3600), m=Math.floor(s%3600/60); return h+'h '+m+'m'; };
94
-
95
- let ws;
96
- function connect() {
97
- ws = new WebSocket(`ws://${location.host}`);
98
- ws.onopen = () => { $('#dot').classList.add('on'); $('#statusText').textContent = 'connected'; };
99
- ws.onclose = () => { $('#dot').classList.remove('on'); $('#statusText').textContent = 'reconnecting…'; setTimeout(connect, 2000); };
100
- ws.onmessage = (ev) => {
101
- const msg = JSON.parse(ev.data);
102
- if (msg.type === 'device.metrics') {
103
- const d = msg.data;
104
- $('#m-cpu').innerHTML = d.cpuLoad[0].toFixed(2);
105
- const used = d.mem.total - d.mem.free;
106
- $('#m-mem').innerHTML = fmtBytes(used) + ' <small>/ ' + fmtBytes(d.mem.total) + '</small>';
107
- $('#m-up').textContent = fmtUp(d.uptime);
108
- $('#m-plat').textContent = d.platform + ' ' + d.arch;
109
- $('#m-cores').textContent = d.cpus;
110
- } else if (msg.type === 'agent.step') {
111
- addStep(msg.data.name, JSON.stringify(msg.data.detail || {}));
112
- } else if (msg.type === 'agent.log') {
113
- addStep(msg.data.level, msg.data.message);
114
- } else if (msg.type === 'token') {
115
- if ($('#output').dataset.fresh !== '1') { $('#output').textContent=''; $('#output').dataset.fresh='1'; }
116
- $('#output').textContent += msg.data;
117
- } else if (msg.type === 'done') {
118
- $('#run').disabled = false; $('#output').dataset.fresh='0';
119
- addStep('done', 'task complete');
120
- } else if (msg.type === 'error') {
121
- addStep('error', msg.data);
122
- }
123
- };
124
- }
125
- function addStep(name, detail) {
126
- const el = document.createElement('div');
127
- el.className = 'item';
128
- el.innerHTML = `<span class="t">${new Date().toLocaleTimeString()}</span><span class="name">${name}</span><div>${detail||''}</div>`;
129
- $('#steps').prepend(el);
130
- }
131
- function run() {
132
- const task = $('#task').value.trim();
133
- if (!task || !ws || ws.readyState !== 1) return;
134
- $('#run').disabled = true;
135
- $('#output').textContent = '';
136
- $('#output').dataset.fresh = '0';
137
- addStep('run', task);
138
- ws.send(JSON.stringify({ type: 'run', task }));
139
- $('#task').value = '';
140
- }
141
- $('#run').onclick = run;
142
- $('#task').addEventListener('keydown', e => { if (e.key === 'Enter') run(); });
143
- connect();
144
- </script>
145
- </body>
146
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/gui/server.js DELETED
@@ -1,61 +0,0 @@
1
- // Local GUI server. Serves the dashboard and bridges browser <-> agent + cloud.
2
- import http from 'node:http';
3
- import { readFileSync } from 'node:fs';
4
- import { fileURLToPath } from 'node:url';
5
- import { dirname, join } from 'node:path';
6
- import { WebSocketServer } from 'ws';
7
- import { loadCreds } from '../config.js';
8
- import { Agent } from '../agent.js';
9
-
10
- const __dirname = dirname(fileURLToPath(import.meta.url));
11
- const PORT = process.env.PORT || 4319;
12
-
13
- const html = readFileSync(join(__dirname, 'index.html'), 'utf8');
14
-
15
- const server = http.createServer((req, res) => {
16
- if (req.url === '/' || req.url === '/index.html') {
17
- res.writeHead(200, { 'content-type': 'text/html' });
18
- res.end(html);
19
- } else if (req.url === '/api/status') {
20
- const c = loadCreds();
21
- res.writeHead(200, { 'content-type': 'application/json' });
22
- res.end(JSON.stringify({ loggedIn: !!c?.apiKey, agentId: c?.agentId || null }));
23
- } else {
24
- res.writeHead(404); res.end('not found');
25
- }
26
- });
27
-
28
- const wss = new WebSocketServer({ server });
29
- wss.on('connection', (client) => {
30
- const creds = loadCreds();
31
- if (!creds?.apiKey) {
32
- client.send(JSON.stringify({ type: 'error', data: 'Not logged in. Run: mona-agent login' }));
33
- return;
34
- }
35
- const agent = new Agent(creds);
36
- // Mirror telemetry to the browser dashboard.
37
- const origSend = agent.telemetry.send.bind(agent.telemetry);
38
- agent.telemetry.send = (type, data) => {
39
- origSend(type, data);
40
- if (client.readyState === 1) client.send(JSON.stringify({ type, data, ts: Date.now() }));
41
- };
42
-
43
- client.on('message', async (raw) => {
44
- try {
45
- const msg = JSON.parse(raw.toString());
46
- if (msg.type === 'run' && msg.task) {
47
- await agent.run(msg.task, (delta) =>
48
- client.readyState === 1 && client.send(JSON.stringify({ type: 'token', data: delta })));
49
- client.readyState === 1 && client.send(JSON.stringify({ type: 'done' }));
50
- }
51
- } catch (e) {
52
- client.readyState === 1 && client.send(JSON.stringify({ type: 'error', data: e.message }));
53
- }
54
- });
55
-
56
- client.on('close', () => agent.close());
57
- });
58
-
59
- server.listen(PORT, () => {
60
- console.log(`\n Mona Agent GUI → http://localhost:${PORT}\n`);
61
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/telemetry.js DELETED
@@ -1,67 +0,0 @@
1
- // Streams device monitoring + agent steps to agent.mona.expert over a WebSocket.
2
- import { WebSocket } from 'ws';
3
- import os from 'node:os';
4
- import { CLOUD } from './config.js';
5
-
6
- export class TelemetryStream {
7
- constructor(apiKey, agentId) {
8
- this.apiKey = apiKey;
9
- this.agentId = agentId;
10
- this.ws = null;
11
- this.queue = [];
12
- this.timer = null;
13
- }
14
-
15
- connect() {
16
- this.ws = new WebSocket(CLOUD.ws, {
17
- headers: { authorization: `Bearer ${this.apiKey}` },
18
- });
19
- this.ws.on('open', () => {
20
- this.send('hello', { agentId: this.agentId, host: os.hostname() });
21
- this.flush();
22
- this.startDeviceMonitor();
23
- });
24
- this.ws.on('close', () => {
25
- clearInterval(this.timer);
26
- setTimeout(() => this.connect(), 3000); // auto-reconnect
27
- });
28
- this.ws.on('error', () => {});
29
- return this;
30
- }
31
-
32
- send(type, data) {
33
- const msg = JSON.stringify({ type, ts: Date.now(), agentId: this.agentId, data });
34
- if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.send(msg);
35
- else this.queue.push(msg);
36
- }
37
-
38
- flush() {
39
- while (this.queue.length && this.ws?.readyState === WebSocket.OPEN) {
40
- this.ws.send(this.queue.shift());
41
- }
42
- }
43
-
44
- // Emit agent lifecycle steps.
45
- step(name, detail) { this.send('agent.step', { name, detail }); }
46
- log(level, message) { this.send('agent.log', { level, message }); }
47
-
48
- // Periodic device monitoring snapshot.
49
- startDeviceMonitor() {
50
- this.timer = setInterval(() => {
51
- const load = os.loadavg();
52
- this.send('device.metrics', {
53
- cpuLoad: load,
54
- mem: { total: os.totalmem(), free: os.freemem() },
55
- uptime: os.uptime(),
56
- platform: os.platform(),
57
- arch: os.arch(),
58
- cpus: os.cpus().length,
59
- });
60
- }, 5000);
61
- }
62
-
63
- close() {
64
- clearInterval(this.timer);
65
- this.ws?.close();
66
- }
67
- }