lvtn-mobile 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lvtn_mobile-1.0.0/.gitignore +1 -0
- lvtn_mobile-1.0.0/PKG-INFO +127 -0
- lvtn_mobile-1.0.0/README.md +99 -0
- lvtn_mobile-1.0.0/bin/lvtn-mobile.js +27 -0
- lvtn_mobile-1.0.0/package.json +38 -0
- lvtn_mobile-1.0.0/pyproject.toml +47 -0
- lvtn_mobile-1.0.0/src/lvtn_mobile/__init__.py +2 -0
- lvtn_mobile-1.0.0/src/lvtn_mobile/main.py +701 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
node_modules
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lvtn-mobile
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Leviathan Shel HaShem — Mobile Swarm Client for Termux/Android. Backend-heavy thin client with offline task scheduling, headless remote control, and full 708-agent swarm access from your phone.
|
|
5
|
+
Project-URL: Homepage, https://lvtn.metanoiaunlimited.com
|
|
6
|
+
Author-email: "HaChazal (Chaz Leland Hamm)" <hachazal418@metanoiaunlimited.com>
|
|
7
|
+
License: Proprietary - Metanoia Unlimited LLC
|
|
8
|
+
Keywords: agent,android,background,headless,mobile,orchestration,scheduler,swarm,termux
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: Android
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Communications :: Chat
|
|
20
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: aiohttp>=3.9
|
|
23
|
+
Provides-Extra: full
|
|
24
|
+
Requires-Dist: lvtn>=3.14; extra == 'full'
|
|
25
|
+
Provides-Extra: notify
|
|
26
|
+
Requires-Dist: plyer>=2.1; extra == 'notify'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# lvtn-mobile — Termux/Android Swarm Client
|
|
30
|
+
|
|
31
|
+
The full 708-agent Leviathan swarm, on your phone. Backend-heavy — all LLM compute runs server-side. Your phone is the remote control.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Termux one-time setup
|
|
35
|
+
pkg update && pkg upgrade
|
|
36
|
+
pkg install python termux-api termux-boot
|
|
37
|
+
pip install lvtn-mobile
|
|
38
|
+
lvtnm login
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
| Command | What it does |
|
|
44
|
+
|---------|-------------|
|
|
45
|
+
| `lvtnm login` | Authenticate with your Leviathan key |
|
|
46
|
+
| `lvtnm chat "prompt"` | Send a prompt to HaChazal |
|
|
47
|
+
| `lvtnm fleet "task"` | Deploy all 7 Angels at once |
|
|
48
|
+
| `lvtnm council "task"` | All Angels answer in parallel |
|
|
49
|
+
| `lvtnm swarm zadkiel "task"` | Deploy one Angel's swarm |
|
|
50
|
+
| `lvtnm schedule "task"` | Queue a task for offline execution |
|
|
51
|
+
| `lvtnm schedule-list` | List scheduled tasks |
|
|
52
|
+
| `lvtnm schedule-run` | Execute all queued tasks now |
|
|
53
|
+
| `lvtnm headless` | Start HTTP server for remote control |
|
|
54
|
+
| `lvtnm status` | Show swarm health + scheduled tasks |
|
|
55
|
+
| `lvtnm balance` | Check KTRS balance |
|
|
56
|
+
| `lvtnm angels` | List all 7 Angels |
|
|
57
|
+
| `lvtnm remember "text"` | Write to HiveMind memory |
|
|
58
|
+
| `lvtnm recall "query"` | Search HiveMind memory |
|
|
59
|
+
| `lvtnm notify on\|off` | Toggle Android notifications |
|
|
60
|
+
| `lvtnm wake on\|off` | Toggle wake lock (keep phone awake) |
|
|
61
|
+
| `lvtnm boot on\|off` | Toggle auto-start on device boot |
|
|
62
|
+
|
|
63
|
+
## Offline Mode
|
|
64
|
+
|
|
65
|
+
Tasks queued while offline execute automatically when connectivity returns. Uses a background watcher thread that checks every 30 seconds.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
lvtnm schedule "research Solana DeFi trends" --type fleet
|
|
69
|
+
# Queued. Will execute when online.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Headless Mode (Remote Control)
|
|
73
|
+
|
|
74
|
+
Start an HTTP server on your phone, control the swarm from any device on the same network:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
lvtnm headless --port 9877
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Then from another device:
|
|
81
|
+
```bash
|
|
82
|
+
curl http://192.168.1.100:9877/fleet -d '{"task":"audit the lead gen pipeline"}'
|
|
83
|
+
curl http://192.168.1.100:9877/status
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Auto-Start on Boot
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
lvtnm boot on
|
|
90
|
+
# Headless server starts automatically when your phone boots
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Notifications
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
lvtnm notify on
|
|
97
|
+
# Get Android notifications when scheduled tasks complete
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Architecture
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
Phone (Termux) Server (api.leviathansi.xyz)
|
|
104
|
+
┌─────────────────┐ ┌──────────────────────────┐
|
|
105
|
+
│ lvtn-mobile │ HTTPS │ Leviathan Backend │
|
|
106
|
+
│ │ ◄──────────► │ │
|
|
107
|
+
│ • chat │ │ • 708 LLM agents │
|
|
108
|
+
│ • fleet │ aiohttp │ • 2,124 binary claws │
|
|
109
|
+
│ • council │ │ • HiveMind memory │
|
|
110
|
+
│ • schedule │ │ • KTRS metering │
|
|
111
|
+
│ • headless │ │ • All compute │
|
|
112
|
+
│ │ │ │
|
|
113
|
+
│ Background │ │ Your phone does nothing │
|
|
114
|
+
│ watcher thread │ │ heavy — just HTTP calls │
|
|
115
|
+
└─────────────────┘ └──────────────────────────┘
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Dependencies
|
|
119
|
+
|
|
120
|
+
Only one: `aiohttp` for async HTTP. No `rich`, no `prompt_toolkit`, no heavy TUI libs. Designed for Termux's limited environment.
|
|
121
|
+
|
|
122
|
+
## Install from npm
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm install -g lvtn-mobile
|
|
126
|
+
lvtnm login
|
|
127
|
+
```
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# lvtn-mobile — Termux/Android Swarm Client
|
|
2
|
+
|
|
3
|
+
The full 708-agent Leviathan swarm, on your phone. Backend-heavy — all LLM compute runs server-side. Your phone is the remote control.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# Termux one-time setup
|
|
7
|
+
pkg update && pkg upgrade
|
|
8
|
+
pkg install python termux-api termux-boot
|
|
9
|
+
pip install lvtn-mobile
|
|
10
|
+
lvtnm login
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
| Command | What it does |
|
|
16
|
+
|---------|-------------|
|
|
17
|
+
| `lvtnm login` | Authenticate with your Leviathan key |
|
|
18
|
+
| `lvtnm chat "prompt"` | Send a prompt to HaChazal |
|
|
19
|
+
| `lvtnm fleet "task"` | Deploy all 7 Angels at once |
|
|
20
|
+
| `lvtnm council "task"` | All Angels answer in parallel |
|
|
21
|
+
| `lvtnm swarm zadkiel "task"` | Deploy one Angel's swarm |
|
|
22
|
+
| `lvtnm schedule "task"` | Queue a task for offline execution |
|
|
23
|
+
| `lvtnm schedule-list` | List scheduled tasks |
|
|
24
|
+
| `lvtnm schedule-run` | Execute all queued tasks now |
|
|
25
|
+
| `lvtnm headless` | Start HTTP server for remote control |
|
|
26
|
+
| `lvtnm status` | Show swarm health + scheduled tasks |
|
|
27
|
+
| `lvtnm balance` | Check KTRS balance |
|
|
28
|
+
| `lvtnm angels` | List all 7 Angels |
|
|
29
|
+
| `lvtnm remember "text"` | Write to HiveMind memory |
|
|
30
|
+
| `lvtnm recall "query"` | Search HiveMind memory |
|
|
31
|
+
| `lvtnm notify on\|off` | Toggle Android notifications |
|
|
32
|
+
| `lvtnm wake on\|off` | Toggle wake lock (keep phone awake) |
|
|
33
|
+
| `lvtnm boot on\|off` | Toggle auto-start on device boot |
|
|
34
|
+
|
|
35
|
+
## Offline Mode
|
|
36
|
+
|
|
37
|
+
Tasks queued while offline execute automatically when connectivity returns. Uses a background watcher thread that checks every 30 seconds.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
lvtnm schedule "research Solana DeFi trends" --type fleet
|
|
41
|
+
# Queued. Will execute when online.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Headless Mode (Remote Control)
|
|
45
|
+
|
|
46
|
+
Start an HTTP server on your phone, control the swarm from any device on the same network:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
lvtnm headless --port 9877
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then from another device:
|
|
53
|
+
```bash
|
|
54
|
+
curl http://192.168.1.100:9877/fleet -d '{"task":"audit the lead gen pipeline"}'
|
|
55
|
+
curl http://192.168.1.100:9877/status
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Auto-Start on Boot
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
lvtnm boot on
|
|
62
|
+
# Headless server starts automatically when your phone boots
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Notifications
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
lvtnm notify on
|
|
69
|
+
# Get Android notifications when scheduled tasks complete
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Architecture
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Phone (Termux) Server (api.leviathansi.xyz)
|
|
76
|
+
┌─────────────────┐ ┌──────────────────────────┐
|
|
77
|
+
│ lvtn-mobile │ HTTPS │ Leviathan Backend │
|
|
78
|
+
│ │ ◄──────────► │ │
|
|
79
|
+
│ • chat │ │ • 708 LLM agents │
|
|
80
|
+
│ • fleet │ aiohttp │ • 2,124 binary claws │
|
|
81
|
+
│ • council │ │ • HiveMind memory │
|
|
82
|
+
│ • schedule │ │ • KTRS metering │
|
|
83
|
+
│ • headless │ │ • All compute │
|
|
84
|
+
│ │ │ │
|
|
85
|
+
│ Background │ │ Your phone does nothing │
|
|
86
|
+
│ watcher thread │ │ heavy — just HTTP calls │
|
|
87
|
+
└─────────────────┘ └──────────────────────────┘
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Dependencies
|
|
91
|
+
|
|
92
|
+
Only one: `aiohttp` for async HTTP. No `rich`, no `prompt_toolkit`, no heavy TUI libs. Designed for Termux's limited environment.
|
|
93
|
+
|
|
94
|
+
## Install from npm
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npm install -g lvtn-mobile
|
|
98
|
+
lvtnm login
|
|
99
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const { spawn } = require("child_process");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const pkgRoot = path.resolve(__dirname, "..");
|
|
7
|
+
const mainPy = path.join(pkgRoot, "src", "lvtn_mobile", "main.py");
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const child = spawn("python3", [mainPy, ...args], {
|
|
11
|
+
cwd: pkgRoot,
|
|
12
|
+
stdio: "inherit",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
child.on("error", () => {
|
|
16
|
+
const child2 = spawn("python", [mainPy, ...args], {
|
|
17
|
+
cwd: pkgRoot,
|
|
18
|
+
stdio: "inherit",
|
|
19
|
+
});
|
|
20
|
+
child2.on("error", () => {
|
|
21
|
+
console.error("lvtn-mobile: Python not found. Install Python 3.9+ in Termux: pkg install python");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
|
24
|
+
child2.on("exit", (code) => process.exit(code || 0));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
child.on("exit", (code) => process.exit(code || 0));
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lvtn-mobile",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Leviathan Shel HaShem — Mobile Swarm Client for Termux/Android. Backend-heavy thin client with offline task scheduling, headless remote control, and full 708-agent swarm access from your phone.",
|
|
5
|
+
"license": "Proprietary",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/chazam41892-gif/leviathan-of-hashem.git"
|
|
9
|
+
},
|
|
10
|
+
"author": "HaChazal (Chaz Leland Hamm) <hachazal418@metanoiaunlimited.com>",
|
|
11
|
+
"bin": {
|
|
12
|
+
"lvtn-mobile": "bin/lvtn-mobile.js",
|
|
13
|
+
"lvtnm": "bin/lvtn-mobile.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"src/",
|
|
18
|
+
"pyproject.toml",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=14.0.0"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"agent",
|
|
26
|
+
"swarm",
|
|
27
|
+
"orchestration",
|
|
28
|
+
"mobile",
|
|
29
|
+
"termux",
|
|
30
|
+
"android",
|
|
31
|
+
"headless",
|
|
32
|
+
"background",
|
|
33
|
+
"scheduler",
|
|
34
|
+
"kabbalah",
|
|
35
|
+
"metanoia",
|
|
36
|
+
"leviathan"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lvtn-mobile"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Leviathan Shel HaShem — Mobile Swarm Client for Termux/Android. Backend-heavy thin client with offline task scheduling, headless remote control, and full 708-agent swarm access from your phone."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Proprietary - Metanoia Unlimited LLC" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "HaChazal (Chaz Leland Hamm)", email = "hachazal418@metanoiaunlimited.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["agent", "swarm", "orchestration", "mobile", "termux", "android", "headless", "background", "scheduler"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: Android",
|
|
21
|
+
"Operating System :: POSIX :: Linux",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: System :: Distributed Computing",
|
|
28
|
+
"Topic :: Communications :: Chat",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
dependencies = [
|
|
32
|
+
"aiohttp>=3.9",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
notify = ["plyer>=2.1"] # Android notifications for task completion
|
|
37
|
+
full = ["lvtn>=3.14"] # Full desktop lvtn as fallback
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://lvtn.metanoiaunlimited.com"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
lvtn-mobile = "lvtn_mobile.main:main"
|
|
44
|
+
lvtnm = "lvtn_mobile.main:main"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["src/lvtn_mobile"]
|
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""lvtn-mobile — Termux/Android Swarm Client
|
|
3
|
+
|
|
4
|
+
Backend-heavy thin client for the 708-agent Leviathan swarm.
|
|
5
|
+
All LLM compute runs server-side. Your phone is just the remote control.
|
|
6
|
+
|
|
7
|
+
Commands:
|
|
8
|
+
lvtnm login Authenticate with your Leviathan key
|
|
9
|
+
lvtnm chat "prompt" Send a prompt to HaChazal
|
|
10
|
+
lvtnm fleet "task" Deploy all 7 Angels at once
|
|
11
|
+
lvtnm council "task" All Angels answer in parallel
|
|
12
|
+
lvtnm swarm <angel> "t" Deploy one Angel's swarm
|
|
13
|
+
lvtnm schedule "task" Queue a task for offline execution
|
|
14
|
+
lvtnm headless Start headless HTTP server (remote control)
|
|
15
|
+
lvtnm status Show swarm health + scheduled tasks
|
|
16
|
+
lvtnm balance Check KTRS balance
|
|
17
|
+
lvtnm angels List all 7 Angels
|
|
18
|
+
lvtnm keys Manage API keys
|
|
19
|
+
lvtnm remember "text" Write to HiveMind memory
|
|
20
|
+
lvtnm recall "query" Search HiveMind memory
|
|
21
|
+
lvtnm notify on|off Toggle Android notifications
|
|
22
|
+
lvtnm wake on|off Toggle wake lock (keep phone awake)
|
|
23
|
+
lvtnm boot on|off Toggle auto-start on device boot
|
|
24
|
+
|
|
25
|
+
Termux Setup (one-time):
|
|
26
|
+
pkg update && pkg upgrade
|
|
27
|
+
pkg install python termux-api termux-boot
|
|
28
|
+
pip install lvtn-mobile
|
|
29
|
+
lvtnm login
|
|
30
|
+
|
|
31
|
+
Offline Mode:
|
|
32
|
+
Tasks queued while offline execute automatically when
|
|
33
|
+
connectivity returns. Uses Termux:JobScheduler on Android 8+
|
|
34
|
+
or a background watcher thread.
|
|
35
|
+
|
|
36
|
+
Headless Mode:
|
|
37
|
+
lvtnm headless --port 9877
|
|
38
|
+
Then control from any device: curl http://<phone-ip>:9877/fleet -d '{"task":"..."}'
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import argparse
|
|
43
|
+
import asyncio
|
|
44
|
+
import json
|
|
45
|
+
import os
|
|
46
|
+
import signal
|
|
47
|
+
import sys
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
from typing import Optional
|
|
50
|
+
|
|
51
|
+
CONF_DIR = Path.home() / ".lvtn"
|
|
52
|
+
CONF_FILE = CONF_DIR / "credentials.json"
|
|
53
|
+
SCHEDULE_FILE = CONF_DIR / "mobile_schedule.jsonl"
|
|
54
|
+
CONFIG_FILE = CONF_DIR / "mobile_config.json"
|
|
55
|
+
|
|
56
|
+
DEFAULT_BASE = "https://api.leviathansi.xyz"
|
|
57
|
+
USER_AGENT = "lvtn-mobile/1.0"
|
|
58
|
+
|
|
59
|
+
# ── Config ──────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def _load_config() -> dict:
|
|
62
|
+
if CONFIG_FILE.exists():
|
|
63
|
+
try:
|
|
64
|
+
return json.loads(CONFIG_FILE.read_text())
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
return {"notify": True, "wake_lock": False, "boot_start": False}
|
|
68
|
+
|
|
69
|
+
def _save_config(cfg: dict):
|
|
70
|
+
CONF_DIR.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
CONFIG_FILE.write_text(json.dumps(cfg, indent=2))
|
|
72
|
+
|
|
73
|
+
def _load_key() -> Optional[str]:
|
|
74
|
+
if CONF_FILE.exists():
|
|
75
|
+
try:
|
|
76
|
+
return json.loads(CONF_FILE.read_text()).get("api_key")
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
def _save_key(key: str):
|
|
82
|
+
CONF_DIR.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
CONF_FILE.write_text(json.dumps({"api_key": key, "base": DEFAULT_BASE}))
|
|
84
|
+
|
|
85
|
+
# ── HTTP Client (aiohttp, async) ───────────────────────────────────
|
|
86
|
+
|
|
87
|
+
async def _api(method: str, path: str, key: Optional[str] = None,
|
|
88
|
+
body: Optional[dict] = None, timeout: float = 900.0) -> tuple[int, dict]:
|
|
89
|
+
import aiohttp
|
|
90
|
+
url = f"{DEFAULT_BASE}{path}"
|
|
91
|
+
headers = {"Content-Type": "application/json", "User-Agent": USER_AGENT}
|
|
92
|
+
if key:
|
|
93
|
+
headers["X-Leviathan-Key"] = key
|
|
94
|
+
try:
|
|
95
|
+
async with aiohttp.ClientSession() as session:
|
|
96
|
+
async with session.request(method, url, json=body, headers=headers,
|
|
97
|
+
timeout=aiohttp.ClientTimeout(total=timeout)) as resp:
|
|
98
|
+
data = await resp.json()
|
|
99
|
+
return resp.status, data
|
|
100
|
+
except aiohttp.ClientError as e:
|
|
101
|
+
return 0, {"detail": str(e)}
|
|
102
|
+
except asyncio.TimeoutError:
|
|
103
|
+
return 0, {"detail": "Request timed out"}
|
|
104
|
+
|
|
105
|
+
# ── Commands ─────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
async def cmd_login(args):
|
|
108
|
+
key = input("Leviathan API key: ").strip()
|
|
109
|
+
if not key:
|
|
110
|
+
print("No key provided.")
|
|
111
|
+
return 1
|
|
112
|
+
status, data = await _api("GET", "/api/llm/me", key=key)
|
|
113
|
+
if status == 200:
|
|
114
|
+
_save_key(key)
|
|
115
|
+
print(f" Logged in as {data.get('email', 'unknown')}")
|
|
116
|
+
print(f" Balance: ${data.get('balance_usd', 0)} KTRS")
|
|
117
|
+
return 0
|
|
118
|
+
print(f" Login failed (HTTP {status}): {data.get('detail', 'unknown')}")
|
|
119
|
+
return 1
|
|
120
|
+
|
|
121
|
+
async def cmd_balance(args):
|
|
122
|
+
key = _load_key()
|
|
123
|
+
if not key:
|
|
124
|
+
print("Not logged in. Run: lvtnm login")
|
|
125
|
+
return 1
|
|
126
|
+
status, data = await _api("GET", "/api/llm/me", key=key)
|
|
127
|
+
if status == 200:
|
|
128
|
+
print(f" ${data.get('balance_usd', 0)} KTRS")
|
|
129
|
+
return 0
|
|
130
|
+
print(f" Failed: {data.get('detail')}")
|
|
131
|
+
return 1
|
|
132
|
+
|
|
133
|
+
async def cmd_angels(args):
|
|
134
|
+
status, data = await _api("GET", "/api/llm/angels")
|
|
135
|
+
if status != 200:
|
|
136
|
+
print(f" Failed: {data.get('detail')}")
|
|
137
|
+
return 1
|
|
138
|
+
for a in data.get("angels", []):
|
|
139
|
+
print(f" {a['name']:<16} {a['sefirah']:<10} {a['title']}")
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
async def cmd_chat(args):
|
|
143
|
+
key = _load_key()
|
|
144
|
+
if not key:
|
|
145
|
+
print("Not logged in. Run: lvtnm login")
|
|
146
|
+
return 1
|
|
147
|
+
prompt = " ".join(args.prompt)
|
|
148
|
+
print(f" HaChazal is thinking...")
|
|
149
|
+
status, data = await _api("POST", "/api/llm/chat", key=key,
|
|
150
|
+
body={"messages": [{"role": "user", "content": prompt}]})
|
|
151
|
+
if status == 200:
|
|
152
|
+
print(f"\n{data.get('text', '')}\n")
|
|
153
|
+
print(f" [${data.get('charged_usd', 0)} charged · ${data.get('balance_usd', 0)} KTRS]")
|
|
154
|
+
return 0
|
|
155
|
+
print(f" Failed (HTTP {status}): {data.get('detail')}")
|
|
156
|
+
return 1
|
|
157
|
+
|
|
158
|
+
async def cmd_fleet(args):
|
|
159
|
+
key = _load_key()
|
|
160
|
+
if not key:
|
|
161
|
+
print("Not logged in.")
|
|
162
|
+
return 1
|
|
163
|
+
prompt = " ".join(args.prompt)
|
|
164
|
+
per_angel = getattr(args, 'per_angel', 2)
|
|
165
|
+
print(f" Fleet deploying — 7 Angels x {per_angel} agents...")
|
|
166
|
+
status, data = await _api("POST", "/api/llm/fleet", key=key,
|
|
167
|
+
body={"prompt": prompt, "per_angel": per_angel})
|
|
168
|
+
if status == 200:
|
|
169
|
+
for a in data.get("council", []):
|
|
170
|
+
print(f"\n --- {a.get('angel')} ({a.get('sefirah')}) ---")
|
|
171
|
+
for o in a.get("outputs", []):
|
|
172
|
+
print(f" {o.get('text', '').strip()[:500]}")
|
|
173
|
+
print(f"\n [${data.get('charged_usd', 0)} charged · ${data.get('balance_usd', 0)} KTRS]")
|
|
174
|
+
return 0
|
|
175
|
+
print(f" Failed (HTTP {status}): {data.get('detail')}")
|
|
176
|
+
return 1
|
|
177
|
+
|
|
178
|
+
async def cmd_council(args):
|
|
179
|
+
key = _load_key()
|
|
180
|
+
if not key:
|
|
181
|
+
print("Not logged in.")
|
|
182
|
+
return 1
|
|
183
|
+
prompt = " ".join(args.prompt)
|
|
184
|
+
per_angel = getattr(args, 'per_angel', 2)
|
|
185
|
+
print(f" Council convening — {per_angel} agents per Angel...")
|
|
186
|
+
status, data = await _api("POST", "/api/llm/council", key=key,
|
|
187
|
+
body={"prompt": prompt, "agents_per_angel": per_angel})
|
|
188
|
+
if status == 200:
|
|
189
|
+
for a in data.get("council", []):
|
|
190
|
+
print(f"\n --- {a.get('angel')} ({a.get('sefirah')}) ---")
|
|
191
|
+
for o in a.get("outputs", []):
|
|
192
|
+
print(f" {o.get('text', '').strip()[:500]}")
|
|
193
|
+
print(f"\n [${data.get('charged_usd', 0)} charged · ${data.get('balance_usd', 0)} KTRS]")
|
|
194
|
+
return 0
|
|
195
|
+
print(f" Failed (HTTP {status}): {data.get('detail')}")
|
|
196
|
+
return 1
|
|
197
|
+
|
|
198
|
+
async def cmd_swarm(args):
|
|
199
|
+
key = _load_key()
|
|
200
|
+
if not key:
|
|
201
|
+
print("Not logged in.")
|
|
202
|
+
return 1
|
|
203
|
+
prompt = " ".join(args.prompt)
|
|
204
|
+
agents = getattr(args, 'agents', 2)
|
|
205
|
+
print(f" {args.angel}'s swarm — {agents} agents...")
|
|
206
|
+
status, data = await _api("POST", "/api/llm/swarm", key=key,
|
|
207
|
+
body={"angel": args.angel, "prompt": prompt, "agents": agents, "tools": True})
|
|
208
|
+
if status == 200:
|
|
209
|
+
for i, o in enumerate(data.get("outputs", []), 1):
|
|
210
|
+
print(f"\n --- agent {i} ---")
|
|
211
|
+
print(f" {o.get('text', '').strip()[:500]}")
|
|
212
|
+
print(f"\n [${data.get('charged_usd', 0)} charged · ${data.get('balance_usd', 0)} KTRS]")
|
|
213
|
+
return 0
|
|
214
|
+
print(f" Failed (HTTP {status}): {data.get('detail')}")
|
|
215
|
+
return 1
|
|
216
|
+
|
|
217
|
+
async def cmd_remember(args):
|
|
218
|
+
key = _load_key()
|
|
219
|
+
if not key:
|
|
220
|
+
print("Not logged in.")
|
|
221
|
+
return 1
|
|
222
|
+
body = " ".join(args.body)
|
|
223
|
+
status, data = await _api("POST", "/api/llm/memory/remember", key=key,
|
|
224
|
+
body={"body": body, "agent": "mobile", "kind": "note"})
|
|
225
|
+
if status == 200:
|
|
226
|
+
print(f" Remembered.")
|
|
227
|
+
return 0
|
|
228
|
+
print(f" Failed: {data.get('detail')}")
|
|
229
|
+
return 1
|
|
230
|
+
|
|
231
|
+
async def cmd_recall(args):
|
|
232
|
+
key = _load_key()
|
|
233
|
+
if not key:
|
|
234
|
+
print("Not logged in.")
|
|
235
|
+
return 1
|
|
236
|
+
query = " ".join(args.query)
|
|
237
|
+
from urllib.parse import quote
|
|
238
|
+
status, data = await _api("GET", f"/api/llm/memory/recall?query={quote(query)}&agent=mobile", key=key)
|
|
239
|
+
if status == 200:
|
|
240
|
+
for m in data.get("memories", [])[:5]:
|
|
241
|
+
print(f" [{m.get('ts', '?')}] {m.get('body', '')[:200]}")
|
|
242
|
+
return 0
|
|
243
|
+
print(f" Failed: {data.get('detail')}")
|
|
244
|
+
return 1
|
|
245
|
+
|
|
246
|
+
# ── Offline Scheduler ────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
async def cmd_schedule(args):
|
|
249
|
+
"""Queue a task for execution. Runs immediately if online, queues if offline."""
|
|
250
|
+
task = " ".join(args.task)
|
|
251
|
+
cmd_type = getattr(args, 'type', 'chat')
|
|
252
|
+
entry = {
|
|
253
|
+
"ts": __import__("time").strftime("%Y-%m-%d %H:%M:%S"),
|
|
254
|
+
"type": cmd_type,
|
|
255
|
+
"task": task,
|
|
256
|
+
"status": "queued",
|
|
257
|
+
}
|
|
258
|
+
CONF_DIR.mkdir(parents=True, exist_ok=True)
|
|
259
|
+
with open(SCHEDULE_FILE, "a") as f:
|
|
260
|
+
f.write(json.dumps(entry) + "\n")
|
|
261
|
+
print(f" Queued: [{cmd_type}] {task[:80]}")
|
|
262
|
+
|
|
263
|
+
# Try executing immediately
|
|
264
|
+
key = _load_key()
|
|
265
|
+
if key:
|
|
266
|
+
try:
|
|
267
|
+
if cmd_type == "chat":
|
|
268
|
+
status, data = await _api("POST", "/api/llm/chat", key=key,
|
|
269
|
+
body={"messages": [{"role": "user", "content": task}]})
|
|
270
|
+
elif cmd_type == "fleet":
|
|
271
|
+
status, data = await _api("POST", "/api/llm/fleet", key=key,
|
|
272
|
+
body={"prompt": task, "per_angel": 2})
|
|
273
|
+
else:
|
|
274
|
+
status, data = await _api("POST", "/api/llm/chat", key=key,
|
|
275
|
+
body={"messages": [{"role": "user", "content": task}]})
|
|
276
|
+
if status == 200:
|
|
277
|
+
entry["status"] = "done"
|
|
278
|
+
entry["result"] = data.get("text", "")[:500]
|
|
279
|
+
print(f" Executed immediately.")
|
|
280
|
+
_notify(f"Task complete: {task[:50]}...")
|
|
281
|
+
else:
|
|
282
|
+
entry["status"] = "failed"
|
|
283
|
+
entry["error"] = data.get("detail", f"HTTP {status}")
|
|
284
|
+
print(f" Queued for retry (offline).")
|
|
285
|
+
except Exception as e:
|
|
286
|
+
entry["status"] = "queued"
|
|
287
|
+
print(f" Queued for retry ({e}).")
|
|
288
|
+
return 0
|
|
289
|
+
|
|
290
|
+
async def cmd_schedule_list(args):
|
|
291
|
+
if not SCHEDULE_FILE.exists():
|
|
292
|
+
print(" No scheduled tasks.")
|
|
293
|
+
return 0
|
|
294
|
+
tasks = []
|
|
295
|
+
with open(SCHEDULE_FILE) as f:
|
|
296
|
+
for line in f:
|
|
297
|
+
if line.strip():
|
|
298
|
+
tasks.append(json.loads(line))
|
|
299
|
+
for t in tasks[-20:]:
|
|
300
|
+
status = t.get("status", "?")
|
|
301
|
+
icon = {"done": "+", "failed": "!", "queued": "~"}.get(status, "?")
|
|
302
|
+
print(f" [{icon}] {t.get('ts')} [{t.get('type')}] {t.get('task', '')[:60]}")
|
|
303
|
+
return 0
|
|
304
|
+
|
|
305
|
+
async def cmd_schedule_run(args):
|
|
306
|
+
"""Execute all queued tasks."""
|
|
307
|
+
if not SCHEDULE_FILE.exists():
|
|
308
|
+
print(" No queued tasks.")
|
|
309
|
+
return 0
|
|
310
|
+
key = _load_key()
|
|
311
|
+
if not key:
|
|
312
|
+
print(" Not logged in — cannot execute queued tasks.")
|
|
313
|
+
return 1
|
|
314
|
+
tasks = []
|
|
315
|
+
with open(SCHEDULE_FILE) as f:
|
|
316
|
+
for line in f:
|
|
317
|
+
if line.strip():
|
|
318
|
+
tasks.append(json.loads(line))
|
|
319
|
+
executed = 0
|
|
320
|
+
for t in tasks:
|
|
321
|
+
if t.get("status") != "queued":
|
|
322
|
+
continue
|
|
323
|
+
print(f" Executing: {t.get('task', '')[:60]}...")
|
|
324
|
+
try:
|
|
325
|
+
status, data = await _api("POST", "/api/llm/chat", key=key,
|
|
326
|
+
body={"messages": [{"role": "user", "content": t["task"]}]})
|
|
327
|
+
if status == 200:
|
|
328
|
+
t["status"] = "done"
|
|
329
|
+
t["result"] = data.get("text", "")[:500]
|
|
330
|
+
executed += 1
|
|
331
|
+
else:
|
|
332
|
+
t["status"] = "failed"
|
|
333
|
+
t["error"] = data.get("detail", f"HTTP {status}")
|
|
334
|
+
except Exception as e:
|
|
335
|
+
t["status"] = "failed"
|
|
336
|
+
t["error"] = str(e)
|
|
337
|
+
# Rewrite file
|
|
338
|
+
with open(SCHEDULE_FILE, "w") as f:
|
|
339
|
+
for t in tasks:
|
|
340
|
+
f.write(json.dumps(t) + "\n")
|
|
341
|
+
print(f" Executed {executed} tasks.")
|
|
342
|
+
if executed > 0:
|
|
343
|
+
_notify(f"{executed} scheduled tasks completed")
|
|
344
|
+
return 0
|
|
345
|
+
|
|
346
|
+
# ── Headless HTTP Server ─────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
async def cmd_headless(args):
|
|
349
|
+
"""Start a lightweight HTTP server for remote control.
|
|
350
|
+
|
|
351
|
+
Other devices on the same network can control the swarm:
|
|
352
|
+
curl http://<phone-ip>:9877/chat -d '{"prompt":"hello"}'
|
|
353
|
+
curl http://<phone-ip>:9877/fleet -d '{"task":"research X"}'
|
|
354
|
+
curl http://<phone-ip>:9877/status
|
|
355
|
+
"""
|
|
356
|
+
from aiohttp import web
|
|
357
|
+
|
|
358
|
+
key = _load_key()
|
|
359
|
+
if not key:
|
|
360
|
+
print("Not logged in. Run: lvtnm login")
|
|
361
|
+
return 1
|
|
362
|
+
|
|
363
|
+
port = getattr(args, 'port', 9877)
|
|
364
|
+
|
|
365
|
+
async def handle_chat(request):
|
|
366
|
+
try:
|
|
367
|
+
body = await request.json()
|
|
368
|
+
prompt = body.get("prompt", "")
|
|
369
|
+
status, data = await _api("POST", "/api/llm/chat", key=key,
|
|
370
|
+
body={"messages": [{"role": "user", "content": prompt}]})
|
|
371
|
+
return web.json_response(data)
|
|
372
|
+
except Exception as e:
|
|
373
|
+
return web.json_response({"error": str(e)}, status=500)
|
|
374
|
+
|
|
375
|
+
async def handle_fleet(request):
|
|
376
|
+
try:
|
|
377
|
+
body = await request.json()
|
|
378
|
+
task = body.get("task", "")
|
|
379
|
+
per_angel = body.get("per_angel", 2)
|
|
380
|
+
status, data = await _api("POST", "/api/llm/fleet", key=key,
|
|
381
|
+
body={"prompt": task, "per_angel": per_angel})
|
|
382
|
+
return web.json_response(data)
|
|
383
|
+
except Exception as e:
|
|
384
|
+
return web.json_response({"error": str(e)}, status=500)
|
|
385
|
+
|
|
386
|
+
async def handle_council(request):
|
|
387
|
+
try:
|
|
388
|
+
body = await request.json()
|
|
389
|
+
task = body.get("task", "")
|
|
390
|
+
per_angel = body.get("per_angel", 2)
|
|
391
|
+
status, data = await _api("POST", "/api/llm/council", key=key,
|
|
392
|
+
body={"prompt": task, "agents_per_angel": per_angel})
|
|
393
|
+
return web.json_response(data)
|
|
394
|
+
except Exception as e:
|
|
395
|
+
return web.json_response({"error": str(e)}, status=500)
|
|
396
|
+
|
|
397
|
+
async def handle_status(request):
|
|
398
|
+
s, d = await _api("GET", "/api/llm/me", key=key)
|
|
399
|
+
scheduled = 0
|
|
400
|
+
if SCHEDULE_FILE.exists():
|
|
401
|
+
scheduled = sum(1 for _ in open(SCHEDULE_FILE))
|
|
402
|
+
return web.json_response({
|
|
403
|
+
"balance": d.get("balance_usd", 0),
|
|
404
|
+
"email": d.get("email", ""),
|
|
405
|
+
"scheduled_tasks": scheduled,
|
|
406
|
+
"mode": "headless",
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
async def handle_schedule(request):
|
|
410
|
+
try:
|
|
411
|
+
body = await request.json()
|
|
412
|
+
task = body.get("task", "")
|
|
413
|
+
cmd_type = body.get("type", "chat")
|
|
414
|
+
entry = {
|
|
415
|
+
"ts": __import__("time").strftime("%Y-%m-%d %H:%M:%S"),
|
|
416
|
+
"type": cmd_type,
|
|
417
|
+
"task": task,
|
|
418
|
+
"status": "queued",
|
|
419
|
+
}
|
|
420
|
+
CONF_DIR.mkdir(parents=True, exist_ok=True)
|
|
421
|
+
with open(SCHEDULE_FILE, "a") as f:
|
|
422
|
+
f.write(json.dumps(entry) + "\n")
|
|
423
|
+
return web.json_response({"ok": True, "queued": task[:80]})
|
|
424
|
+
except Exception as e:
|
|
425
|
+
return web.json_response({"error": str(e)}, status=500)
|
|
426
|
+
|
|
427
|
+
app = web.Application()
|
|
428
|
+
app.router.add_post("/chat", handle_chat)
|
|
429
|
+
app.router.add_post("/fleet", handle_fleet)
|
|
430
|
+
app.router.add_post("/council", handle_council)
|
|
431
|
+
app.router.add_post("/schedule", handle_schedule)
|
|
432
|
+
app.router.add_get("/status", handle_status)
|
|
433
|
+
|
|
434
|
+
# Get local IP
|
|
435
|
+
import socket
|
|
436
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
437
|
+
try:
|
|
438
|
+
s.connect(("8.8.8.8", 80))
|
|
439
|
+
local_ip = s.getsockname()[0]
|
|
440
|
+
except Exception:
|
|
441
|
+
local_ip = "127.0.0.1"
|
|
442
|
+
finally:
|
|
443
|
+
s.close()
|
|
444
|
+
|
|
445
|
+
print(f"\n Headless server on http://{local_ip}:{port}")
|
|
446
|
+
print(f" Endpoints:")
|
|
447
|
+
print(f" POST /chat {{\"prompt\": \"...\"}}")
|
|
448
|
+
print(f" POST /fleet {{\"task\": \"...\", \"per_angel\": 2}}")
|
|
449
|
+
print(f" POST /council {{\"task\": \"...\", \"per_angel\": 2}}")
|
|
450
|
+
print(f" POST /schedule {{\"task\": \"...\", \"type\": \"chat\"}}")
|
|
451
|
+
print(f" GET /status")
|
|
452
|
+
print(f"\n Press Ctrl+C to stop.")
|
|
453
|
+
|
|
454
|
+
runner = web.AppRunner(app)
|
|
455
|
+
await runner.setup()
|
|
456
|
+
site = web.TCPSite(runner, "0.0.0.0", port)
|
|
457
|
+
await site.start()
|
|
458
|
+
|
|
459
|
+
# Keep running
|
|
460
|
+
stop = asyncio.Event()
|
|
461
|
+
try:
|
|
462
|
+
await stop.wait()
|
|
463
|
+
except asyncio.CancelledError:
|
|
464
|
+
pass
|
|
465
|
+
|
|
466
|
+
# ── Notifications ────────────────────────────────────────────────────
|
|
467
|
+
|
|
468
|
+
def _notify(msg: str):
|
|
469
|
+
"""Send Android notification via Termux:API."""
|
|
470
|
+
cfg = _load_config()
|
|
471
|
+
if not cfg.get("notify", True):
|
|
472
|
+
return
|
|
473
|
+
try:
|
|
474
|
+
import subprocess
|
|
475
|
+
subprocess.run(["termux-notification", "--title", "lvtn-mobile",
|
|
476
|
+
"--content", msg, "--priority", "default"],
|
|
477
|
+
capture_output=True, timeout=5)
|
|
478
|
+
except Exception:
|
|
479
|
+
pass # termux-api not installed — silent fail
|
|
480
|
+
|
|
481
|
+
async def cmd_notify(args):
|
|
482
|
+
cfg = _load_config()
|
|
483
|
+
state = args.state.lower()
|
|
484
|
+
if state == "on":
|
|
485
|
+
cfg["notify"] = True
|
|
486
|
+
print(" Notifications: ON")
|
|
487
|
+
elif state == "off":
|
|
488
|
+
cfg["notify"] = False
|
|
489
|
+
print(" Notifications: OFF")
|
|
490
|
+
else:
|
|
491
|
+
print(f" Notifications: {'ON' if cfg.get('notify', True) else 'OFF'}")
|
|
492
|
+
_save_config(cfg)
|
|
493
|
+
return 0
|
|
494
|
+
|
|
495
|
+
async def cmd_wake(args):
|
|
496
|
+
"""Toggle wake lock via Termux:API."""
|
|
497
|
+
cfg = _load_config()
|
|
498
|
+
state = args.state.lower()
|
|
499
|
+
if state == "on":
|
|
500
|
+
cfg["wake_lock"] = True
|
|
501
|
+
try:
|
|
502
|
+
import subprocess
|
|
503
|
+
subprocess.run(["termux-wake-lock", "acquire"], capture_output=True, timeout=5)
|
|
504
|
+
except Exception:
|
|
505
|
+
pass
|
|
506
|
+
print(" Wake lock: ON (phone won't sleep)")
|
|
507
|
+
elif state == "off":
|
|
508
|
+
cfg["wake_lock"] = False
|
|
509
|
+
try:
|
|
510
|
+
import subprocess
|
|
511
|
+
subprocess.run(["termux-wake-lock", "release"], capture_output=True, timeout=5)
|
|
512
|
+
except Exception:
|
|
513
|
+
pass
|
|
514
|
+
print(" Wake lock: OFF")
|
|
515
|
+
else:
|
|
516
|
+
print(f" Wake lock: {'ON' if cfg.get('wake_lock', False) else 'OFF'}")
|
|
517
|
+
_save_config(cfg)
|
|
518
|
+
return 0
|
|
519
|
+
|
|
520
|
+
async def cmd_boot(args):
|
|
521
|
+
"""Toggle auto-start on device boot via Termux:Boot."""
|
|
522
|
+
cfg = _load_config()
|
|
523
|
+
state = args.state.lower()
|
|
524
|
+
boot_script = Path.home() / ".termux" / "boot" / "lvtn-mobile"
|
|
525
|
+
if state == "on":
|
|
526
|
+
cfg["boot_start"] = True
|
|
527
|
+
boot_script.parent.mkdir(parents=True, exist_ok=True)
|
|
528
|
+
boot_script.write_text("#!/data/data/com.termux/files/usr/bin/bash\nlvtnm headless --port 9877 &\n")
|
|
529
|
+
boot_script.chmod(0o755)
|
|
530
|
+
print(" Boot start: ON (headless server starts on device boot)")
|
|
531
|
+
elif state == "off":
|
|
532
|
+
cfg["boot_start"] = False
|
|
533
|
+
if boot_script.exists():
|
|
534
|
+
boot_script.unlink()
|
|
535
|
+
print(" Boot start: OFF")
|
|
536
|
+
else:
|
|
537
|
+
print(f" Boot start: {'ON' if cfg.get('boot_start', False) else 'OFF'}")
|
|
538
|
+
_save_config(cfg)
|
|
539
|
+
return 0
|
|
540
|
+
|
|
541
|
+
async def cmd_status(args):
|
|
542
|
+
"""Show swarm health + local state."""
|
|
543
|
+
key = _load_key()
|
|
544
|
+
print(f" lvtn-mobile v1.0.0")
|
|
545
|
+
if key:
|
|
546
|
+
status, data = await _api("GET", "/api/llm/me", key=key)
|
|
547
|
+
if status == 200:
|
|
548
|
+
print(f" Balance: ${data.get('balance_usd', 0)} KTRS")
|
|
549
|
+
print(f" Email: {data.get('email', 'unknown')}")
|
|
550
|
+
else:
|
|
551
|
+
print(f" API: unreachable")
|
|
552
|
+
else:
|
|
553
|
+
print(f" Status: not logged in")
|
|
554
|
+
|
|
555
|
+
cfg = _load_config()
|
|
556
|
+
print(f" Notify: {'ON' if cfg.get('notify', True) else 'OFF'}")
|
|
557
|
+
print(f" Wake: {'ON' if cfg.get('wake_lock', False) else 'OFF'}")
|
|
558
|
+
print(f" Boot: {'ON' if cfg.get('boot_start', False) else 'OFF'}")
|
|
559
|
+
|
|
560
|
+
if SCHEDULE_FILE.exists():
|
|
561
|
+
tasks = [json.loads(l) for l in open(SCHEDULE_FILE) if l.strip()]
|
|
562
|
+
queued = sum(1 for t in tasks if t.get("status") == "queued")
|
|
563
|
+
done = sum(1 for t in tasks if t.get("status") == "done")
|
|
564
|
+
failed = sum(1 for t in tasks if t.get("status") == "failed")
|
|
565
|
+
print(f" Tasks: {len(tasks)} total ({queued} queued, {done} done, {failed} failed)")
|
|
566
|
+
else:
|
|
567
|
+
print(f" Tasks: none scheduled")
|
|
568
|
+
return 0
|
|
569
|
+
|
|
570
|
+
# ── Background Watcher (offline task executor) ──────────────────────
|
|
571
|
+
|
|
572
|
+
async def _background_watcher():
|
|
573
|
+
"""Watch for connectivity and execute queued tasks."""
|
|
574
|
+
import time as _time
|
|
575
|
+
while True:
|
|
576
|
+
await asyncio.sleep(30)
|
|
577
|
+
if not SCHEDULE_FILE.exists():
|
|
578
|
+
continue
|
|
579
|
+
key = _load_key()
|
|
580
|
+
if not key:
|
|
581
|
+
continue
|
|
582
|
+
# Check connectivity
|
|
583
|
+
try:
|
|
584
|
+
status, _ = await _api("GET", "/api/llm/angels", timeout=10)
|
|
585
|
+
if status != 200:
|
|
586
|
+
continue
|
|
587
|
+
except Exception:
|
|
588
|
+
continue
|
|
589
|
+
# Execute queued tasks
|
|
590
|
+
tasks = []
|
|
591
|
+
with open(SCHEDULE_FILE) as f:
|
|
592
|
+
for line in f:
|
|
593
|
+
if line.strip():
|
|
594
|
+
tasks.append(json.loads(line))
|
|
595
|
+
changed = False
|
|
596
|
+
for t in tasks:
|
|
597
|
+
if t.get("status") != "queued":
|
|
598
|
+
continue
|
|
599
|
+
try:
|
|
600
|
+
status, data = await _api("POST", "/api/llm/chat", key=key,
|
|
601
|
+
body={"messages": [{"role": "user", "content": t["task"]}]})
|
|
602
|
+
if status == 200:
|
|
603
|
+
t["status"] = "done"
|
|
604
|
+
t["result"] = data.get("text", "")[:500]
|
|
605
|
+
changed = True
|
|
606
|
+
_notify(f"Task done: {t['task'][:50]}...")
|
|
607
|
+
else:
|
|
608
|
+
t["status"] = "failed"
|
|
609
|
+
t["error"] = data.get("detail", f"HTTP {status}")
|
|
610
|
+
changed = True
|
|
611
|
+
except Exception:
|
|
612
|
+
break # stop on first failure, retry next cycle
|
|
613
|
+
if changed:
|
|
614
|
+
with open(SCHEDULE_FILE, "w") as f:
|
|
615
|
+
for t in tasks:
|
|
616
|
+
f.write(json.dumps(t) + "\n")
|
|
617
|
+
|
|
618
|
+
# ── Main ─────────────────────────────────────────────────────────────
|
|
619
|
+
|
|
620
|
+
def main():
|
|
621
|
+
parser = argparse.ArgumentParser(description="lvtn-mobile — Termux/Android Swarm Client")
|
|
622
|
+
sub = parser.add_subparsers(dest="command")
|
|
623
|
+
|
|
624
|
+
sub.add_parser("login", help="Authenticate with Leviathan key")
|
|
625
|
+
sub.add_parser("balance", help="Check KTRS balance")
|
|
626
|
+
sub.add_parser("angels", help="List all 7 Angels")
|
|
627
|
+
sub.add_parser("status", help="Show swarm health + local state")
|
|
628
|
+
|
|
629
|
+
p = sub.add_parser("chat", help="Send prompt to HaChazal")
|
|
630
|
+
p.add_argument("prompt", nargs="+", help="Your prompt")
|
|
631
|
+
|
|
632
|
+
p = sub.add_parser("fleet", help="Deploy all 7 Angels")
|
|
633
|
+
p.add_argument("prompt", nargs="+", help="Task for the fleet")
|
|
634
|
+
p.add_argument("--per-angel", type=int, default=2, help="Agents per Angel")
|
|
635
|
+
|
|
636
|
+
p = sub.add_parser("council", help="All Angels answer in parallel")
|
|
637
|
+
p.add_argument("prompt", nargs="+", help="Task for the council")
|
|
638
|
+
p.add_argument("--per-angel", type=int, default=2, help="Agents per Angel")
|
|
639
|
+
|
|
640
|
+
p = sub.add_parser("swarm", help="Deploy one Angel's swarm")
|
|
641
|
+
p.add_argument("angel", help="Angel ID (zadkiel, gavriel, etc.)")
|
|
642
|
+
p.add_argument("prompt", nargs="+", help="Task for the swarm")
|
|
643
|
+
p.add_argument("--agents", type=int, default=2, help="Number of agents")
|
|
644
|
+
|
|
645
|
+
p = sub.add_parser("schedule", help="Queue task for offline execution")
|
|
646
|
+
p.add_argument("task", nargs="+", help="Task to queue")
|
|
647
|
+
p.add_argument("--type", default="chat", choices=["chat", "fleet", "council"])
|
|
648
|
+
|
|
649
|
+
sub.add_parser("schedule-list", help="List scheduled tasks")
|
|
650
|
+
sub.add_parser("schedule-run", help="Execute all queued tasks now")
|
|
651
|
+
|
|
652
|
+
p = sub.add_parser("headless", help="Start headless HTTP server")
|
|
653
|
+
p.add_argument("--port", type=int, default=9877, help="Port to listen on")
|
|
654
|
+
|
|
655
|
+
p = sub.add_parser("remember", help="Write to HiveMind memory")
|
|
656
|
+
p.add_argument("body", nargs="+", help="Memory content")
|
|
657
|
+
|
|
658
|
+
p = sub.add_parser("recall", help="Search HiveMind memory")
|
|
659
|
+
p.add_argument("query", nargs="+", help="Search query")
|
|
660
|
+
|
|
661
|
+
p = sub.add_parser("notify", help="Toggle Android notifications")
|
|
662
|
+
p.add_argument("state", nargs="?", default="", help="on|off")
|
|
663
|
+
|
|
664
|
+
p = sub.add_parser("wake", help="Toggle wake lock")
|
|
665
|
+
p.add_argument("state", nargs="?", default="", help="on|off")
|
|
666
|
+
|
|
667
|
+
p = sub.add_parser("boot", help="Toggle auto-start on boot")
|
|
668
|
+
p.add_argument("state", nargs="?", default="", help="on|off")
|
|
669
|
+
|
|
670
|
+
args = parser.parse_args()
|
|
671
|
+
if not args.command:
|
|
672
|
+
parser.print_help()
|
|
673
|
+
return 1
|
|
674
|
+
|
|
675
|
+
cmds = {
|
|
676
|
+
"login": cmd_login,
|
|
677
|
+
"balance": cmd_balance,
|
|
678
|
+
"angels": cmd_angels,
|
|
679
|
+
"status": cmd_status,
|
|
680
|
+
"chat": cmd_chat,
|
|
681
|
+
"fleet": cmd_fleet,
|
|
682
|
+
"council": cmd_council,
|
|
683
|
+
"swarm": cmd_swarm,
|
|
684
|
+
"schedule": cmd_schedule,
|
|
685
|
+
"schedule-list": cmd_schedule_list,
|
|
686
|
+
"schedule-run": cmd_schedule_run,
|
|
687
|
+
"headless": cmd_headless,
|
|
688
|
+
"remember": cmd_remember,
|
|
689
|
+
"recall": cmd_recall,
|
|
690
|
+
"notify": cmd_notify,
|
|
691
|
+
"wake": cmd_wake,
|
|
692
|
+
"boot": cmd_boot,
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
fn = cmds.get(args.command)
|
|
696
|
+
if fn:
|
|
697
|
+
return asyncio.run(fn(args))
|
|
698
|
+
return 1
|
|
699
|
+
|
|
700
|
+
if __name__ == "__main__":
|
|
701
|
+
sys.exit(main())
|