@hachazal/lvtn 3.10.0
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.
- package/LICENSE +20 -0
- package/README.md +57 -0
- package/bin/lvtn.mjs +21 -0
- package/package.json +45 -0
- package/src/api.mjs +114 -0
- package/src/cli.mjs +110 -0
- package/src/commands.mjs +578 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright © 2026 Chaz Leland Hamm (HaChazal) / Metanoia Unlimited LLC.
|
|
2
|
+
All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
This software ("lvtn", the Leviathan Shel HaShem command-line client) is
|
|
5
|
+
proprietary. It is a thin client that authenticates to and calls the
|
|
6
|
+
Leviathan-of-HaShem cloud backend; it performs no model compute locally.
|
|
7
|
+
|
|
8
|
+
Permission is granted to install and run this client solely to access the
|
|
9
|
+
Leviathan-of-HaShem service using a valid Leviathan-of-HaShem API key, subject
|
|
10
|
+
to the service's terms. No other rights are granted. You may not copy, modify,
|
|
11
|
+
redistribute, sublicense, reverse-engineer, or create derivative works of this
|
|
12
|
+
software without the prior written consent of Metanoia Unlimited LLC.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# lvtn — Leviathan Shel HaShem CLI
|
|
2
|
+
|
|
3
|
+
**A Kabbalistic 708-agent swarm in your terminal.** Zero local compute — you hold a key, the cloud backend runs the swarm, you're metered in KTRS.
|
|
4
|
+
|
|
5
|
+
Master orchestrator `hachazal` at Da'ath → 7 Angels (Zadkiel · Gavriel · Raphael · Uriel · Michael · Malika · Katan HaShem) × 100 agents each = 708.
|
|
6
|
+
|
|
7
|
+
## Install — one line, universal
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g lvtn
|
|
11
|
+
# or run with zero install:
|
|
12
|
+
npx lvtn
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Aliases install the same binary — `lvtn`, `leviathantalon`, and `hachazal` all do the same thing. (`npm i -g leviathantalon`, `npm i -g hachazal`, and `npm i -g lvtn-sl-hshm` are thin wrappers that pull in `lvtn`.)
|
|
16
|
+
|
|
17
|
+
Requires **Node 18+** (uses the built-in global `fetch`). No other dependencies.
|
|
18
|
+
|
|
19
|
+
> Prefer Python? `pip install lvtn` ships the same commands plus a 6-frame sixel art TUI. Both share your key at `~/.lvtn/credentials.json`.
|
|
20
|
+
|
|
21
|
+
## First run
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
lvtn signup # open the portal → join the email list → get your key
|
|
25
|
+
lvtn login # paste your lvtn-hshm-... key (stored at ~/.lvtn/credentials.json)
|
|
26
|
+
lvtn balance # KTRS balance
|
|
27
|
+
lvtn chat # interactive cloud chat
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
| Area | Command |
|
|
33
|
+
|---|---|
|
|
34
|
+
| Account | `signup`, `login [--key …]`, `balance` |
|
|
35
|
+
| Swarm | `chat`, `angels`, `swarm <angel> "task" [--agents N] [--tools]`, `council "task" [--angels a,b] [--per-angel N]`, `orchestrate "task"`, `fleet "task" [--per-angel N]` |
|
|
36
|
+
| Skills | `skill search <q>`, `skill run <id> "input"`, `skill stats`, `skill domains` |
|
|
37
|
+
| Integrations | `integrations list`, `integrations connect <svc> <token>`, `integrations action <svc> <action> --params '{…}'`, `integrations status`, `integrations disconnect <svc>` |
|
|
38
|
+
| Memory | `remember "…" [--tag a,b]`, `recall "query" [--all] [--limit N]` |
|
|
39
|
+
| Media | `video gen "prompt"`, `video extend --video-url …`, `video slingshot "prompt"`, `video jobs` |
|
|
40
|
+
| Local files | `fs read\|write\|ls\|find\|grep\|mv\|cp\|rm\|extract\|zip\|reveal\|ask` (`rm` archives, never true-deletes) |
|
|
41
|
+
|
|
42
|
+
`lvtn --version` · `lvtn help`.
|
|
43
|
+
|
|
44
|
+
## Environment overrides
|
|
45
|
+
|
|
46
|
+
| Var | Default |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `LVTN_API_BASE` | `https://api.leviathansi.xyz` |
|
|
49
|
+
| `LVTN_NODE_BASE` | `https://lvtn.metanoiaunlimited.com` (video extend/slingshot) |
|
|
50
|
+
| `LVTN_PORTAL_URL` | `https://portal.leviathansi.xyz` (signup) |
|
|
51
|
+
|
|
52
|
+
## Notes
|
|
53
|
+
|
|
54
|
+
- This is a **thin client** — every swarm/LLM/skill call runs server-side and is KTRS-metered. The only local work is the `fs` tools.
|
|
55
|
+
- The rich 6-frame sixel/braille art deck is in the Python build only; the npm `banner`/`deck` print a lightweight ANSI banner.
|
|
56
|
+
|
|
57
|
+
Copyright © 2026 Chaz Leland Hamm / Metanoia Unlimited LLC. All Rights Reserved. Proprietary — see `LICENSE`.
|
package/bin/lvtn.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lvtn — Leviathan of HaShem swarm CLI (npm). Universal one-line install:
|
|
3
|
+
// npm i -g lvtn · npx lvtn
|
|
4
|
+
// Aliases (same binary): leviathantalon · hachazal · lvtn-sl-hshm
|
|
5
|
+
import { run } from '../src/cli.mjs'
|
|
6
|
+
|
|
7
|
+
// Set exitCode and let the event loop drain — do NOT call process.exit().
|
|
8
|
+
// On Windows, process.exit() right after an undici fetch races libuv's socket
|
|
9
|
+
// teardown and trips `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)`.
|
|
10
|
+
// Letting the loop drain exits cleanly and promptly (undici unrefs idle sockets).
|
|
11
|
+
// The unref'd safety timer only force-exits if some handle genuinely hangs the
|
|
12
|
+
// loop past 30s; being unref'd it never delays a clean exit.
|
|
13
|
+
function finish(code) {
|
|
14
|
+
process.exitCode = typeof code === 'number' ? code : 0
|
|
15
|
+
const guard = setTimeout(() => process.exit(process.exitCode), 30_000)
|
|
16
|
+
if (typeof guard.unref === 'function') guard.unref()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
run(process.argv.slice(2))
|
|
20
|
+
.then(finish)
|
|
21
|
+
.catch((e) => { process.stderr.write(`\nlvtn error: ${e && e.stack ? e.stack : e}\n`); finish(1) })
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hachazal/lvtn",
|
|
3
|
+
"version": "3.10.0",
|
|
4
|
+
"description": "Leviathan Shel HaShem — a Kabbalistic 708-agent swarm in your terminal. Thin cloud client: login/balance/chat + angels/swarm/council/orchestrate/fleet, skills, integrations, video, HiveMind memory. KTRS-metered; the backend does all compute. Zero dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lvtn": "bin/lvtn.mjs",
|
|
8
|
+
"leviathantalon": "bin/lvtn.mjs",
|
|
9
|
+
"hachazal": "bin/lvtn.mjs",
|
|
10
|
+
"lvtn-sl-hshm": "bin/lvtn.mjs"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/cli.mjs"
|
|
14
|
+
},
|
|
15
|
+
"main": "./src/cli.mjs",
|
|
16
|
+
"files": [
|
|
17
|
+
"bin",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"lvtn": "node bin/lvtn.mjs"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"leviathan",
|
|
30
|
+
"leviathantalon",
|
|
31
|
+
"lvtn",
|
|
32
|
+
"swarm",
|
|
33
|
+
"agent",
|
|
34
|
+
"orchestration",
|
|
35
|
+
"ai",
|
|
36
|
+
"cli",
|
|
37
|
+
"kabbalah",
|
|
38
|
+
"metanoia",
|
|
39
|
+
"hachazal",
|
|
40
|
+
"thin-client"
|
|
41
|
+
],
|
|
42
|
+
"author": "HaChazal (Chaz Leland Hamm), Metanoia Unlimited LLC <hachazal418@metanoiaunlimited.com>",
|
|
43
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
44
|
+
"homepage": "https://leviathansi.xyz"
|
|
45
|
+
}
|
package/src/api.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// lvtn (npm) — thin cloud client for the Leviathan-of-HaShem backend.
|
|
2
|
+
//
|
|
3
|
+
// Port of the Python `lvtn` CLI's cloud.py. ALL compute (the 708-agent swarm,
|
|
4
|
+
// LLMs, metering) runs server-side — this client only stores the API key and
|
|
5
|
+
// does HTTP. Zero runtime dependencies (Node 18+ global fetch). Mirrors the
|
|
6
|
+
// exact endpoint contract + key-storage path so a key works across pip + npm.
|
|
7
|
+
//
|
|
8
|
+
// Backend base URL: $LVTN_API_BASE or https://api.leviathansi.xyz
|
|
9
|
+
// Node base URL: $LVTN_NODE_BASE or https://lvtn.metanoiaunlimited.com
|
|
10
|
+
// Key storage: ~/.lvtn/credentials.json (chmod 600, shared with pip CLI)
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs'
|
|
13
|
+
import os from 'node:os'
|
|
14
|
+
import path from 'node:path'
|
|
15
|
+
|
|
16
|
+
export const VERSION = '3.10.0'
|
|
17
|
+
|
|
18
|
+
const DEFAULT_BASE = 'https://api.leviathansi.xyz'
|
|
19
|
+
const DEFAULT_NODE_BASE = 'https://lvtn.metanoiaunlimited.com'
|
|
20
|
+
const CONF_DIR = path.join(os.homedir(), '.lvtn')
|
|
21
|
+
const CONF_FILE = path.join(CONF_DIR, 'credentials.json')
|
|
22
|
+
|
|
23
|
+
export const PORTAL_URL = process.env.LVTN_PORTAL_URL || 'https://portal.leviathansi.xyz'
|
|
24
|
+
|
|
25
|
+
// Cloudflare's Browser Integrity Check 403-bans bare/unknown User-Agents
|
|
26
|
+
// (error 1010). A named UA passes. WITHOUT THIS the CLI cannot reach the
|
|
27
|
+
// backend through Cloudflare. Verified 2026-05-29.
|
|
28
|
+
export const USER_AGENT = `lvtn/${VERSION} (+https://leviathansi.xyz)`
|
|
29
|
+
|
|
30
|
+
export function base() {
|
|
31
|
+
return (process.env.LVTN_API_BASE || DEFAULT_BASE).replace(/\/+$/, '')
|
|
32
|
+
}
|
|
33
|
+
export function nodeBase() {
|
|
34
|
+
return (process.env.LVTN_NODE_BASE || DEFAULT_NODE_BASE).replace(/\/+$/, '')
|
|
35
|
+
}
|
|
36
|
+
export { CONF_FILE }
|
|
37
|
+
|
|
38
|
+
export function loadKey() {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(fs.readFileSync(CONF_FILE, 'utf-8')).api_key || null
|
|
41
|
+
} catch {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function saveKey(key) {
|
|
47
|
+
fs.mkdirSync(CONF_DIR, { recursive: true })
|
|
48
|
+
fs.writeFileSync(CONF_FILE, JSON.stringify({ api_key: key, base: base() }), 'utf-8')
|
|
49
|
+
try { fs.chmodSync(CONF_FILE, 0o600) } catch { /* no-op on Windows */ }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Call a backend. Returns { status, data } — status 0 means unreachable.
|
|
54
|
+
* @param {string} root - resolved base URL (base() or nodeBase())
|
|
55
|
+
*/
|
|
56
|
+
async function request(root, method, apiPath, { key = null, body = undefined, timeout = 120_000 } = {}) {
|
|
57
|
+
const headers = { 'Content-Type': 'application/json', 'User-Agent': USER_AGENT }
|
|
58
|
+
if (key) headers['X-Leviathan-Key'] = key
|
|
59
|
+
const ctrl = new AbortController()
|
|
60
|
+
const timer = setTimeout(() => ctrl.abort(), timeout)
|
|
61
|
+
// Don't let the timeout handle hold the loop open or churn at exit. While a
|
|
62
|
+
// fetch is in flight the request itself keeps the loop alive, so an unref'd
|
|
63
|
+
// timer still fires on a genuinely hung request.
|
|
64
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetch(`${root}${apiPath}`, {
|
|
67
|
+
method,
|
|
68
|
+
headers,
|
|
69
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
70
|
+
signal: ctrl.signal,
|
|
71
|
+
})
|
|
72
|
+
let data = {}
|
|
73
|
+
const text = await res.text()
|
|
74
|
+
if (text) { try { data = JSON.parse(text) } catch { data = { detail: text } } }
|
|
75
|
+
return { status: res.status, data }
|
|
76
|
+
} catch (e) {
|
|
77
|
+
const aborted = e && e.name === 'AbortError'
|
|
78
|
+
return {
|
|
79
|
+
status: 0,
|
|
80
|
+
data: { detail: aborted ? `timed out after ${timeout}ms` : `cannot reach ${root} (${e && e.message})` },
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
clearTimeout(timer)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Call the credits-orchestrator backend (api.leviathansi.xyz). */
|
|
88
|
+
export function api(method, apiPath, opts = {}) {
|
|
89
|
+
return request(base(), method, apiPath, opts)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Call the NODE backend (lvtn.metanoiaunlimited.com) — video extend/slingshot. */
|
|
93
|
+
export function nodeApi(method, apiPath, opts = {}) {
|
|
94
|
+
return request(nodeBase(), method, apiPath, opts)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Require a stored key or print the login hint and return null. */
|
|
98
|
+
export function requireKey() {
|
|
99
|
+
const key = loadKey()
|
|
100
|
+
if (!key) process.stderr.write('Not logged in. Run: lvtn login\n')
|
|
101
|
+
return key
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function openUrl(url) {
|
|
105
|
+
// Best-effort cross-platform browser open, no deps.
|
|
106
|
+
import('node:child_process').then(({ spawn }) => {
|
|
107
|
+
try {
|
|
108
|
+
const plt = process.platform
|
|
109
|
+
const cmd = plt === 'win32' ? 'cmd' : plt === 'darwin' ? 'open' : 'xdg-open'
|
|
110
|
+
const args = plt === 'win32' ? ['/c', 'start', '', url] : [url]
|
|
111
|
+
spawn(cmd, args, { stdio: 'ignore', detached: true, shell: false }).unref()
|
|
112
|
+
} catch { /* ignore */ }
|
|
113
|
+
}).catch(() => {})
|
|
114
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// lvtn (npm) — argv parser + command dispatcher. Zero deps.
|
|
2
|
+
// Exported `run(argv)` is the single entry point used by every bin alias.
|
|
3
|
+
|
|
4
|
+
import { VERSION } from './api.mjs'
|
|
5
|
+
import * as cmd from './commands.mjs'
|
|
6
|
+
|
|
7
|
+
// Tiny flag parser: collects --flag value / --flag=value / --bool, positionals → _.
|
|
8
|
+
// Known value-flags consume the next token; everything else after `--` style is boolean.
|
|
9
|
+
const VALUE_FLAGS = new Set([
|
|
10
|
+
'agents', 'angel', 'angels', 'per-angel', 'max-tokens', 'tier', 'domain', 'limit',
|
|
11
|
+
'agent', 'kind', 'tag', 'params', 'model', 'resolution', 'ratio', 'seconds', 'negative',
|
|
12
|
+
'video-url', 'target-seconds', 'segments', 'mode', 'provider', 'duration', 'key',
|
|
13
|
+
])
|
|
14
|
+
const BOOL_FLAGS = new Set(['tools', 'all'])
|
|
15
|
+
|
|
16
|
+
function parse(tokens) {
|
|
17
|
+
const args = { _: [] }
|
|
18
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
19
|
+
const t = tokens[i]
|
|
20
|
+
if (t.startsWith('--')) {
|
|
21
|
+
let name = t.slice(2)
|
|
22
|
+
let val
|
|
23
|
+
const eq = name.indexOf('=')
|
|
24
|
+
if (eq >= 0) { val = name.slice(eq + 1); name = name.slice(0, eq) }
|
|
25
|
+
const camel = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
|
|
26
|
+
if (BOOL_FLAGS.has(name)) { args[camel] = true }
|
|
27
|
+
else if (VALUE_FLAGS.has(name)) { args[camel] = val !== undefined ? val : tokens[++i] }
|
|
28
|
+
else { args[camel] = val !== undefined ? val : true }
|
|
29
|
+
} else {
|
|
30
|
+
args._.push(t)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return args
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function num(v, d) { const n = parseInt(v, 10); return Number.isFinite(n) ? n : d }
|
|
37
|
+
|
|
38
|
+
function help() {
|
|
39
|
+
cmd.cmdBanner()
|
|
40
|
+
const lines = [
|
|
41
|
+
' Account',
|
|
42
|
+
' lvtn signup open the portal → join the list → get a key',
|
|
43
|
+
' lvtn login [--key lvtn-hshm-...] store + verify your key',
|
|
44
|
+
' lvtn balance show KTRS balance',
|
|
45
|
+
'',
|
|
46
|
+
' Talk to the swarm (cloud, KTRS-metered)',
|
|
47
|
+
' lvtn chat interactive REPL',
|
|
48
|
+
' lvtn angels list the 7 Angels',
|
|
49
|
+
' lvtn swarm <angel> "task" [--agents N] [--tools]',
|
|
50
|
+
' lvtn council "task" [--angels a,b] [--per-angel N]',
|
|
51
|
+
' lvtn orchestrate "task" [--max-tokens N] HaChazal plans→dispatches→synthesizes',
|
|
52
|
+
' lvtn fleet "task" [--per-angel N] all 7 Angels × tool-agents',
|
|
53
|
+
'',
|
|
54
|
+
' Skills · Integrations · Memory',
|
|
55
|
+
' lvtn skill search|run|stats|domains',
|
|
56
|
+
' lvtn integrations list|status|connect|action|disconnect',
|
|
57
|
+
' lvtn remember "..." [--tag a,b] · lvtn recall "query" [--all]',
|
|
58
|
+
'',
|
|
59
|
+
' Media · Local files',
|
|
60
|
+
' lvtn video gen|extend|slingshot|jobs',
|
|
61
|
+
' lvtn fs read|write|ls|find|grep|mv|cp|rm|extract|zip|reveal|ask',
|
|
62
|
+
'',
|
|
63
|
+
' lvtn --version · aliases: leviathantalon · hachazal · lvtn-sl-hshm',
|
|
64
|
+
' Env: LVTN_API_BASE · LVTN_NODE_BASE · LVTN_PORTAL_URL',
|
|
65
|
+
'',
|
|
66
|
+
]
|
|
67
|
+
lines.forEach((l) => process.stdout.write(l + '\n'))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
71
|
+
const sub = argv[0]
|
|
72
|
+
if (!sub || sub === '-h' || sub === '--help' || sub === 'help') { help(); return 0 }
|
|
73
|
+
if (sub === '-v' || sub === '--version' || sub === 'version') { process.stdout.write(`lvtn ${VERSION}\n`); return 0 }
|
|
74
|
+
|
|
75
|
+
const a = parse(argv.slice(1))
|
|
76
|
+
|
|
77
|
+
switch (sub) {
|
|
78
|
+
case 'banner': cmd.cmdBanner(); return 0
|
|
79
|
+
case 'deck': case 'art':
|
|
80
|
+
cmd.cmdBanner()
|
|
81
|
+
process.stdout.write(' (the full 6-frame sixel art deck ships with the Python build: pip install lvtn)\n\n')
|
|
82
|
+
return 0
|
|
83
|
+
case 'signup': return cmd.cmdSignup()
|
|
84
|
+
case 'login': return cmd.cmdLogin(a)
|
|
85
|
+
case 'balance': return cmd.cmdBalance()
|
|
86
|
+
case 'me': return cmd.cmdBalance()
|
|
87
|
+
case 'chat': return cmd.cmdChat()
|
|
88
|
+
case 'vibe': // `lvtn vibe code` or `lvtn vibe`
|
|
89
|
+
case 'code': return cmd.cmdChat()
|
|
90
|
+
case 'angels': return cmd.cmdAngels()
|
|
91
|
+
case 'swarm': { a.agents = num(a.agents, 3); return cmd.cmdSwarm(a) }
|
|
92
|
+
case 'council': { a.perAngel = num(a.perAngel, 2); return cmd.cmdCouncil(a) }
|
|
93
|
+
case 'orchestrate': { if (a.maxTokens) a.maxTokens = num(a.maxTokens); return cmd.cmdOrchestrate(a) }
|
|
94
|
+
case 'fleet': { a.perAngel = num(a.perAngel, 2); return cmd.cmdFleet(a) }
|
|
95
|
+
case 'remember': { a.agent = a.agent || 'hachazal'; a.kind = a.kind || 'observation'; return cmd.cmdRemember(a) }
|
|
96
|
+
case 'recall': { a.agent = a.agent || 'hachazal'; a.limit = num(a.limit, 10); return cmd.cmdRecall(a) }
|
|
97
|
+
case 'skill': case 'skills': { a.limit = num(a.limit, 20); return cmd.cmdSkill(a) }
|
|
98
|
+
case 'integrations': case 'integration': return cmd.cmdIntegrations(a)
|
|
99
|
+
case 'video': {
|
|
100
|
+
a.targetSeconds = num(a.targetSeconds, 30)
|
|
101
|
+
if (a.segments != null) a.segments = num(a.segments, undefined)
|
|
102
|
+
if (a.duration != null) a.duration = num(a.duration, undefined)
|
|
103
|
+
return cmd.cmdVideo(a)
|
|
104
|
+
}
|
|
105
|
+
case 'fs': return cmd.cmdFs(a)
|
|
106
|
+
default:
|
|
107
|
+
process.stderr.write(`Unknown command: ${sub}\nRun \`lvtn help\` for usage.\n`)
|
|
108
|
+
return 1
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/commands.mjs
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
// lvtn (npm) — command handlers. Each returns a process exit code (0 = ok).
|
|
2
|
+
// Port of the Python CLI's cloud.py + local_tools.py command surface.
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import os from 'node:os'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import readline from 'node:readline'
|
|
8
|
+
import { spawnSync } from 'node:child_process'
|
|
9
|
+
import {
|
|
10
|
+
api, nodeApi, loadKey, saveKey, requireKey, openUrl,
|
|
11
|
+
PORTAL_URL, CONF_FILE, base, VERSION,
|
|
12
|
+
} from './api.mjs'
|
|
13
|
+
|
|
14
|
+
const out = (s = '') => process.stdout.write(s + '\n')
|
|
15
|
+
const err = (s = '') => process.stderr.write(s + '\n')
|
|
16
|
+
|
|
17
|
+
function ask(question, { hidden = false } = {}) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
20
|
+
if (hidden) {
|
|
21
|
+
const stdout = process.stdout
|
|
22
|
+
rl._writeToOutput = function (str) { if (str.includes(question)) stdout.write(str); else stdout.write('') }
|
|
23
|
+
}
|
|
24
|
+
rl.question(question, (answer) => { rl.close(); resolve((answer || '').trim()) })
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function openSignup(reason = '') {
|
|
29
|
+
if (reason) out(reason)
|
|
30
|
+
out(` Opening ${PORTAL_URL} — sign up with your email to join the Leviathan`)
|
|
31
|
+
out(' email list and get your Leviathan-of-HaShem key.')
|
|
32
|
+
openUrl(PORTAL_URL)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── auth / account ───────────────────────────────────────────────────────────
|
|
36
|
+
export async function cmdSignup() {
|
|
37
|
+
openSignup()
|
|
38
|
+
out(' After you get your key, run: lvtn login')
|
|
39
|
+
return 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function cmdLogin(args) {
|
|
43
|
+
let key = args.key || null
|
|
44
|
+
if (!key) {
|
|
45
|
+
if (!loadKey()) openSignup(' No key found.')
|
|
46
|
+
key = await ask('Paste your Leviathan-of-HaShem key (lvtn-hshm-...): ', { hidden: true })
|
|
47
|
+
}
|
|
48
|
+
if (!key) { err('No key entered.'); return 1 }
|
|
49
|
+
const { status, data } = await api('GET', '/api/llm/me', { key, timeout: 30_000 })
|
|
50
|
+
if (status === 200) {
|
|
51
|
+
saveKey(key)
|
|
52
|
+
out(` Logged in as ${data.email}`)
|
|
53
|
+
out(` KTRS balance: $${data.balance_usd}`)
|
|
54
|
+
out(` Key stored at ${CONF_FILE}`)
|
|
55
|
+
return 0
|
|
56
|
+
}
|
|
57
|
+
if (status === 401 || status === 403) err(' Key rejected — check it and try again.')
|
|
58
|
+
else if (status === 0) err(` ${data.detail}`)
|
|
59
|
+
else err(` Login failed (HTTP ${status}): ${data.detail}`)
|
|
60
|
+
return 1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function cmdBalance() {
|
|
64
|
+
const key = loadKey()
|
|
65
|
+
if (!key) { err('Not logged in. Run: lvtn login'); return 1 }
|
|
66
|
+
const { status, data } = await api('GET', '/api/llm/me', { key, timeout: 30_000 })
|
|
67
|
+
if (status === 200) { out(` ${data.email} — $${data.balance_usd} KTRS`); return 0 }
|
|
68
|
+
if (status === 401 || status === 403) err(' Key rejected — run: lvtn login')
|
|
69
|
+
else err(` Could not fetch balance (HTTP ${status}): ${data.detail}`)
|
|
70
|
+
return 1
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── chat REPL ────────────────────────────────────────────────────────────────
|
|
74
|
+
export async function cmdChat() {
|
|
75
|
+
const key = loadKey()
|
|
76
|
+
if (!key) { err('Not logged in. Run: lvtn login'); return 1 }
|
|
77
|
+
out(' Leviathan of HaShem — cloud chat (backend swarm, KTRS-metered).')
|
|
78
|
+
out(" Type your message. Ctrl-C or 'exit' to quit.\n")
|
|
79
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
80
|
+
const history = []
|
|
81
|
+
return await new Promise((resolve) => {
|
|
82
|
+
const prompt = () => rl.question('you> ', async (line) => {
|
|
83
|
+
line = (line || '').trim()
|
|
84
|
+
if (!line) return prompt()
|
|
85
|
+
if (['exit', 'quit', ':q'].includes(line.toLowerCase())) { rl.close(); return resolve(0) }
|
|
86
|
+
history.push({ role: 'user', content: line })
|
|
87
|
+
const { status, data } = await api('POST', '/api/llm/chat', { key, body: { messages: history } })
|
|
88
|
+
if (status === 200) {
|
|
89
|
+
const reply = data.text || ''
|
|
90
|
+
history.push({ role: 'assistant', content: reply })
|
|
91
|
+
out(`\nleviathan> ${reply}\n`)
|
|
92
|
+
if (data.balance_usd != null) out(` [${data.provider} · $${data.balance_usd} KTRS left]\n`)
|
|
93
|
+
} else if (status === 402) {
|
|
94
|
+
err('\n Out of KTRS credits — your balance refills automatically. Try again later.\n')
|
|
95
|
+
history.pop()
|
|
96
|
+
} else if (status === 401 || status === 403) {
|
|
97
|
+
err('\n Key rejected — run: lvtn login\n'); rl.close(); return resolve(1)
|
|
98
|
+
} else {
|
|
99
|
+
err(`\n Error (HTTP ${status}): ${data.detail}\n`); history.pop()
|
|
100
|
+
}
|
|
101
|
+
prompt()
|
|
102
|
+
})
|
|
103
|
+
rl.on('SIGINT', () => { out(''); rl.close(); resolve(0) })
|
|
104
|
+
prompt()
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── angels / swarm / council / orchestrate / fleet ───────────────────────────
|
|
109
|
+
export async function cmdAngels() {
|
|
110
|
+
const { status, data } = await api('GET', '/api/llm/angels', { timeout: 30_000 })
|
|
111
|
+
if (status !== 200) { err(` Could not fetch roster (HTTP ${status}): ${data.detail}`); return 1 }
|
|
112
|
+
const angels = data.angels || []
|
|
113
|
+
const cap = angels[0]?.swarm_capacity ?? 100
|
|
114
|
+
out(`\n The Council — ${angels.length} Angels, each commands a ${cap}-agent swarm:\n`)
|
|
115
|
+
for (const a of angels) {
|
|
116
|
+
out(` ${String(a.id).padEnd(14)} ${String(a.name).padEnd(14)} ${String(a.sefirah).padEnd(11)} — ${a.title}`)
|
|
117
|
+
out(` ${''.padEnd(14)} ${a.blurb} [${a.swarm_tier}]\n`)
|
|
118
|
+
}
|
|
119
|
+
out(' Deploy one: lvtn swarm <id> "your task" --agents 5')
|
|
120
|
+
out(' Convene all: lvtn council "your task" --per-angel 2\n')
|
|
121
|
+
return 0
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function cmdSwarm(args) {
|
|
125
|
+
const key = requireKey(); if (!key) return 1
|
|
126
|
+
const prompt = (args._.join(' ')).trim()
|
|
127
|
+
if (!args.angel) { err('Usage: lvtn swarm <angel> "task" --agents N'); return 1 }
|
|
128
|
+
if (!prompt) { err('Give the Angel a task: lvtn swarm uriel "scan Solana for launches"'); return 1 }
|
|
129
|
+
out(` Deploying ${args.angel}'s swarm — ${args.agents} parallel cloud agents…`)
|
|
130
|
+
const { status, data } = await api('POST', '/api/llm/swarm', {
|
|
131
|
+
key, body: { angel: args.angel, prompt, agents: args.agents, tools: !!args.tools },
|
|
132
|
+
})
|
|
133
|
+
if (status === 402) { err(' Out of KTRS credits — balance refills automatically. Try later.'); return 1 }
|
|
134
|
+
if (status === 401 || status === 403) { err(' Key rejected — run: lvtn login'); return 1 }
|
|
135
|
+
if (status !== 200) { err(` Swarm failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
136
|
+
const outputs = data.outputs || data.agents || []
|
|
137
|
+
out(`\n ${args.angel} · ${outputs.length} agents:\n`)
|
|
138
|
+
outputs.forEach((o, i) => { out(` ── agent ${i + 1} ──`); out(` ${(o.text || o.content || '').trim()}\n`) })
|
|
139
|
+
out(` [charged $${data.charged_usd} · $${data.balance_usd} KTRS left]\n`)
|
|
140
|
+
return 0
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function cmdCouncil(args) {
|
|
144
|
+
const key = requireKey(); if (!key) return 1
|
|
145
|
+
const prompt = (args._.join(' ')).trim()
|
|
146
|
+
if (!prompt) { err('Give the Council a task: lvtn council "design a launch plan" --per-angel 2'); return 1 }
|
|
147
|
+
const body = { prompt, agents_per_angel: args.perAngel }
|
|
148
|
+
if (args.angels) body.angels = args.angels.split(',').map((s) => s.trim()).filter(Boolean)
|
|
149
|
+
out(' Convening the Council — Angels answering in parallel…')
|
|
150
|
+
const { status, data } = await api('POST', '/api/llm/council', { key, body, timeout: 240_000 })
|
|
151
|
+
if (status === 402) { err(' Out of KTRS credits — balance refills automatically. Try later.'); return 1 }
|
|
152
|
+
if (status === 401 || status === 403) { err(' Key rejected — run: lvtn login'); return 1 }
|
|
153
|
+
if (status !== 200) { err(` Council failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
154
|
+
const council = data.council || data.outputs || []
|
|
155
|
+
out(`\n Council — ${council.length} replies:\n`)
|
|
156
|
+
for (const c of council) {
|
|
157
|
+
out(` ◆ ${c.angel || c.id || '?'}`)
|
|
158
|
+
out(` ${(c.text || c.content || '').trim()}\n`)
|
|
159
|
+
}
|
|
160
|
+
out(` [charged $${data.charged_usd} · $${data.balance_usd} KTRS left]\n`)
|
|
161
|
+
return 0
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function cmdOrchestrate(args) {
|
|
165
|
+
const key = requireKey(); if (!key) return 1
|
|
166
|
+
const prompt = (args._.join(' ')).trim()
|
|
167
|
+
if (!prompt) { err('Give HaChazal a task to orchestrate: lvtn orchestrate "plan and launch a product"'); return 1 }
|
|
168
|
+
const body = { messages: [{ role: 'user', content: prompt }] }
|
|
169
|
+
if (args.maxTokens) body.max_tokens = args.maxTokens
|
|
170
|
+
out(' HaChazal is orchestrating — planning, dispatching the Angels, synthesizing…')
|
|
171
|
+
const { status, data } = await api('POST', '/api/llm/orchestrate', { key, body, timeout: 240_000 })
|
|
172
|
+
if (status === 402) { err(' Out of KTRS credits — balance refills automatically. Try later.'); return 1 }
|
|
173
|
+
if (status === 401 || status === 403) { err(' Key rejected — run: lvtn login'); return 1 }
|
|
174
|
+
if (status !== 200) { err(` Orchestrate failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
175
|
+
out(`\n${(data.text || '').trim()}\n`)
|
|
176
|
+
if (data.balance_usd != null) out(` [charged $${data.charged_usd ?? 0} · $${data.balance_usd} KTRS left]\n`)
|
|
177
|
+
return 0
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function cmdFleet(args) {
|
|
181
|
+
const key = requireKey(); if (!key) return 1
|
|
182
|
+
const prompt = (args._.join(' ')).trim()
|
|
183
|
+
if (!prompt) { err('Give the fleet a task: lvtn fleet "audit my repo" --per-angel 2'); return 1 }
|
|
184
|
+
out(' Deploying the full fleet — all 7 Angels × NanoClaw tool-agents…')
|
|
185
|
+
const { status, data } = await api('POST', '/api/llm/fleet', { key, body: { prompt, per_angel: args.perAngel }, timeout: 240_000 })
|
|
186
|
+
if (status === 402) { err(' Out of KTRS credits — balance refills automatically. Try later.'); return 1 }
|
|
187
|
+
if (status === 401 || status === 403) { err(' Key rejected — run: lvtn login'); return 1 }
|
|
188
|
+
if (status !== 200) { err(` Fleet failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
189
|
+
const council = data.council || data.outputs || []
|
|
190
|
+
out(`\n Fleet — ${council.length} Angel replies:\n`)
|
|
191
|
+
for (const c of council) { out(` ◆ ${c.angel || c.id || '?'}`); out(` ${(c.text || c.content || '').trim()}\n`) }
|
|
192
|
+
out(` [charged $${data.charged_usd} · $${data.balance_usd} KTRS left]\n`)
|
|
193
|
+
return 0
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── memory (HiveMind) ────────────────────────────────────────────────────────
|
|
197
|
+
export async function cmdRemember(args) {
|
|
198
|
+
const key = requireKey(); if (!key) return 1
|
|
199
|
+
const body = (args._.join(' ')).trim()
|
|
200
|
+
if (!body) { err('Give it something to remember: lvtn remember "we chose X because Y"'); return 1 }
|
|
201
|
+
const payload = { body, agent: args.agent, kind: args.kind }
|
|
202
|
+
if (args.tag) payload.tags = args.tag.split(',').map((t) => t.trim()).filter(Boolean)
|
|
203
|
+
const { status, data } = await api('POST', '/api/llm/memory/remember', { key, body: payload })
|
|
204
|
+
if (status === 401 || status === 403) { err(' Key rejected — run: lvtn login'); return 1 }
|
|
205
|
+
if (status !== 200) { err(` Remember failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
206
|
+
out(` remembered as ${data.agent} · ${data.kind} · ${data.ts}`)
|
|
207
|
+
return 0
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function cmdRecall(args) {
|
|
211
|
+
const key = requireKey(); if (!key) return 1
|
|
212
|
+
const q = (args._.join(' ')).trim()
|
|
213
|
+
const path_ = `/api/llm/memory/recall?query=${encodeURIComponent(q)}&agent=${encodeURIComponent(args.agent)}`
|
|
214
|
+
+ `&limit=${parseInt(args.limit, 10)}&all_agents=${args.all ? 'true' : 'false'}`
|
|
215
|
+
const { status, data } = await api('GET', path_, { key })
|
|
216
|
+
if (status === 401 || status === 403) { err(' Key rejected — run: lvtn login'); return 1 }
|
|
217
|
+
if (status !== 200) { err(` Recall failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
218
|
+
const mems = data.memories || []
|
|
219
|
+
if (!mems.length) { out(' (no memories match)'); return 0 }
|
|
220
|
+
out(`\n ${data.count} memory(ies):\n`)
|
|
221
|
+
for (const m of mems) {
|
|
222
|
+
out(` ◆ ${m.ts} · ${m.agent} · ${m.kind} tags=${JSON.stringify(m.tags)}`)
|
|
223
|
+
out(` ${(m.body || '').trim()}\n`)
|
|
224
|
+
}
|
|
225
|
+
return 0
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── skills ───────────────────────────────────────────────────────────────────
|
|
229
|
+
export async function cmdSkill(args) {
|
|
230
|
+
const sub = args._.shift()
|
|
231
|
+
if (sub === 'search') {
|
|
232
|
+
const query = args._.join(' ').trim()
|
|
233
|
+
const params = new URLSearchParams({ limit: String(args.limit || 20) })
|
|
234
|
+
if (query) params.set('q', query)
|
|
235
|
+
if (args.tier) params.set('tier', args.tier)
|
|
236
|
+
if (args.domain) params.set('domain', args.domain)
|
|
237
|
+
const { status, data } = await api('GET', `/api/llm/skills/search?${params}`, { timeout: 30_000 })
|
|
238
|
+
if (status !== 200) { err(` Search failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
239
|
+
const results = data.results || []
|
|
240
|
+
const total = data.total_in_catalog ?? data.total ?? '?'
|
|
241
|
+
out(`\n ${results.length} results (catalog: ${total} skills)\n`)
|
|
242
|
+
for (const s of results) {
|
|
243
|
+
const desc = (s.description || '').slice(0, 72)
|
|
244
|
+
out(` [${String(s.tier || '?').padEnd(8)}] ${String(s.id).padEnd(42)} ${s.domain || ''}`)
|
|
245
|
+
if (desc && desc !== s.id) out(` ${''.padStart(10)} ${desc}`)
|
|
246
|
+
}
|
|
247
|
+
if (results.length) out(`\n Run one: lvtn skill run ${results[0].id} "your input here"`)
|
|
248
|
+
out('')
|
|
249
|
+
return 0
|
|
250
|
+
}
|
|
251
|
+
if (sub === 'run') {
|
|
252
|
+
const key = requireKey(); if (!key) return 1
|
|
253
|
+
const skillId = (args._.shift() || '').trim()
|
|
254
|
+
let prompt = args._.join(' ').trim()
|
|
255
|
+
if (!skillId) { err('Usage: lvtn skill run <skill_id> "your input"'); return 1 }
|
|
256
|
+
if (!prompt) prompt = await ask(` Input for skill '${skillId}': `)
|
|
257
|
+
const body = { skill_id: skillId, input: prompt }
|
|
258
|
+
if (args.angel) body.angel = args.angel
|
|
259
|
+
out(` Running skill: ${skillId}…`)
|
|
260
|
+
const { status, data } = await api('POST', '/api/llm/skills/execute', { key, body, timeout: 120_000 })
|
|
261
|
+
if (status === 402) { err(' Out of KTRS — balance refills automatically.'); return 1 }
|
|
262
|
+
if (status === 404) { err(` Skill '${skillId}' not found. Try: lvtn skill search <topic>`); return 1 }
|
|
263
|
+
if (status === 401 || status === 403) { err(' Key rejected — run: lvtn login'); return 1 }
|
|
264
|
+
if (status !== 200) { err(` Execute failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
265
|
+
out(`\n [${data.skill_name || skillId}]\n`)
|
|
266
|
+
out(` ${(data.text || '').trim()}\n`)
|
|
267
|
+
if (data.balance_usd != null) out(` [${data.provider} · $${data.balance_usd} KTRS left]\n`)
|
|
268
|
+
return 0
|
|
269
|
+
}
|
|
270
|
+
if (sub === 'stats') {
|
|
271
|
+
const { status, data } = await api('GET', '/api/llm/skills/stats', { timeout: 30_000 })
|
|
272
|
+
if (status !== 200) { err(` Stats failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
273
|
+
out(`\n ${JSON.stringify(data, null, 2)}\n`); return 0
|
|
274
|
+
}
|
|
275
|
+
if (sub === 'domains') {
|
|
276
|
+
const { status, data } = await api('GET', '/api/llm/skills/domains', { timeout: 30_000 })
|
|
277
|
+
if (status !== 200) { err(` Domains failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
278
|
+
const domains = data.domains || data || []
|
|
279
|
+
out(`\n ${domains.length} domains:\n`)
|
|
280
|
+
out(' ' + domains.join('\n ') + '\n'); return 0
|
|
281
|
+
}
|
|
282
|
+
out(' lvtn skill search <query> search the catalog')
|
|
283
|
+
out(' lvtn skill run <id> "your input" execute a skill via LLM')
|
|
284
|
+
out(' lvtn skill stats show counts by tier')
|
|
285
|
+
out(' lvtn skill domains list all domains')
|
|
286
|
+
return sub ? 1 : 0
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── integrations ─────────────────────────────────────────────────────────────
|
|
290
|
+
export async function cmdIntegrations(args) {
|
|
291
|
+
const sub = args._.shift() || 'list'
|
|
292
|
+
if (sub === 'list') {
|
|
293
|
+
const { status, data } = await api('GET', '/api/integrations/services', { timeout: 30_000 })
|
|
294
|
+
if (status !== 200) { err(` Could not list services (HTTP ${status}): ${data.detail}`); return 1 }
|
|
295
|
+
const services = data.services || []
|
|
296
|
+
const filter = (args._.join(' ') || '').toLowerCase()
|
|
297
|
+
const shown = filter ? services.filter((s) => JSON.stringify(s).toLowerCase().includes(filter)) : services
|
|
298
|
+
out(`\n ${shown.length}${filter ? `/${services.length}` : ''} integrations:\n`)
|
|
299
|
+
for (const s of shown) {
|
|
300
|
+
out(` ${String(s.id || s.service).padEnd(22)} ${s.category || ''} ${s.token_format ? `token: ${s.token_format}` : ''}`)
|
|
301
|
+
}
|
|
302
|
+
out(`\n Connect: lvtn integrations connect <service> <token>`)
|
|
303
|
+
out(` Run: lvtn integrations action <service> <action> --params '{"k":"v"}'\n`)
|
|
304
|
+
return 0
|
|
305
|
+
}
|
|
306
|
+
if (sub === 'status') {
|
|
307
|
+
const key = requireKey(); if (!key) return 1
|
|
308
|
+
const { status, data } = await api('GET', '/api/integrations/status', { key, timeout: 30_000 })
|
|
309
|
+
if (status !== 200) { err(` Status failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
310
|
+
out(`\n ${JSON.stringify(data, null, 2)}\n`); return 0
|
|
311
|
+
}
|
|
312
|
+
if (sub === 'connect') {
|
|
313
|
+
const key = requireKey(); if (!key) return 1
|
|
314
|
+
const service = (args._.shift() || '').trim()
|
|
315
|
+
const token = (args._.shift() || '').trim()
|
|
316
|
+
if (!service || !token) {
|
|
317
|
+
err('Usage: lvtn integrations connect <service> <token>')
|
|
318
|
+
err(' Run `lvtn integrations list` to see token formats.'); return 1
|
|
319
|
+
}
|
|
320
|
+
out(` Testing credentials for ${service}…`)
|
|
321
|
+
const { status, data } = await api('POST', '/api/integrations/connect', { key, body: { service, token }, timeout: 60_000 })
|
|
322
|
+
if (status === 401 || status === 403) { err(' Key rejected — run: lvtn login'); return 1 }
|
|
323
|
+
if (status === 400) { err(` ${data.detail || 'bad request'}`); return 1 }
|
|
324
|
+
if (status !== 200) { err(` Connect failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
325
|
+
if (!data.ok) { err(` ✗ Connection test failed: ${data.error || 'unknown'}`); return 1 }
|
|
326
|
+
const info = Object.entries(data).filter(([k]) => !['ok', 'service', 'connected'].includes(k))
|
|
327
|
+
out(` ✓ ${service} connected${info.length ? ' ' + info.map(([k, v]) => `${k}=${v}`).join(' ') : ''}`)
|
|
328
|
+
return 0
|
|
329
|
+
}
|
|
330
|
+
if (sub === 'action') {
|
|
331
|
+
const key = requireKey(); if (!key) return 1
|
|
332
|
+
const service = (args._.shift() || '').trim()
|
|
333
|
+
const action = (args._.shift() || '').trim()
|
|
334
|
+
if (!service || !action) { err("Usage: lvtn integrations action <service> <action> [--params '{\"to\":\"x@y.com\"}']"); return 1 }
|
|
335
|
+
let params = {}
|
|
336
|
+
if (args.params) { try { params = JSON.parse(args.params) } catch { err(` --params must be valid JSON, got: ${args.params}`); return 1 } }
|
|
337
|
+
const { status, data } = await api('POST', '/api/integrations/action', { key, body: { service, action, params }, timeout: 60_000 })
|
|
338
|
+
if (status === 401 || status === 403) { err(' Key rejected — run: lvtn login'); return 1 }
|
|
339
|
+
if (status === 402) { err(' Out of KTRS — balance refills automatically. Try later.'); return 1 }
|
|
340
|
+
if (status === 424) { err(` ${data.detail || 'service not connected'} → connect it first: lvtn integrations connect ${service} <token>`); return 1 }
|
|
341
|
+
if (status !== 200) { err(` Action failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
342
|
+
out(JSON.stringify(data, null, 2)); return 0
|
|
343
|
+
}
|
|
344
|
+
if (sub === 'disconnect') {
|
|
345
|
+
const key = requireKey(); if (!key) return 1
|
|
346
|
+
const service = (args._.shift() || '').trim()
|
|
347
|
+
if (!service) { err('Usage: lvtn integrations disconnect <service>'); return 1 }
|
|
348
|
+
const { status, data } = await api('DELETE', `/api/integrations/${service}`, { key, timeout: 30_000 })
|
|
349
|
+
if (status !== 200) { err(` Disconnect failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
350
|
+
out(` ✓ ${service} disconnected`); return 0
|
|
351
|
+
}
|
|
352
|
+
err(` Unknown integrations subcommand: ${sub}`)
|
|
353
|
+
return 1
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── video ────────────────────────────────────────────────────────────────────
|
|
357
|
+
async function pollVideo(getStatus, { tries = 60, every = 10_000, label = 'status' } = {}) {
|
|
358
|
+
for (let i = 0; i < tries; i++) {
|
|
359
|
+
await new Promise((r) => setTimeout(r, every))
|
|
360
|
+
const { status, data } = await getStatus()
|
|
361
|
+
if (status !== 200) { out(` [${String(i + 1).padStart(2, '0')}] poll error (HTTP ${status}) — retrying`); continue }
|
|
362
|
+
const st = data.status || ''
|
|
363
|
+
out(` [${String(i + 1).padStart(2, '0')}] ${label}=${st}${data.provider ? ` via ${data.provider}` : ''}`)
|
|
364
|
+
if (['completed', 'done', 'complete', 'delivered'].includes(st)) {
|
|
365
|
+
const url = data.video_url || data.output_url || data.url || data.delivery_path
|
|
366
|
+
out(`\n ✓ Done! ${url || '(no url returned)'}\n`); return 0
|
|
367
|
+
}
|
|
368
|
+
if (['error', 'failed', 'cancelled'].includes(st)) { err(` ✗ Job ${st}: ${data.error}`); return 1 }
|
|
369
|
+
}
|
|
370
|
+
err(' Timed out waiting for video.'); return 1
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export async function cmdVideo(args) {
|
|
374
|
+
const sub = args._.shift() || 'gen'
|
|
375
|
+
const key = requireKey(); if (!key) return 1
|
|
376
|
+
|
|
377
|
+
if (sub === 'gen' || sub === 'generate') {
|
|
378
|
+
const prompt = args._.join(' ').trim()
|
|
379
|
+
if (!prompt) { err('Usage: lvtn video gen "a dragon over the sea" [--model X --resolution 720p --ratio 16:9 --seconds 5]'); return 1 }
|
|
380
|
+
const body = {
|
|
381
|
+
prompt,
|
|
382
|
+
model: args.model || 'Wan-AI/wan2.7-t2v',
|
|
383
|
+
resolution: args.resolution || '720p',
|
|
384
|
+
ratio: args.ratio || '16:9',
|
|
385
|
+
seconds: String(args.seconds || 5),
|
|
386
|
+
}
|
|
387
|
+
if (args.negative) body.negative_prompt = args.negative
|
|
388
|
+
out(` Generating video: ${prompt.slice(0, 72)}${prompt.length > 72 ? '…' : ''}`)
|
|
389
|
+
const { status, data } = await api('POST', '/api/llm/media/video', { key, body, timeout: 60_000 })
|
|
390
|
+
if (status === 402) { err(' Out of KTRS credits — balance refills automatically. Try later.'); return 1 }
|
|
391
|
+
if (status === 401 || status === 403) { err(' Key rejected — run: lvtn login'); return 1 }
|
|
392
|
+
if (status === 503) { err(` ${data.detail || 'video backend not configured'}`); return 1 }
|
|
393
|
+
if (status !== 200) { err(` Video submission failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
394
|
+
out(` Job ID: ${data.job_id} (charged $${data.charged_usd ?? 0} · $${data.balance_usd} KTRS left)`)
|
|
395
|
+
out(' Polling for completion (Wan2.x takes ~40 s) …')
|
|
396
|
+
return pollVideo(() => api('GET', `/api/llm/media/video/${data.job_id}`, { key, timeout: 30_000 }))
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (sub === 'extend') {
|
|
400
|
+
const videoUrl = (args.videoUrl || '').trim()
|
|
401
|
+
if (!videoUrl) { err('Usage: lvtn video extend --video-url https://... --target-seconds 30'); return 1 }
|
|
402
|
+
const body = { video_url: videoUrl, target_seconds: args.targetSeconds || 30 }
|
|
403
|
+
if (args.segments != null) body.segments = args.segments
|
|
404
|
+
out(` Extending video to ~${body.target_seconds}s`)
|
|
405
|
+
const { status, data } = await nodeApi('POST', '/api/video/extend', { key, body, timeout: 60_000 })
|
|
406
|
+
if (status === 401 || status === 403) { err(' Key rejected — run: lvtn login'); return 1 }
|
|
407
|
+
if (![200, 201, 202].includes(status)) { err(` Extend submit failed (HTTP ${status}): ${data.detail || JSON.stringify(data)}`); return 1 }
|
|
408
|
+
const jobId = data.jobId || data.job_id || data.id
|
|
409
|
+
if (!jobId) { out(` Job submitted but no job ID returned: ${JSON.stringify(data)}`); return 0 }
|
|
410
|
+
out(` Job ID: ${jobId} — polling…`)
|
|
411
|
+
return pollVideo(() => nodeApi('GET', `/api/video/extend/status/${jobId}`, { key, timeout: 30_000 }), { tries: 120 })
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (sub === 'slingshot') {
|
|
415
|
+
const prompt = args._.join(' ').trim()
|
|
416
|
+
if (!prompt) { err('Usage: lvtn video slingshot "prompt" [--mode text_to_video --provider X --duration 5]'); return 1 }
|
|
417
|
+
const body = { prompt, mode: args.mode || 'text_to_video' }
|
|
418
|
+
const options = {}
|
|
419
|
+
if (args.provider) options.provider = args.provider
|
|
420
|
+
if (args.duration) options.duration_seconds = args.duration
|
|
421
|
+
if (Object.keys(options).length) body.options = options
|
|
422
|
+
out(` Slingshot: ${prompt.slice(0, 72)}${prompt.length > 72 ? '…' : ''} mode=${body.mode}`)
|
|
423
|
+
const { status, data } = await nodeApi('POST', '/api/video/slingshot/submit', { key, body, timeout: 60_000 })
|
|
424
|
+
if (status === 401 || status === 403) { err(' Key rejected — run: lvtn login'); return 1 }
|
|
425
|
+
if (![200, 201, 202].includes(status)) { err(` Slingshot submit failed (HTTP ${status}): ${data.detail || JSON.stringify(data)}`); return 1 }
|
|
426
|
+
const jobId = data.jobId || data.job_id || data.id
|
|
427
|
+
if (!jobId) { out(` Job submitted but no job ID returned: ${JSON.stringify(data)}`); return 0 }
|
|
428
|
+
out(` Job ID: ${jobId} — polling…`)
|
|
429
|
+
return pollVideo(() => nodeApi('GET', `/api/video/slingshot/status/${jobId}`, { key, timeout: 30_000 }), { tries: 120 })
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (sub === 'jobs' || sub === 'extensions') {
|
|
433
|
+
const e = await nodeApi('GET', '/api/video/extend/jobs', { key, timeout: 30_000 })
|
|
434
|
+
const s = await nodeApi('GET', '/api/video/slingshot/jobs', { key, timeout: 30_000 })
|
|
435
|
+
out('\n Extend jobs:'); out(' ' + JSON.stringify(e.data))
|
|
436
|
+
out('\n Slingshot jobs:'); out(' ' + JSON.stringify(s.data) + '\n')
|
|
437
|
+
return 0
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
err(` Unknown video subcommand: ${sub} (gen | extend | slingshot | jobs)`)
|
|
441
|
+
return 1
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── local filesystem tools (no backend) ──────────────────────────────────────
|
|
445
|
+
const ARCHIVE_DIR = path.join(os.homedir(), '.lvtn-archive')
|
|
446
|
+
|
|
447
|
+
export async function cmdFs(args) {
|
|
448
|
+
const sub = args._.shift()
|
|
449
|
+
const p = (i = 0) => args._[i]
|
|
450
|
+
switch (sub) {
|
|
451
|
+
case 'read': {
|
|
452
|
+
const f = p(0); if (!f) { err('Usage: lvtn fs read <file>'); return 1 }
|
|
453
|
+
try {
|
|
454
|
+
const st = fs.statSync(f)
|
|
455
|
+
if (st.size > 16 * 1024 * 1024) { err(' File >16MB — refusing to print.'); return 1 }
|
|
456
|
+
process.stdout.write(fs.readFileSync(f, 'utf-8')); return 0
|
|
457
|
+
} catch (e) { err(` ${e.message}`); return 1 }
|
|
458
|
+
}
|
|
459
|
+
case 'write': {
|
|
460
|
+
const f = p(0); if (!f) { err('Usage: echo "body" | lvtn fs write <file>'); return 1 }
|
|
461
|
+
const body = await readStdin()
|
|
462
|
+
try {
|
|
463
|
+
if (fs.existsSync(f)) {
|
|
464
|
+
fs.mkdirSync(ARCHIVE_DIR, { recursive: true })
|
|
465
|
+
fs.copyFileSync(f, path.join(ARCHIVE_DIR, `${path.basename(f)}.${Date.now()}`))
|
|
466
|
+
}
|
|
467
|
+
fs.mkdirSync(path.dirname(path.resolve(f)), { recursive: true })
|
|
468
|
+
fs.writeFileSync(f, body, 'utf-8'); out(` wrote ${f} (${body.length} bytes)`); return 0
|
|
469
|
+
} catch (e) { err(` ${e.message}`); return 1 }
|
|
470
|
+
}
|
|
471
|
+
case 'ls': {
|
|
472
|
+
const d = p(0) || '.'
|
|
473
|
+
try {
|
|
474
|
+
for (const name of fs.readdirSync(d)) {
|
|
475
|
+
const st = fs.statSync(path.join(d, name))
|
|
476
|
+
out(` ${st.isDirectory() ? 'd' : '-'} ${String(st.size).padStart(10)} ${name}`)
|
|
477
|
+
}
|
|
478
|
+
return 0
|
|
479
|
+
} catch (e) { err(` ${e.message}`); return 1 }
|
|
480
|
+
}
|
|
481
|
+
case 'find': {
|
|
482
|
+
const pattern = p(0) || '*'; const root = p(1) || '.'
|
|
483
|
+
const re = new RegExp('^' + pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$')
|
|
484
|
+
walk(root, (fp, name) => { if (re.test(name)) out(' ' + fp) }); return 0
|
|
485
|
+
}
|
|
486
|
+
case 'grep': {
|
|
487
|
+
const needle = p(0); const root = p(1) || '.'
|
|
488
|
+
if (!needle) { err('Usage: lvtn fs grep <text> [root]'); return 1 }
|
|
489
|
+
walk(root, (fp) => {
|
|
490
|
+
try {
|
|
491
|
+
const lines = fs.readFileSync(fp, 'utf-8').split('\n')
|
|
492
|
+
lines.forEach((ln, i) => { if (ln.includes(needle)) out(` ${fp}:${i + 1}: ${ln.trim().slice(0, 120)}`) })
|
|
493
|
+
} catch { /* binary/unreadable */ }
|
|
494
|
+
}); return 0
|
|
495
|
+
}
|
|
496
|
+
case 'mv': { try { fs.renameSync(p(0), p(1)); out(` moved ${p(0)} -> ${p(1)}`); return 0 } catch (e) { err(` ${e.message}`); return 1 } }
|
|
497
|
+
case 'cp': { try { fs.cpSync(p(0), p(1), { recursive: true }); out(` copied ${p(0)} -> ${p(1)}`); return 0 } catch (e) { err(` ${e.message}`); return 1 } }
|
|
498
|
+
case 'rm': {
|
|
499
|
+
const f = p(0); if (!f) { err('Usage: lvtn fs rm <path> (archives — never true-deletes)'); return 1 }
|
|
500
|
+
try {
|
|
501
|
+
fs.mkdirSync(ARCHIVE_DIR, { recursive: true })
|
|
502
|
+
const dest = path.join(ARCHIVE_DIR, `${path.basename(f)}.${Date.now()}`)
|
|
503
|
+
fs.cpSync(f, dest, { recursive: true }); fs.rmSync(f, { recursive: true, force: true })
|
|
504
|
+
out(` archived to ${dest} (restorable)`); return 0
|
|
505
|
+
} catch (e) { err(` ${e.message}`); return 1 }
|
|
506
|
+
}
|
|
507
|
+
case 'reveal': {
|
|
508
|
+
const f = p(0) || '.'
|
|
509
|
+
const plt = process.platform
|
|
510
|
+
const cmd = plt === 'win32' ? 'explorer' : plt === 'darwin' ? 'open' : 'xdg-open'
|
|
511
|
+
spawnSync(cmd, [path.resolve(f)], { stdio: 'ignore' }); out(` opened ${f}`); return 0
|
|
512
|
+
}
|
|
513
|
+
case 'extract': {
|
|
514
|
+
const f = p(0); const dest = p(1) || '.'
|
|
515
|
+
if (!f) { err('Usage: lvtn fs extract <archive> [dest]'); return 1 }
|
|
516
|
+
const r = process.platform === 'win32'
|
|
517
|
+
? spawnSync('tar', ['-xf', f, '-C', dest], { stdio: 'inherit' })
|
|
518
|
+
: spawnSync('tar', ['-xf', f, '-C', dest], { stdio: 'inherit' })
|
|
519
|
+
return r.status || 0
|
|
520
|
+
}
|
|
521
|
+
case 'zip': {
|
|
522
|
+
const root = p(0); const o = p(1)
|
|
523
|
+
if (!root || !o) { err('Usage: lvtn fs zip <path> <out.tar.gz>'); return 1 }
|
|
524
|
+
const r = spawnSync('tar', ['-czf', o, root], { stdio: 'inherit' }); return r.status || 0
|
|
525
|
+
}
|
|
526
|
+
case 'ask': {
|
|
527
|
+
const f = p(0); if (!f) { err('Usage: lvtn fs ask <file> "your question"'); return 1 }
|
|
528
|
+
const key = requireKey(); if (!key) return 1
|
|
529
|
+
const question = args._.slice(1).join(' ').trim()
|
|
530
|
+
let content
|
|
531
|
+
try { content = fs.readFileSync(f, 'utf-8').slice(0, 200_000) } catch (e) { err(` ${e.message}`); return 1 }
|
|
532
|
+
const prompt = `File ${f}:\n\n${content}\n\n---\n${question || 'Summarize this file.'}`
|
|
533
|
+
const { status, data } = await api('POST', '/api/llm/orchestrate', { key, body: { messages: [{ role: 'user', content: prompt }] }, timeout: 240_000 })
|
|
534
|
+
if (status !== 200) { err(` Ask failed (HTTP ${status}): ${data.detail}`); return 1 }
|
|
535
|
+
out(`\n${(data.text || '').trim()}\n`); return 0
|
|
536
|
+
}
|
|
537
|
+
default:
|
|
538
|
+
out(' lvtn fs read|write|ls|find|grep|mv|cp|rm|extract|zip|reveal|ask')
|
|
539
|
+
return sub ? 1 : 0
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function walk(root, cb) {
|
|
544
|
+
let entries
|
|
545
|
+
try { entries = fs.readdirSync(root, { withFileTypes: true }) } catch { return }
|
|
546
|
+
for (const e of entries) {
|
|
547
|
+
const fp = path.join(root, e.name)
|
|
548
|
+
if (e.isDirectory()) { if (!['node_modules', '.git'].includes(e.name)) walk(fp, cb) }
|
|
549
|
+
else cb(fp, e.name)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function readStdin() {
|
|
554
|
+
return new Promise((resolve) => {
|
|
555
|
+
if (process.stdin.isTTY) { resolve(''); return }
|
|
556
|
+
let data = ''
|
|
557
|
+
process.stdin.setEncoding('utf-8')
|
|
558
|
+
process.stdin.on('data', (c) => { data += c })
|
|
559
|
+
process.stdin.on('end', () => resolve(data))
|
|
560
|
+
})
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ── banner ───────────────────────────────────────────────────────────────────
|
|
564
|
+
export function cmdBanner() {
|
|
565
|
+
const C = (n, s) => `\x1b[${n}m${s}\x1b[0m`
|
|
566
|
+
out('')
|
|
567
|
+
out(C('36', ' ╔═══════════════════════════════════════════════════════════╗'))
|
|
568
|
+
out(C('36', ' ║') + C('1;35', ' 🐉 L E V I A T H A N T A L O N 🐉 ') + C('36', '║'))
|
|
569
|
+
out(C('36', ' ║') + C('2;37', ' Leviathan of HaShem Synthetic Intelligence · swarm ') + C('36', '║'))
|
|
570
|
+
out(C('36', ' ╚═══════════════════════════════════════════════════════════╝'))
|
|
571
|
+
out('')
|
|
572
|
+
out(C('2;37', ` 708-agent Kabbalistic swarm in your terminal · v${VERSION}`))
|
|
573
|
+
out(C('2;37', ' hachazal (Da\'ath) → 7 Angels × 100 agents · cloud, KTRS-metered'))
|
|
574
|
+
out('')
|
|
575
|
+
out(` ${C('33', 'Get started:')} lvtn signup → lvtn login → lvtn chat`)
|
|
576
|
+
out(` ${C('33', 'Full TUI:')} the 6-frame sixel art deck ships with the pip build (pip install lvtn)`)
|
|
577
|
+
out('')
|
|
578
|
+
}
|