@vellumai/cli 0.4.1 → 0.4.3
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/package.json +1 -1
- package/src/adapters/install.sh +122 -3
- package/src/commands/hatch.ts +49 -2
- package/src/commands/login.ts +27 -0
- package/src/commands/pair.ts +8 -0
- package/src/commands/ps.ts +11 -0
- package/src/commands/recover.ts +11 -0
- package/src/commands/retire.ts +14 -0
- package/src/commands/sleep.ts +8 -0
- package/src/commands/ssh.ts +11 -0
- package/src/commands/wake.ts +8 -0
- package/src/components/DefaultMainScreen.tsx +84 -49
- package/src/index.ts +2 -2
- package/src/lib/local.ts +28 -1
package/package.json
CHANGED
package/src/adapters/install.sh
CHANGED
|
@@ -14,13 +14,23 @@ success() { printf "${GREEN}${BOLD}==>${RESET} ${BOLD}%s${RESET}\n" "$1"; }
|
|
|
14
14
|
error() { printf "${RED}error:${RESET} %s\n" "$1" >&2; }
|
|
15
15
|
|
|
16
16
|
ensure_git() {
|
|
17
|
-
|
|
17
|
+
# On macOS, /usr/bin/git is a shim that triggers an "Install Command Line
|
|
18
|
+
# Developer Tools" popup instead of running git. Check that git actually
|
|
19
|
+
# works, not just that the binary exists.
|
|
20
|
+
if command -v git >/dev/null 2>&1 && git --version >/dev/null 2>&1; then
|
|
18
21
|
success "git already installed ($(git --version))"
|
|
19
22
|
return
|
|
20
23
|
fi
|
|
21
24
|
|
|
22
25
|
info "Installing git..."
|
|
23
|
-
if
|
|
26
|
+
if [ "$(uname -s)" = "Darwin" ]; then
|
|
27
|
+
if command -v brew >/dev/null 2>&1; then
|
|
28
|
+
brew install git
|
|
29
|
+
else
|
|
30
|
+
error "git is required. Install Homebrew (https://brew.sh) then run: brew install git"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
elif command -v apt-get >/dev/null 2>&1; then
|
|
24
34
|
sudo apt-get update -qq && sudo apt-get install -y -qq git
|
|
25
35
|
elif command -v yum >/dev/null 2>&1; then
|
|
26
36
|
sudo yum install -y git
|
|
@@ -31,7 +41,11 @@ ensure_git() {
|
|
|
31
41
|
exit 1
|
|
32
42
|
fi
|
|
33
43
|
|
|
34
|
-
|
|
44
|
+
# Clear bash's command hash so it finds the newly installed git binary
|
|
45
|
+
# instead of the cached path to the macOS /usr/bin/git shim.
|
|
46
|
+
hash -r 2>/dev/null || true
|
|
47
|
+
|
|
48
|
+
if ! git --version >/dev/null 2>&1; then
|
|
35
49
|
error "git installation failed. Please install manually."
|
|
36
50
|
exit 1
|
|
37
51
|
fi
|
|
@@ -79,6 +93,98 @@ ensure_bun() {
|
|
|
79
93
|
success "bun installed ($(bun --version))"
|
|
80
94
|
}
|
|
81
95
|
|
|
96
|
+
# Ensure ~/.bun/bin is in the user's shell profile so bun and vellum are
|
|
97
|
+
# available in new terminal sessions. The bun installer sometimes skips
|
|
98
|
+
# this (e.g. when stdin is piped via curl | bash).
|
|
99
|
+
configure_shell_profile() {
|
|
100
|
+
local bun_line='export BUN_INSTALL="$HOME/.bun"'
|
|
101
|
+
local path_line='export PATH="$BUN_INSTALL/bin:$PATH"'
|
|
102
|
+
local snippet
|
|
103
|
+
snippet=$(printf '\n# bun\n%s\n%s\n' "$bun_line" "$path_line")
|
|
104
|
+
|
|
105
|
+
local profiles=()
|
|
106
|
+
local shell_name="${SHELL:-}"
|
|
107
|
+
|
|
108
|
+
if [[ "$shell_name" == */zsh ]]; then
|
|
109
|
+
profiles+=("$HOME/.zshrc")
|
|
110
|
+
elif [[ "$shell_name" == */bash ]]; then
|
|
111
|
+
# Write to both .bashrc (non-login shells, e.g. new terminal on Linux)
|
|
112
|
+
# and .bash_profile (login shells, e.g. macOS Terminal.app)
|
|
113
|
+
profiles+=("$HOME/.bashrc")
|
|
114
|
+
[ -f "$HOME/.bash_profile" ] && profiles+=("$HOME/.bash_profile")
|
|
115
|
+
else
|
|
116
|
+
# Unknown shell — try both
|
|
117
|
+
profiles+=("$HOME/.bashrc")
|
|
118
|
+
[ -f "$HOME/.zshrc" ] && profiles+=("$HOME/.zshrc")
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
for profile in "${profiles[@]}"; do
|
|
122
|
+
if [ -f "$profile" ] && grep -q 'BUN_INSTALL' "$profile" 2>/dev/null; then
|
|
123
|
+
continue
|
|
124
|
+
fi
|
|
125
|
+
printf '%s\n' "$snippet" >> "$profile"
|
|
126
|
+
success "Added bun to PATH in $profile"
|
|
127
|
+
done
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Create a symlink so `vellum` is available without ~/.bun/bin in PATH.
|
|
131
|
+
# Tries /usr/local/bin first (works on most systems), falls back to
|
|
132
|
+
# ~/.local/bin (user-writable, no sudo needed).
|
|
133
|
+
# This is best-effort — failure must not abort the install script.
|
|
134
|
+
symlink_vellum() {
|
|
135
|
+
local vellum_bin="$HOME/.bun/bin/vellum"
|
|
136
|
+
if [ ! -f "$vellum_bin" ]; then
|
|
137
|
+
return 0
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# Skip if vellum is already resolvable outside of ~/.bun/bin
|
|
141
|
+
local resolved
|
|
142
|
+
resolved=$(command -v vellum 2>/dev/null || true)
|
|
143
|
+
if [ -n "$resolved" ] && [ "$resolved" != "$vellum_bin" ]; then
|
|
144
|
+
return 0
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# Try /usr/local/bin (may need sudo on some systems)
|
|
148
|
+
if [ -d "/usr/local/bin" ] && [ -w "/usr/local/bin" ]; then
|
|
149
|
+
if ln -sf "$vellum_bin" /usr/local/bin/vellum 2>/dev/null; then
|
|
150
|
+
success "Symlinked /usr/local/bin/vellum → $vellum_bin"
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# Fallback: ~/.local/bin
|
|
156
|
+
local local_bin="$HOME/.local/bin"
|
|
157
|
+
mkdir -p "$local_bin" 2>/dev/null || true
|
|
158
|
+
if ln -sf "$vellum_bin" "$local_bin/vellum" 2>/dev/null; then
|
|
159
|
+
success "Symlinked $local_bin/vellum → $vellum_bin"
|
|
160
|
+
# Ensure ~/.local/bin is in PATH in shell profile
|
|
161
|
+
for profile in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile"; do
|
|
162
|
+
if [ -f "$profile" ] && ! grep -q "$local_bin" "$profile" 2>/dev/null; then
|
|
163
|
+
printf '\nexport PATH="%s:$PATH"\n' "$local_bin" >> "$profile"
|
|
164
|
+
fi
|
|
165
|
+
done
|
|
166
|
+
return 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
return 0
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Write a small sourceable env file to ~/.config/vellum/env so callers can
|
|
173
|
+
# pick up PATH changes without restarting their shell:
|
|
174
|
+
# curl -fsSL https://assistant.vellum.ai/install.sh | bash && . ~/.config/vellum/env
|
|
175
|
+
write_env_file() {
|
|
176
|
+
local env_dir="${XDG_CONFIG_HOME:-$HOME/.config}/vellum"
|
|
177
|
+
local env_file="$env_dir/env"
|
|
178
|
+
mkdir -p "$env_dir"
|
|
179
|
+
cat > "$env_file" <<'ENVEOF'
|
|
180
|
+
export BUN_INSTALL="$HOME/.bun"
|
|
181
|
+
case ":$PATH:" in
|
|
182
|
+
*":$BUN_INSTALL/bin:"*) ;;
|
|
183
|
+
*) export PATH="$BUN_INSTALL/bin:$PATH" ;;
|
|
184
|
+
esac
|
|
185
|
+
ENVEOF
|
|
186
|
+
}
|
|
187
|
+
|
|
82
188
|
install_vellum() {
|
|
83
189
|
if command -v vellum >/dev/null 2>&1; then
|
|
84
190
|
info "Updating vellum to latest..."
|
|
@@ -103,7 +209,20 @@ main() {
|
|
|
103
209
|
|
|
104
210
|
ensure_git
|
|
105
211
|
ensure_bun
|
|
212
|
+
configure_shell_profile
|
|
106
213
|
install_vellum
|
|
214
|
+
symlink_vellum
|
|
215
|
+
|
|
216
|
+
# Write a sourceable env file so the quickstart one-liner can pick up
|
|
217
|
+
# PATH changes in the caller's shell:
|
|
218
|
+
# curl ... | bash && . ~/.config/vellum/env
|
|
219
|
+
write_env_file
|
|
220
|
+
|
|
221
|
+
# Source the shell profile so vellum hatch runs with the correct PATH
|
|
222
|
+
# in this session (the profile changes only take effect in new shells
|
|
223
|
+
# otherwise).
|
|
224
|
+
export BUN_INSTALL="$HOME/.bun"
|
|
225
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
107
226
|
|
|
108
227
|
info "Running vellum hatch..."
|
|
109
228
|
printf "\n"
|
package/src/commands/hatch.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { loadAllAssistants, saveAssistantEntry, syncConfigToLockfile } from "../
|
|
|
13
13
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
14
14
|
import { hatchAws } from "../lib/aws";
|
|
15
15
|
import {
|
|
16
|
+
GATEWAY_PORT,
|
|
16
17
|
SPECIES_CONFIG,
|
|
17
18
|
VALID_REMOTE_HOSTS,
|
|
18
19
|
VALID_SPECIES,
|
|
@@ -21,6 +22,7 @@ import type { RemoteHost, Species } from "../lib/constants";
|
|
|
21
22
|
import { hatchGcp } from "../lib/gcp";
|
|
22
23
|
import type { PollResult, WatchHatchingResult } from "../lib/gcp";
|
|
23
24
|
import { startLocalDaemon, startGateway, stopLocalProcesses } from "../lib/local";
|
|
25
|
+
import { probePort } from "../lib/port-probe";
|
|
24
26
|
import { isProcessAlive } from "../lib/process";
|
|
25
27
|
import { generateRandomSuffix } from "../lib/random-name";
|
|
26
28
|
import { validateAssistantName } from "../lib/retire-archive";
|
|
@@ -155,7 +157,22 @@ function parseArgs(): HatchArgs {
|
|
|
155
157
|
|
|
156
158
|
for (let i = 0; i < args.length; i++) {
|
|
157
159
|
const arg = args[i];
|
|
158
|
-
if (arg === "-
|
|
160
|
+
if (arg === "--help" || arg === "-h") {
|
|
161
|
+
console.log("Usage: vellum hatch [species] [options]");
|
|
162
|
+
console.log("");
|
|
163
|
+
console.log("Create a new assistant instance.");
|
|
164
|
+
console.log("");
|
|
165
|
+
console.log("Species:");
|
|
166
|
+
console.log(" vellum Default assistant (default)");
|
|
167
|
+
console.log(" openclaw OpenClaw adapter");
|
|
168
|
+
console.log("");
|
|
169
|
+
console.log("Options:");
|
|
170
|
+
console.log(" -d Run in detached mode");
|
|
171
|
+
console.log(" --name <name> Custom instance name");
|
|
172
|
+
console.log(" --remote <host> Remote host (local, gcp, aws, custom)");
|
|
173
|
+
console.log(" --daemon-only Start daemon only, skip gateway");
|
|
174
|
+
process.exit(0);
|
|
175
|
+
} else if (arg === "-d") {
|
|
159
176
|
detached = true;
|
|
160
177
|
} else if (arg === "--daemon-only") {
|
|
161
178
|
daemonOnly = true;
|
|
@@ -496,6 +513,8 @@ async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | un
|
|
|
496
513
|
});
|
|
497
514
|
|
|
498
515
|
if (!registerRes.ok) {
|
|
516
|
+
const body = await registerRes.text().catch(() => "");
|
|
517
|
+
console.warn(`⚠ Could not register pairing request: ${registerRes.status} ${registerRes.statusText}${body ? ` — ${body}` : ""}. Run \`vellum pair\` to try again.\n`);
|
|
499
518
|
return;
|
|
500
519
|
}
|
|
501
520
|
|
|
@@ -519,8 +538,10 @@ async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | un
|
|
|
519
538
|
console.log(qrString);
|
|
520
539
|
console.log("This pairing request expires in 5 minutes.");
|
|
521
540
|
console.log("Run `vellum pair` to generate a new one.\n");
|
|
522
|
-
} catch {
|
|
541
|
+
} catch (err) {
|
|
523
542
|
// Non-fatal — pairing is optional
|
|
543
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
544
|
+
console.warn(`⚠ Could not generate pairing QR code: ${reason}. Run \`vellum pair\` to try again.\n`);
|
|
524
545
|
}
|
|
525
546
|
}
|
|
526
547
|
|
|
@@ -540,6 +561,32 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
|
|
|
540
561
|
console.log("🧹 Cleaning up stale local processes (no lock file entry)...\n");
|
|
541
562
|
await stopLocalProcesses();
|
|
542
563
|
}
|
|
564
|
+
|
|
565
|
+
// Verify required ports are available before starting any services.
|
|
566
|
+
// Only check when no local assistants exist — if there are existing local
|
|
567
|
+
// assistants, their daemon/gateway/qdrant legitimately own these ports.
|
|
568
|
+
const RUNTIME_HTTP_PORT = Number(process.env.RUNTIME_HTTP_PORT) || 7821;
|
|
569
|
+
const QDRANT_PORT = 6333;
|
|
570
|
+
const requiredPorts = [
|
|
571
|
+
{ name: "daemon", port: RUNTIME_HTTP_PORT },
|
|
572
|
+
{ name: "gateway", port: GATEWAY_PORT },
|
|
573
|
+
{ name: "qdrant", port: QDRANT_PORT },
|
|
574
|
+
];
|
|
575
|
+
const conflicts: string[] = [];
|
|
576
|
+
await Promise.all(
|
|
577
|
+
requiredPorts.map(async ({ name, port }) => {
|
|
578
|
+
if (await probePort(port)) {
|
|
579
|
+
conflicts.push(` - Port ${port} (${name}) is already in use`);
|
|
580
|
+
}
|
|
581
|
+
}),
|
|
582
|
+
);
|
|
583
|
+
if (conflicts.length > 0) {
|
|
584
|
+
throw new Error(
|
|
585
|
+
`Cannot hatch — required ports are already in use:\n${conflicts.join("\n")}\n\n` +
|
|
586
|
+
"Stop the conflicting processes or use environment variables to configure alternative ports " +
|
|
587
|
+
"(RUNTIME_HTTP_PORT, GATEWAY_PORT).",
|
|
588
|
+
);
|
|
589
|
+
}
|
|
543
590
|
}
|
|
544
591
|
|
|
545
592
|
const baseDataDir = join(process.env.BASE_DATA_DIR?.trim() || (process.env.HOME ?? userInfo().homedir), ".vellum");
|
package/src/commands/login.ts
CHANGED
|
@@ -7,6 +7,17 @@ import {
|
|
|
7
7
|
|
|
8
8
|
export async function login(): Promise<void> {
|
|
9
9
|
const args = process.argv.slice(3);
|
|
10
|
+
|
|
11
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
12
|
+
console.log("Usage: vellum login --token <session-token>");
|
|
13
|
+
console.log("");
|
|
14
|
+
console.log("Log in to the Vellum platform.");
|
|
15
|
+
console.log("");
|
|
16
|
+
console.log("Options:");
|
|
17
|
+
console.log(" --token <token> Session token from the Vellum platform");
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
let token: string | null = null;
|
|
11
22
|
|
|
12
23
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -43,11 +54,27 @@ export async function login(): Promise<void> {
|
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
export async function logout(): Promise<void> {
|
|
57
|
+
const args = process.argv.slice(3);
|
|
58
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
59
|
+
console.log("Usage: vellum logout");
|
|
60
|
+
console.log("");
|
|
61
|
+
console.log("Log out of the Vellum platform and remove the stored session token.");
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
clearPlatformToken();
|
|
47
66
|
console.log("Logged out. Platform token removed.");
|
|
48
67
|
}
|
|
49
68
|
|
|
50
69
|
export async function whoami(): Promise<void> {
|
|
70
|
+
const args = process.argv.slice(3);
|
|
71
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
72
|
+
console.log("Usage: vellum whoami");
|
|
73
|
+
console.log("");
|
|
74
|
+
console.log("Show the currently logged-in Vellum platform user.");
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
51
78
|
const token = readPlatformToken();
|
|
52
79
|
if (!token) {
|
|
53
80
|
console.error("Not logged in. Run `vellum login --token <token>` first.");
|
package/src/commands/pair.ts
CHANGED
|
@@ -72,6 +72,14 @@ async function pollForApproval(
|
|
|
72
72
|
|
|
73
73
|
export async function pair(): Promise<void> {
|
|
74
74
|
const args = process.argv.slice(3);
|
|
75
|
+
|
|
76
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
77
|
+
console.log("Usage: vellum pair <path-to-qrcode.png>");
|
|
78
|
+
console.log("");
|
|
79
|
+
console.log("Pair with a remote assistant by scanning the QR code PNG generated during setup.");
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
const qrCodePath = args[0] || process.env.VELLUM_CUSTOM_QR_CODE_PATH;
|
|
76
84
|
|
|
77
85
|
if (!qrCodePath) {
|
package/src/commands/ps.ts
CHANGED
|
@@ -409,6 +409,17 @@ async function listAllAssistants(): Promise<void> {
|
|
|
409
409
|
// ── Entry point ─────────────────────────────────────────────────
|
|
410
410
|
|
|
411
411
|
export async function ps(): Promise<void> {
|
|
412
|
+
const args = process.argv.slice(3);
|
|
413
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
414
|
+
console.log("Usage: vellum ps [<name>]");
|
|
415
|
+
console.log("");
|
|
416
|
+
console.log("List all assistants, or show processes for a specific assistant.");
|
|
417
|
+
console.log("");
|
|
418
|
+
console.log("Arguments:");
|
|
419
|
+
console.log(" <name> Show processes for the named assistant");
|
|
420
|
+
process.exit(0);
|
|
421
|
+
}
|
|
422
|
+
|
|
412
423
|
const assistantId = process.argv[3];
|
|
413
424
|
|
|
414
425
|
if (!assistantId) {
|
package/src/commands/recover.ts
CHANGED
|
@@ -9,6 +9,17 @@ import { getArchivePath, getMetadataPath } from "../lib/retire-archive";
|
|
|
9
9
|
import { exec } from "../lib/step-runner";
|
|
10
10
|
|
|
11
11
|
export async function recover(): Promise<void> {
|
|
12
|
+
const args = process.argv.slice(3);
|
|
13
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
14
|
+
console.log("Usage: vellum recover <name>");
|
|
15
|
+
console.log("");
|
|
16
|
+
console.log("Restore a previously retired local assistant from its archive.");
|
|
17
|
+
console.log("");
|
|
18
|
+
console.log("Arguments:");
|
|
19
|
+
console.log(" <name> Name of the retired assistant to recover");
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
const name = process.argv[3];
|
|
13
24
|
if (!name) {
|
|
14
25
|
console.error("Usage: vellum recover <name>");
|
package/src/commands/retire.ts
CHANGED
|
@@ -121,6 +121,20 @@ function parseSource(): string | undefined {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
export async function retire(): Promise<void> {
|
|
124
|
+
const args = process.argv.slice(3);
|
|
125
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
126
|
+
console.log("Usage: vellum retire <name> [--source <source>]");
|
|
127
|
+
console.log("");
|
|
128
|
+
console.log("Delete an assistant instance and archive its data.");
|
|
129
|
+
console.log("");
|
|
130
|
+
console.log("Arguments:");
|
|
131
|
+
console.log(" <name> Name of the assistant to retire");
|
|
132
|
+
console.log("");
|
|
133
|
+
console.log("Options:");
|
|
134
|
+
console.log(" --source <source> Source identifier for the retirement");
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
|
|
124
138
|
const name = process.argv[3];
|
|
125
139
|
|
|
126
140
|
if (!name) {
|
package/src/commands/sleep.ts
CHANGED
|
@@ -5,6 +5,14 @@ import { loadAllAssistants } from "../lib/assistant-config";
|
|
|
5
5
|
import { stopProcessByPidFile } from "../lib/process";
|
|
6
6
|
|
|
7
7
|
export async function sleep(): Promise<void> {
|
|
8
|
+
const args = process.argv.slice(3);
|
|
9
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
10
|
+
console.log("Usage: vellum sleep");
|
|
11
|
+
console.log("");
|
|
12
|
+
console.log("Stop the daemon and gateway processes.");
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
const assistants = loadAllAssistants();
|
|
9
17
|
const hasLocal = assistants.some((a) => a.cloud === "local");
|
|
10
18
|
if (!hasLocal) {
|
package/src/commands/ssh.ts
CHANGED
|
@@ -33,6 +33,17 @@ function extractHostFromUrl(url: string): string {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export async function ssh(): Promise<void> {
|
|
36
|
+
const args = process.argv.slice(3);
|
|
37
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
38
|
+
console.log("Usage: vellum ssh [<name>]");
|
|
39
|
+
console.log("");
|
|
40
|
+
console.log("SSH into a remote assistant instance.");
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log("Arguments:");
|
|
43
|
+
console.log(" <name> Name of the assistant to connect to (defaults to latest)");
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
const name = process.argv[3];
|
|
37
48
|
const entry = name ? findAssistantByName(name) : loadLatestAssistant();
|
|
38
49
|
|
package/src/commands/wake.ts
CHANGED
|
@@ -7,6 +7,14 @@ import { isProcessAlive } from "../lib/process";
|
|
|
7
7
|
import { startLocalDaemon, startGateway } from "../lib/local";
|
|
8
8
|
|
|
9
9
|
export async function wake(): Promise<void> {
|
|
10
|
+
const args = process.argv.slice(3);
|
|
11
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
12
|
+
console.log("Usage: vellum wake");
|
|
13
|
+
console.log("");
|
|
14
|
+
console.log("Start the daemon and gateway processes.");
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
const assistants = loadAllAssistants();
|
|
11
19
|
const hasLocal = assistants.some((a) => a.cloud === "local");
|
|
12
20
|
if (!hasLocal) {
|
|
@@ -32,6 +32,51 @@ const POLL_INTERVAL_MS = 3000;
|
|
|
32
32
|
const SEND_TIMEOUT_MS = 5000;
|
|
33
33
|
const RESPONSE_POLL_INTERVAL_MS = 1000;
|
|
34
34
|
|
|
35
|
+
// ── Layout constants ──────────────────────────────────────
|
|
36
|
+
const MAX_TOTAL_WIDTH = 72;
|
|
37
|
+
const DEFAULT_TERMINAL_COLUMNS = 80;
|
|
38
|
+
const DEFAULT_TERMINAL_ROWS = 24;
|
|
39
|
+
const LEFT_PANEL_WIDTH = 36;
|
|
40
|
+
|
|
41
|
+
const HEADER_PREFIX = "── Vellum ";
|
|
42
|
+
|
|
43
|
+
// Left panel structure: HEADER lines + art + FOOTER lines
|
|
44
|
+
const LEFT_HEADER_LINES = 3; // spacer + heading + spacer
|
|
45
|
+
const LEFT_FOOTER_LINES = 3; // spacer + runtimeUrl + dirName
|
|
46
|
+
|
|
47
|
+
// Right panel structure
|
|
48
|
+
const TIPS = ["Send a message to start chatting", "Use /help to see available commands"];
|
|
49
|
+
const RIGHT_PANEL_INFO_SECTIONS = 3; // Assistant, Species, Status — each with heading + value
|
|
50
|
+
const RIGHT_PANEL_SPACERS = 2; // top spacer + spacer between tips and info
|
|
51
|
+
const RIGHT_PANEL_TIPS_HEADING = 1;
|
|
52
|
+
const RIGHT_PANEL_LINE_COUNT =
|
|
53
|
+
RIGHT_PANEL_SPACERS + RIGHT_PANEL_TIPS_HEADING + TIPS.length + RIGHT_PANEL_INFO_SECTIONS * 2;
|
|
54
|
+
|
|
55
|
+
// Header chrome (borders around panel content)
|
|
56
|
+
const HEADER_TOP_BORDER_LINES = 1; // "── Vellum ───..." line
|
|
57
|
+
const HEADER_BOTTOM_BORDER_LINES = 2; // bottom rule + blank line
|
|
58
|
+
const HEADER_CHROME_LINES = HEADER_TOP_BORDER_LINES + HEADER_BOTTOM_BORDER_LINES;
|
|
59
|
+
|
|
60
|
+
// Selection / Secret windows
|
|
61
|
+
const DIALOG_WINDOW_WIDTH = 60;
|
|
62
|
+
const DIALOG_TITLE_CHROME = 5; // "┌─ " (3) + " " (1) + "┐" (1)
|
|
63
|
+
const DIALOG_BORDER_CORNERS = 2; // └ and ┘
|
|
64
|
+
const SELECTION_OPTION_CHROME = 6; // "│ " (2) + marker (1) + " " (1) + padding‐adjust + "│" (1)
|
|
65
|
+
const SECRET_CONTENT_CHROME = 4; // "│ " (2) + padding + "│" (1) + adjustment
|
|
66
|
+
|
|
67
|
+
// Chat area heights
|
|
68
|
+
const TOOLTIP_HEIGHT = 3;
|
|
69
|
+
const INPUT_AREA_HEIGHT = 4; // separator + input row + separator + hint
|
|
70
|
+
const SELECTION_CHROME_LINES = 3; // title bar + bottom border + spacing
|
|
71
|
+
const SECRET_INPUT_HEIGHT = 5; // title bar + content row + bottom border + tooltip chrome
|
|
72
|
+
const SPINNER_HEIGHT = 1;
|
|
73
|
+
const MIN_FEED_ROWS = 3;
|
|
74
|
+
|
|
75
|
+
// Feed item height estimation
|
|
76
|
+
const TOOL_CALL_CHROME_LINES = 2; // header (┌) + footer (└)
|
|
77
|
+
const MESSAGE_SPACING = 1;
|
|
78
|
+
const HELP_DISPLAY_HEIGHT = 6;
|
|
79
|
+
|
|
35
80
|
interface ListMessagesResponse {
|
|
36
81
|
messages: RuntimeMessage[];
|
|
37
82
|
nextCursor?: string;
|
|
@@ -598,12 +643,10 @@ function DefaultMainScreen({
|
|
|
598
643
|
const accentColor = species === "openclaw" ? "red" : "magenta";
|
|
599
644
|
|
|
600
645
|
const { stdout } = useStdout();
|
|
601
|
-
const terminalColumns = stdout.columns ||
|
|
602
|
-
const totalWidth = Math.min(
|
|
646
|
+
const terminalColumns = stdout.columns || DEFAULT_TERMINAL_COLUMNS;
|
|
647
|
+
const totalWidth = Math.min(MAX_TOTAL_WIDTH, terminalColumns);
|
|
603
648
|
const rightPanelWidth = Math.max(1, totalWidth - LEFT_PANEL_WIDTH);
|
|
604
649
|
|
|
605
|
-
const tips = ["Send a message to start chatting", "Use /help to see available commands"];
|
|
606
|
-
|
|
607
650
|
const leftLines = [
|
|
608
651
|
" ",
|
|
609
652
|
" Meet your Assistant!",
|
|
@@ -617,7 +660,7 @@ function DefaultMainScreen({
|
|
|
617
660
|
const rightLines: StyledLine[] = [
|
|
618
661
|
{ text: " ", style: "normal" },
|
|
619
662
|
{ text: "Tips for getting started", style: "heading" },
|
|
620
|
-
...
|
|
663
|
+
...TIPS.map((t) => ({ text: t, style: "normal" as const })),
|
|
621
664
|
{ text: " ", style: "normal" },
|
|
622
665
|
{ text: "Assistant", style: "heading" },
|
|
623
666
|
{ text: assistantId, style: "dim" },
|
|
@@ -631,7 +674,7 @@ function DefaultMainScreen({
|
|
|
631
674
|
|
|
632
675
|
return (
|
|
633
676
|
<Box flexDirection="column" width={totalWidth}>
|
|
634
|
-
<Text dimColor>{
|
|
677
|
+
<Text dimColor>{HEADER_PREFIX + "─".repeat(Math.max(0, totalWidth - HEADER_PREFIX.length))}</Text>
|
|
635
678
|
<Box flexDirection="row">
|
|
636
679
|
<Box flexDirection="column" width={LEFT_PANEL_WIDTH}>
|
|
637
680
|
{Array.from({ length: maxLines }, (_, i) => {
|
|
@@ -684,15 +727,10 @@ function DefaultMainScreen({
|
|
|
684
727
|
</Box>
|
|
685
728
|
<Text dimColor>{"─".repeat(totalWidth)}</Text>
|
|
686
729
|
<Text> </Text>
|
|
687
|
-
<Tooltip text="Type ? or /help for available commands" delay={1000}>
|
|
688
|
-
<Text dimColor> ? for shortcuts</Text>
|
|
689
|
-
</Tooltip>
|
|
690
|
-
<Text> </Text>
|
|
691
730
|
</Box>
|
|
692
731
|
);
|
|
693
732
|
}
|
|
694
733
|
|
|
695
|
-
const LEFT_PANEL_WIDTH = 36;
|
|
696
734
|
|
|
697
735
|
export interface SelectionRequest {
|
|
698
736
|
title: string;
|
|
@@ -737,13 +775,21 @@ function estimateItemHeight(item: FeedItem, terminalColumns: number): number {
|
|
|
737
775
|
for (const tc of item.toolCalls) {
|
|
738
776
|
const paramCount =
|
|
739
777
|
typeof tc.input === "object" && tc.input ? Object.keys(tc.input).length : 0;
|
|
740
|
-
lines +=
|
|
778
|
+
lines += TOOL_CALL_CHROME_LINES + paramCount + (tc.result !== undefined ? 1 : 0);
|
|
741
779
|
}
|
|
742
780
|
}
|
|
743
|
-
return lines +
|
|
781
|
+
return lines + MESSAGE_SPACING;
|
|
744
782
|
}
|
|
745
783
|
if (item.type === "help") {
|
|
746
|
-
return
|
|
784
|
+
return HELP_DISPLAY_HEIGHT;
|
|
785
|
+
}
|
|
786
|
+
if (item.type === "status" || item.type === "error") {
|
|
787
|
+
const cols = Math.max(1, terminalColumns);
|
|
788
|
+
let lines = 0;
|
|
789
|
+
for (const line of item.text.split("\n")) {
|
|
790
|
+
lines += Math.max(1, Math.ceil(line.length / cols));
|
|
791
|
+
}
|
|
792
|
+
return lines;
|
|
747
793
|
}
|
|
748
794
|
return 1;
|
|
749
795
|
}
|
|
@@ -751,10 +797,9 @@ function estimateItemHeight(item: FeedItem, terminalColumns: number): number {
|
|
|
751
797
|
function calculateHeaderHeight(species: Species): number {
|
|
752
798
|
const config = SPECIES_CONFIG[species];
|
|
753
799
|
const artLength = config.art.length;
|
|
754
|
-
const leftLineCount =
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
return maxLines + 5;
|
|
800
|
+
const leftLineCount = LEFT_HEADER_LINES + artLength + LEFT_FOOTER_LINES;
|
|
801
|
+
const maxLines = Math.max(leftLineCount, RIGHT_PANEL_LINE_COUNT);
|
|
802
|
+
return maxLines + HEADER_CHROME_LINES;
|
|
758
803
|
}
|
|
759
804
|
|
|
760
805
|
const SCROLL_STEP = 5;
|
|
@@ -763,9 +808,8 @@ export function render(runtimeUrl: string, assistantId: string, species: Species
|
|
|
763
808
|
const config = SPECIES_CONFIG[species];
|
|
764
809
|
const art = config.art;
|
|
765
810
|
|
|
766
|
-
const leftLineCount =
|
|
767
|
-
const
|
|
768
|
-
const maxLines = Math.max(leftLineCount, rightLineCount);
|
|
811
|
+
const leftLineCount = LEFT_HEADER_LINES + art.length + LEFT_FOOTER_LINES;
|
|
812
|
+
const maxLines = Math.max(leftLineCount, RIGHT_PANEL_LINE_COUNT);
|
|
769
813
|
|
|
770
814
|
const { unmount } = inkRender(
|
|
771
815
|
<DefaultMainScreen runtimeUrl={runtimeUrl} assistantId={assistantId} species={species} />,
|
|
@@ -773,7 +817,7 @@ export function render(runtimeUrl: string, assistantId: string, species: Species
|
|
|
773
817
|
);
|
|
774
818
|
unmount();
|
|
775
819
|
|
|
776
|
-
const statusCanvasLine =
|
|
820
|
+
const statusCanvasLine = RIGHT_PANEL_LINE_COUNT + HEADER_TOP_BORDER_LINES;
|
|
777
821
|
const statusCol = LEFT_PANEL_WIDTH + 1;
|
|
778
822
|
checkHealth(runtimeUrl)
|
|
779
823
|
.then((health) => {
|
|
@@ -784,7 +828,7 @@ export function render(runtimeUrl: string, assistantId: string, species: Species
|
|
|
784
828
|
})
|
|
785
829
|
.catch(() => {});
|
|
786
830
|
|
|
787
|
-
return
|
|
831
|
+
return maxLines + HEADER_CHROME_LINES;
|
|
788
832
|
}
|
|
789
833
|
|
|
790
834
|
interface SelectionWindowProps {
|
|
@@ -825,15 +869,14 @@ function SelectionWindow({
|
|
|
825
869
|
},
|
|
826
870
|
);
|
|
827
871
|
|
|
828
|
-
const
|
|
829
|
-
const borderH = "\u2500".repeat(Math.max(0, windowWidth - title.length - 5));
|
|
872
|
+
const borderH = "\u2500".repeat(Math.max(0, DIALOG_WINDOW_WIDTH - title.length - DIALOG_TITLE_CHROME));
|
|
830
873
|
|
|
831
874
|
return (
|
|
832
|
-
<Box flexDirection="column" width={
|
|
875
|
+
<Box flexDirection="column" width={DIALOG_WINDOW_WIDTH}>
|
|
833
876
|
<Text>{"\u250C\u2500 " + title + " " + borderH + "\u2510"}</Text>
|
|
834
877
|
{options.map((option, i) => {
|
|
835
878
|
const marker = i === selectedIndex ? "\u276F" : " ";
|
|
836
|
-
const padding = " ".repeat(Math.max(0,
|
|
879
|
+
const padding = " ".repeat(Math.max(0, DIALOG_WINDOW_WIDTH - option.length - SELECTION_OPTION_CHROME));
|
|
837
880
|
return (
|
|
838
881
|
<Text key={i}>
|
|
839
882
|
{"\u2502 "}
|
|
@@ -844,7 +887,7 @@ function SelectionWindow({
|
|
|
844
887
|
</Text>
|
|
845
888
|
);
|
|
846
889
|
})}
|
|
847
|
-
<Text>{"\u2514" + "\u2500".repeat(
|
|
890
|
+
<Text>{"\u2514" + "\u2500".repeat(DIALOG_WINDOW_WIDTH - DIALOG_BORDER_CORNERS) + "\u2518"}</Text>
|
|
848
891
|
<Tooltip text="\u2191/\u2193 navigate Enter select Esc cancel" delay={1000} />
|
|
849
892
|
</Box>
|
|
850
893
|
);
|
|
@@ -888,15 +931,14 @@ function SecretInputWindow({
|
|
|
888
931
|
},
|
|
889
932
|
);
|
|
890
933
|
|
|
891
|
-
const
|
|
892
|
-
const borderH = "\u2500".repeat(Math.max(0, windowWidth - label.length - 5));
|
|
934
|
+
const borderH = "\u2500".repeat(Math.max(0, DIALOG_WINDOW_WIDTH - label.length - DIALOG_TITLE_CHROME));
|
|
893
935
|
const masked = "\u2022".repeat(value.length);
|
|
894
936
|
const displayText = value.length > 0 ? masked : (placeholder ?? "Enter secret...");
|
|
895
937
|
const displayColor = value.length > 0 ? undefined : "gray";
|
|
896
|
-
const contentPad = " ".repeat(Math.max(0,
|
|
938
|
+
const contentPad = " ".repeat(Math.max(0, DIALOG_WINDOW_WIDTH - displayText.length - SECRET_CONTENT_CHROME));
|
|
897
939
|
|
|
898
940
|
return (
|
|
899
|
-
<Box flexDirection="column" width={
|
|
941
|
+
<Box flexDirection="column" width={DIALOG_WINDOW_WIDTH}>
|
|
900
942
|
<Text>{"\u250C\u2500 " + label + " " + borderH + "\u2510"}</Text>
|
|
901
943
|
<Text>
|
|
902
944
|
{"\u2502 "}
|
|
@@ -904,7 +946,7 @@ function SecretInputWindow({
|
|
|
904
946
|
{contentPad}
|
|
905
947
|
{"\u2502"}
|
|
906
948
|
</Text>
|
|
907
|
-
<Text>{"\u2514" + "\u2500".repeat(
|
|
949
|
+
<Text>{"\u2514" + "\u2500".repeat(DIALOG_WINDOW_WIDTH - DIALOG_BORDER_CORNERS) + "\u2518"}</Text>
|
|
908
950
|
<Tooltip text="Enter submit Esc cancel" delay={1000} />
|
|
909
951
|
</Box>
|
|
910
952
|
);
|
|
@@ -985,20 +1027,18 @@ function ChatApp({
|
|
|
985
1027
|
const handleRef_ = useRef<ChatAppHandle | null>(null);
|
|
986
1028
|
|
|
987
1029
|
const { stdout } = useStdout();
|
|
988
|
-
const terminalRows = stdout.rows ||
|
|
989
|
-
const terminalColumns = stdout.columns ||
|
|
1030
|
+
const terminalRows = stdout.rows || DEFAULT_TERMINAL_ROWS;
|
|
1031
|
+
const terminalColumns = stdout.columns || DEFAULT_TERMINAL_COLUMNS;
|
|
990
1032
|
const headerHeight = calculateHeaderHeight(species);
|
|
991
|
-
|
|
992
|
-
const showTooltip = inputFocused && inputValue.length === 0;
|
|
993
|
-
const tooltipHeight = showTooltip ? tooltipBubbleHeight : 0;
|
|
1033
|
+
|
|
994
1034
|
const bottomHeight = selection
|
|
995
|
-
? selection.options.length +
|
|
1035
|
+
? selection.options.length + SELECTION_CHROME_LINES + TOOLTIP_HEIGHT
|
|
996
1036
|
: secretInput
|
|
997
|
-
?
|
|
1037
|
+
? SECRET_INPUT_HEIGHT + TOOLTIP_HEIGHT
|
|
998
1038
|
: spinnerText
|
|
999
|
-
?
|
|
1000
|
-
:
|
|
1001
|
-
const availableRows = Math.max(
|
|
1039
|
+
? SPINNER_HEIGHT + INPUT_AREA_HEIGHT
|
|
1040
|
+
: INPUT_AREA_HEIGHT;
|
|
1041
|
+
const availableRows = Math.max(MIN_FEED_ROWS, terminalRows - headerHeight - bottomHeight);
|
|
1002
1042
|
|
|
1003
1043
|
const addMessage = useCallback((msg: RuntimeMessage) => {
|
|
1004
1044
|
setFeed((prev) => [...prev, msg]);
|
|
@@ -1823,12 +1863,6 @@ function ChatApp({
|
|
|
1823
1863
|
|
|
1824
1864
|
{!selection && !secretInput ? (
|
|
1825
1865
|
<Box flexDirection="column">
|
|
1826
|
-
<Tooltip
|
|
1827
|
-
text="Type a message or /help for commands"
|
|
1828
|
-
visible={inputFocused && inputValue.length === 0}
|
|
1829
|
-
position="above"
|
|
1830
|
-
delay={1000}
|
|
1831
|
-
/>
|
|
1832
1866
|
<Text dimColor>{"\u2500".repeat(terminalColumns)}</Text>
|
|
1833
1867
|
<Box paddingLeft={1}>
|
|
1834
1868
|
<Text color="green" bold>
|
|
@@ -1843,6 +1877,7 @@ function ChatApp({
|
|
|
1843
1877
|
/>
|
|
1844
1878
|
</Box>
|
|
1845
1879
|
<Text dimColor>{"\u2500".repeat(terminalColumns)}</Text>
|
|
1880
|
+
<Text dimColor> ? for shortcuts</Text>
|
|
1846
1881
|
</Box>
|
|
1847
1882
|
) : null}
|
|
1848
1883
|
</Box>
|
package/src/index.ts
CHANGED
|
@@ -31,8 +31,8 @@ const commands = {
|
|
|
31
31
|
email,
|
|
32
32
|
hatch,
|
|
33
33
|
login,
|
|
34
|
-
pair,
|
|
35
34
|
logout,
|
|
35
|
+
pair,
|
|
36
36
|
ps,
|
|
37
37
|
recover,
|
|
38
38
|
retire,
|
|
@@ -90,9 +90,9 @@ async function main() {
|
|
|
90
90
|
console.log(" contacts Manage the contact graph");
|
|
91
91
|
console.log(" email Email operations (status, create inbox)");
|
|
92
92
|
console.log(" hatch Create a new assistant instance");
|
|
93
|
-
console.log(" pair Pair with a remote assistant via QR code");
|
|
94
93
|
console.log(" login Log in to the Vellum platform");
|
|
95
94
|
console.log(" logout Log out of the Vellum platform");
|
|
95
|
+
console.log(" pair Pair with a remote assistant via QR code");
|
|
96
96
|
console.log(" ps List assistants (or processes for a specific assistant)");
|
|
97
97
|
console.log(" recover Restore a previously retired local assistant");
|
|
98
98
|
console.log(" retire Delete an assistant instance");
|
package/src/lib/local.ts
CHANGED
|
@@ -557,8 +557,35 @@ export async function startGateway(assistantId?: string): Promise<string> {
|
|
|
557
557
|
writeFileSync(join(vellumDir, "gateway.pid"), String(gateway.pid), "utf-8");
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
+
const gatewayUrl = publicUrl || `http://localhost:${GATEWAY_PORT}`;
|
|
561
|
+
|
|
562
|
+
// Wait for the gateway to be responsive before returning. Without this,
|
|
563
|
+
// callers (e.g. displayPairingQRCode) may try to connect before the HTTP
|
|
564
|
+
// server is listening and get connection-refused errors.
|
|
565
|
+
const start = Date.now();
|
|
566
|
+
const timeoutMs = 30000;
|
|
567
|
+
let ready = false;
|
|
568
|
+
while (Date.now() - start < timeoutMs) {
|
|
569
|
+
try {
|
|
570
|
+
const res = await fetch(`http://localhost:${GATEWAY_PORT}/healthz`, {
|
|
571
|
+
signal: AbortSignal.timeout(2000),
|
|
572
|
+
});
|
|
573
|
+
if (res.ok) {
|
|
574
|
+
ready = true;
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
} catch {
|
|
578
|
+
// Gateway not ready yet
|
|
579
|
+
}
|
|
580
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (!ready) {
|
|
584
|
+
console.warn("⚠ Gateway started but health check did not respond within 30s");
|
|
585
|
+
}
|
|
586
|
+
|
|
560
587
|
console.log("✅ Gateway started\n");
|
|
561
|
-
return
|
|
588
|
+
return gatewayUrl;
|
|
562
589
|
}
|
|
563
590
|
|
|
564
591
|
/**
|