@vellumai/cli 0.4.52 → 0.4.54
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/bun.lock +1 -0
- package/knip.json +2 -4
- package/package.json +2 -1
- package/src/commands/hatch.ts +4 -11
- package/src/commands/recover.ts +1 -3
- package/src/commands/wake.ts +32 -13
- package/src/lib/jwt.ts +62 -0
- package/src/lib/local.ts +22 -3
- package/src/lib/platform-client.ts +5 -2
- package/src/lib/policy.ts +7 -0
package/bun.lock
CHANGED
package/knip.json
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"entry": ["src/**/*.test.ts", "src/**/__tests__/**/*.ts"],
|
|
3
|
-
"project": ["src/**/*.ts", "src/**/*.tsx"]
|
|
4
|
-
"ignore": ["src/adapters/openclaw-http-server.ts"],
|
|
5
|
-
"ignoreDependencies": ["chalk"]
|
|
2
|
+
"entry": ["src/**/*.test.ts", "src/**/__tests__/**/*.ts", "src/adapters/openclaw-http-server.ts"],
|
|
3
|
+
"project": ["src/**/*.ts", "src/**/*.tsx"]
|
|
6
4
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vellumai/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.54",
|
|
4
4
|
"description": "CLI tools for vellum-assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"author": "Vellum AI",
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
|
+
"chalk": "^5.6.0",
|
|
27
28
|
"ink": "^6.7.0",
|
|
28
29
|
"jsqr": "^1.4.0",
|
|
29
30
|
"pngjs": "^7.0.0",
|
package/src/commands/hatch.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
} from "../lib/constants";
|
|
41
41
|
import type { RemoteHost, Species } from "../lib/constants";
|
|
42
42
|
import { hatchDocker } from "../lib/docker";
|
|
43
|
+
import { mintLocalBearerToken } from "../lib/jwt";
|
|
43
44
|
import { hatchGcp } from "../lib/gcp";
|
|
44
45
|
import type { PollResult, WatchHatchingResult } from "../lib/gcp";
|
|
45
46
|
import {
|
|
@@ -794,17 +795,9 @@ async function hatchLocal(
|
|
|
794
795
|
delete process.env.BASE_DATA_DIR;
|
|
795
796
|
}
|
|
796
797
|
|
|
797
|
-
//
|
|
798
|
-
//
|
|
799
|
-
|
|
800
|
-
let bearerToken: string | undefined;
|
|
801
|
-
try {
|
|
802
|
-
const tokenPath = join(resources.instanceDir, ".vellum", "http-token");
|
|
803
|
-
const token = readFileSync(tokenPath, "utf-8").trim();
|
|
804
|
-
if (token) bearerToken = token;
|
|
805
|
-
} catch {
|
|
806
|
-
// Token file may not exist if daemon started without HTTP server
|
|
807
|
-
}
|
|
798
|
+
// Mint a JWT from the signing key so the CLI can authenticate with the
|
|
799
|
+
// daemon/gateway (which requires auth by default).
|
|
800
|
+
const bearerToken = mintLocalBearerToken(resources.instanceDir);
|
|
808
801
|
|
|
809
802
|
const localEntry: AssistantEntry = {
|
|
810
803
|
assistantId: instanceName,
|
package/src/commands/recover.ts
CHANGED
|
@@ -68,9 +68,7 @@ export async function recover(): Promise<void> {
|
|
|
68
68
|
|
|
69
69
|
// 7. Start daemon + gateway (same as wake)
|
|
70
70
|
await startLocalDaemon(false, entry.resources);
|
|
71
|
-
|
|
72
|
-
await startGateway(false, entry.resources);
|
|
73
|
-
}
|
|
71
|
+
await startGateway(false, entry.resources);
|
|
74
72
|
|
|
75
73
|
console.log(`✅ Recovered assistant '${name}'.`);
|
|
76
74
|
}
|
package/src/commands/wake.ts
CHANGED
|
@@ -3,7 +3,11 @@ import { join } from "path";
|
|
|
3
3
|
|
|
4
4
|
import { resolveTargetAssistant } from "../lib/assistant-config.js";
|
|
5
5
|
import { isProcessAlive, stopProcessByPidFile } from "../lib/process";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
isWatchModeAvailable,
|
|
8
|
+
startLocalDaemon,
|
|
9
|
+
startGateway,
|
|
10
|
+
} from "../lib/local";
|
|
7
11
|
import { maybeStartNgrokTunnel } from "../lib/ngrok";
|
|
8
12
|
|
|
9
13
|
export async function wake(): Promise<void> {
|
|
@@ -56,12 +60,21 @@ export async function wake(): Promise<void> {
|
|
|
56
60
|
process.kill(pid, 0);
|
|
57
61
|
daemonRunning = true;
|
|
58
62
|
if (watch) {
|
|
59
|
-
// Restart in watch mode
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// Restart in watch mode — but only if source files are available.
|
|
64
|
+
// Watch mode requires bun --watch with .ts sources; packaged desktop
|
|
65
|
+
// builds only have a compiled binary. Stopping the daemon without a
|
|
66
|
+
// viable watch-mode path would leave the user with no running assistant.
|
|
67
|
+
if (!isWatchModeAvailable()) {
|
|
68
|
+
console.log(
|
|
69
|
+
`Assistant running (pid ${pid}) — watch mode not available (no source files). Keeping existing process.`,
|
|
70
|
+
);
|
|
71
|
+
} else {
|
|
72
|
+
console.log(
|
|
73
|
+
`Assistant running (pid ${pid}) — restarting in watch mode...`,
|
|
74
|
+
);
|
|
75
|
+
await stopProcessByPidFile(pidFile, "assistant");
|
|
76
|
+
daemonRunning = false;
|
|
77
|
+
}
|
|
65
78
|
} else {
|
|
66
79
|
console.log(`Assistant already running (pid ${pid}).`);
|
|
67
80
|
}
|
|
@@ -82,12 +95,18 @@ export async function wake(): Promise<void> {
|
|
|
82
95
|
const { alive, pid } = isProcessAlive(gatewayPidFile);
|
|
83
96
|
if (alive) {
|
|
84
97
|
if (watch) {
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
// Same guard as the daemon: only restart if watch mode is viable.
|
|
99
|
+
if (!isWatchModeAvailable()) {
|
|
100
|
+
console.log(
|
|
101
|
+
`Gateway running (pid ${pid}) — watch mode not available (no source files). Keeping existing process.`,
|
|
102
|
+
);
|
|
103
|
+
} else {
|
|
104
|
+
console.log(
|
|
105
|
+
`Gateway running (pid ${pid}) — restarting in watch mode...`,
|
|
106
|
+
);
|
|
107
|
+
await stopProcessByPidFile(gatewayPidFile, "gateway");
|
|
108
|
+
await startGateway(watch, resources);
|
|
109
|
+
}
|
|
91
110
|
} else {
|
|
92
111
|
console.log(`Gateway already running (pid ${pid}).`);
|
|
93
112
|
}
|
package/src/lib/jwt.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal JWT minting for the external CLI.
|
|
3
|
+
*
|
|
4
|
+
* Loads the shared HMAC signing key from disk and mints short-lived JWTs
|
|
5
|
+
* so the CLI can authenticate with the daemon's HTTP server without reading
|
|
6
|
+
* the deprecated http-token file.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHmac, randomBytes } from "crypto";
|
|
10
|
+
import { readFileSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
|
|
13
|
+
import { CURRENT_POLICY_EPOCH } from "./policy.js";
|
|
14
|
+
|
|
15
|
+
function base64urlEncode(data: Buffer | string): string {
|
|
16
|
+
const buf = typeof data === "string" ? Buffer.from(data, "utf-8") : data;
|
|
17
|
+
return buf.toString("base64url");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const JWT_HEADER = base64urlEncode(
|
|
21
|
+
JSON.stringify({ alg: "HS256", typ: "JWT" }),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Mint a short-lived JWT bearer token for the given instance directory.
|
|
26
|
+
*
|
|
27
|
+
* Reads the signing key from `<instanceDir>/.vellum/protected/actor-token-signing-key`
|
|
28
|
+
* and mints a 30-day JWT with `aud=vellum-gateway`.
|
|
29
|
+
*
|
|
30
|
+
* Returns undefined if the signing key doesn't exist yet (daemon not started).
|
|
31
|
+
*/
|
|
32
|
+
export function mintLocalBearerToken(instanceDir: string): string | undefined {
|
|
33
|
+
try {
|
|
34
|
+
const keyPath = join(
|
|
35
|
+
instanceDir,
|
|
36
|
+
".vellum",
|
|
37
|
+
"protected",
|
|
38
|
+
"actor-token-signing-key",
|
|
39
|
+
);
|
|
40
|
+
const key = readFileSync(keyPath);
|
|
41
|
+
if (key.length !== 32) return undefined;
|
|
42
|
+
|
|
43
|
+
const now = Math.floor(Date.now() / 1000);
|
|
44
|
+
const claims = {
|
|
45
|
+
iss: "vellum-auth",
|
|
46
|
+
aud: "vellum-gateway",
|
|
47
|
+
sub: "local:cli:cli",
|
|
48
|
+
scope_profile: "actor_client_v1",
|
|
49
|
+
exp: now + 30 * 24 * 60 * 60,
|
|
50
|
+
policy_epoch: CURRENT_POLICY_EPOCH,
|
|
51
|
+
iat: now,
|
|
52
|
+
jti: randomBytes(16).toString("hex"),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const payload = base64urlEncode(JSON.stringify(claims));
|
|
56
|
+
const sigInput = JWT_HEADER + "." + payload;
|
|
57
|
+
const sig = createHmac("sha256", key).update(sigInput).digest();
|
|
58
|
+
return sigInput + "." + base64urlEncode(sig);
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/lib/local.ts
CHANGED
|
@@ -715,6 +715,20 @@ export function getLocalLanIPv4(): string | undefined {
|
|
|
715
715
|
return undefined;
|
|
716
716
|
}
|
|
717
717
|
|
|
718
|
+
/**
|
|
719
|
+
* Check whether watch-mode startup is possible. Watch mode requires source
|
|
720
|
+
* files (bun --watch only works with .ts sources, not compiled binaries).
|
|
721
|
+
* Returns true when assistant source can be resolved, false otherwise.
|
|
722
|
+
*
|
|
723
|
+
* Use this before stopping a running assistant for a watch-mode restart — if
|
|
724
|
+
* watch mode isn't available (e.g. packaged desktop app without source), the
|
|
725
|
+
* caller should keep the existing process alive rather than killing it and
|
|
726
|
+
* failing.
|
|
727
|
+
*/
|
|
728
|
+
export function isWatchModeAvailable(): boolean {
|
|
729
|
+
return resolveAssistantIndexPath() !== undefined;
|
|
730
|
+
}
|
|
731
|
+
|
|
718
732
|
// NOTE: startLocalDaemon() is the CLI-side daemon lifecycle manager.
|
|
719
733
|
// It should eventually converge with
|
|
720
734
|
// assistant/src/daemon/daemon-control.ts::startDaemon which is the
|
|
@@ -789,12 +803,17 @@ export async function startLocalDaemon(
|
|
|
789
803
|
// macOS app the CLI inherits a huge environment (XPC_SERVICE_NAME,
|
|
790
804
|
// __CFBundleIdentifier, CLAUDE_CODE_ENTRYPOINT, etc.) that can cause
|
|
791
805
|
// the daemon to take 50+ seconds to start instead of ~1s.
|
|
792
|
-
const
|
|
806
|
+
const home = homedir();
|
|
807
|
+
const bunBinDir = join(home, ".bun", "bin");
|
|
808
|
+
const localBinDir = join(home, ".local", "bin");
|
|
793
809
|
const basePath =
|
|
794
810
|
process.env.PATH || "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
811
|
+
const extraDirs = [bunBinDir, localBinDir].filter(
|
|
812
|
+
(d) => !basePath.split(":").includes(d),
|
|
813
|
+
);
|
|
795
814
|
const daemonEnv: Record<string, string> = {
|
|
796
|
-
HOME: process.env.HOME ||
|
|
797
|
-
PATH:
|
|
815
|
+
HOME: process.env.HOME || home,
|
|
816
|
+
PATH: [...extraDirs, basePath].filter(Boolean).join(":"),
|
|
798
817
|
};
|
|
799
818
|
// Forward optional config env vars the daemon may need
|
|
800
819
|
for (const key of [
|
|
@@ -11,9 +11,12 @@ import { join, dirname } from "path";
|
|
|
11
11
|
|
|
12
12
|
const DEFAULT_PLATFORM_URL = "https://platform.vellum.ai";
|
|
13
13
|
|
|
14
|
+
function getXdgConfigHome(): string {
|
|
15
|
+
return process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config");
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
function getPlatformTokenPath(): string {
|
|
15
|
-
|
|
16
|
-
return join(base, ".vellum", "platform-token");
|
|
19
|
+
return join(getXdgConfigHome(), "vellum", "platform-token");
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export function getPlatformUrl(): string {
|