@vellumai/cli 0.1.10 → 0.1.12

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.
@@ -0,0 +1,115 @@
1
+ import { useRef, useState, type ReactElement } from "react";
2
+ import chalk from "chalk";
3
+ import { Text, useInput } from "ink";
4
+
5
+ interface TextInputProps {
6
+ value: string;
7
+ onChange: (value: string) => void;
8
+ onSubmit?: (value: string) => void;
9
+ focus?: boolean;
10
+ placeholder?: string;
11
+ }
12
+
13
+ function TextInput({
14
+ value,
15
+ onChange,
16
+ onSubmit,
17
+ focus = true,
18
+ placeholder = "",
19
+ }: TextInputProps): ReactElement {
20
+ const cursorOffsetRef = useRef(value.length);
21
+ const valueRef = useRef(value);
22
+
23
+ valueRef.current = value;
24
+
25
+ if (cursorOffsetRef.current > value.length) {
26
+ cursorOffsetRef.current = value.length;
27
+ }
28
+
29
+ const [, setRenderTick] = useState(0);
30
+
31
+ useInput(
32
+ (input, key) => {
33
+ if (
34
+ key.upArrow ||
35
+ key.downArrow ||
36
+ (key.ctrl && input === "c") ||
37
+ key.tab ||
38
+ (key.shift && key.tab)
39
+ ) {
40
+ return;
41
+ }
42
+
43
+ if (key.return) {
44
+ onSubmit?.(valueRef.current);
45
+ return;
46
+ }
47
+
48
+ const currentValue = valueRef.current;
49
+ const currentOffset = cursorOffsetRef.current;
50
+ let nextValue = currentValue;
51
+ let nextOffset = currentOffset;
52
+
53
+ if (key.leftArrow) {
54
+ nextOffset = Math.max(0, currentOffset - 1);
55
+ } else if (key.rightArrow) {
56
+ nextOffset = Math.min(currentValue.length, currentOffset + 1);
57
+ } else if (key.backspace || key.delete) {
58
+ if (currentOffset > 0) {
59
+ nextValue = currentValue.slice(0, currentOffset - 1) + currentValue.slice(currentOffset);
60
+ nextOffset = currentOffset - 1;
61
+ }
62
+ } else {
63
+ nextValue =
64
+ currentValue.slice(0, currentOffset) + input + currentValue.slice(currentOffset);
65
+ nextOffset = currentOffset + input.length;
66
+ }
67
+
68
+ cursorOffsetRef.current = nextOffset;
69
+
70
+ if (nextValue !== currentValue) {
71
+ valueRef.current = nextValue;
72
+ onChange(nextValue);
73
+ }
74
+
75
+ setRenderTick((t) => t + 1);
76
+ },
77
+ { isActive: focus },
78
+ );
79
+
80
+ const cursorOffset = cursorOffsetRef.current;
81
+ let renderedValue: string;
82
+ let renderedPlaceholder: string | undefined;
83
+
84
+ if (focus) {
85
+ renderedPlaceholder =
86
+ placeholder.length > 0
87
+ ? chalk.inverse(placeholder[0]) + chalk.grey(placeholder.slice(1))
88
+ : chalk.inverse(" ");
89
+
90
+ if (value.length > 0) {
91
+ renderedValue = "";
92
+ let i = 0;
93
+ for (const char of value) {
94
+ renderedValue += i === cursorOffset ? chalk.inverse(char) : char;
95
+ i++;
96
+ }
97
+ if (cursorOffset === value.length) {
98
+ renderedValue += chalk.inverse(" ");
99
+ }
100
+ } else {
101
+ renderedValue = chalk.inverse(" ");
102
+ }
103
+ } else {
104
+ renderedValue = value;
105
+ renderedPlaceholder = placeholder ? chalk.grey(placeholder) : undefined;
106
+ }
107
+
108
+ return (
109
+ <Text>
110
+ {placeholder ? (value.length > 0 ? renderedValue : renderedPlaceholder) : renderedValue}
111
+ </Text>
112
+ );
113
+ }
114
+
115
+ export default TextInput;
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ import { client } from "./commands/client";
3
4
  import { hatch } from "./commands/hatch";
4
5
  import { ps } from "./commands/ps";
5
6
  import { retire } from "./commands/retire";
@@ -7,6 +8,7 @@ import { sleep } from "./commands/sleep";
7
8
  import { wake } from "./commands/wake";
8
9
 
9
10
  const commands = {
11
+ client,
10
12
  hatch,
11
13
  ps,
12
14
  retire,
@@ -24,8 +26,9 @@ async function main() {
24
26
  console.log("Usage: vellum-cli <command> [options]");
25
27
  console.log("");
26
28
  console.log("Commands:");
29
+ console.log(" client Connect to a hatched assistant");
27
30
  console.log(" hatch Create a new assistant instance");
28
- console.log(" ps List assistants and their health status");
31
+ console.log(" ps List assistants (or processes for a specific assistant)");
29
32
  console.log(" retire Delete an assistant instance");
30
33
  console.log(" sleep Stop the daemon process");
31
34
  console.log(" wake Start the daemon and gateway");
@@ -1,5 +1,5 @@
1
1
  export const FIREWALL_TAG = "vellum-assistant";
2
- export const GATEWAY_PORT = 7830;
2
+ export const GATEWAY_PORT = process.env.GATEWAY_PORT ? Number(process.env.GATEWAY_PORT) : 7830;
3
3
  export const VALID_REMOTE_HOSTS = ["local", "gcp", "aws", "custom"] as const;
4
4
  export type RemoteHost = (typeof VALID_REMOTE_HOSTS)[number];
5
5
  export const VALID_SPECIES = ["openclaw", "vellum"] as const;
@@ -0,0 +1,127 @@
1
+ const DOCTOR_URL = process.env.DOCTOR_URL || "https://doctor.vellum.ai";
2
+
3
+ export type ProgressPhase= "invoking_prompt" | "calling_tool" | "processing_tool_result";
4
+
5
+ export interface ProgressEvent {
6
+ phase: ProgressPhase;
7
+ toolName?: string;
8
+ }
9
+
10
+ interface DoctorResult {
11
+ assistantId: string;
12
+ diagnostics: string | null;
13
+ recommendation: string | null;
14
+ error: string | null;
15
+ }
16
+
17
+ export interface ChatLogEntry {
18
+ role: "user" | "assistant" | "error";
19
+ content: string;
20
+ }
21
+
22
+ type DoctorProgressCallback = (event: ProgressEvent) => void;
23
+ type DoctorLogCallback = (message: string) => void;
24
+
25
+ async function streamDoctorResponse(
26
+ response: globalThis.Response,
27
+ onProgress?: DoctorProgressCallback,
28
+ onLog?: DoctorLogCallback,
29
+ ): Promise<DoctorResult> {
30
+ if (!response.body) {
31
+ throw new Error(
32
+ `No response body from doctor (HTTP ${response.status} ${response.statusText})`,
33
+ );
34
+ }
35
+
36
+ let result: DoctorResult | null = null;
37
+ const decoder = new TextDecoder();
38
+ let buffer = "";
39
+ let chunkCount = 0;
40
+ const receivedEventTypes: string[] = [];
41
+
42
+ try {
43
+ for await (const chunk of response.body) {
44
+ chunkCount++;
45
+ buffer += decoder.decode(chunk, { stream: true });
46
+ const lines = buffer.split("\n");
47
+ buffer = lines.pop() ?? "";
48
+
49
+ for (const line of lines) {
50
+ if (!line.trim()) continue;
51
+ const parsed = JSON.parse(line) as { type: string } & Record<string, unknown>;
52
+ receivedEventTypes.push(parsed.type);
53
+ if (parsed.type === "progress") {
54
+ onProgress?.(parsed as unknown as ProgressEvent);
55
+ } else if (parsed.type === "log") {
56
+ onLog?.((parsed as unknown as { message: string }).message);
57
+ } else if (parsed.type === "result") {
58
+ result = parsed as unknown as DoctorResult;
59
+ }
60
+ }
61
+ }
62
+ } catch (streamErr) {
63
+ const detail = streamErr instanceof Error ? streamErr.message : String(streamErr);
64
+ throw new Error(
65
+ `Doctor stream interrupted after ${chunkCount} chunks ` +
66
+ `(received events: [${receivedEventTypes.join(", ")}]): ${detail}`,
67
+ );
68
+ }
69
+
70
+ if (buffer.trim()) {
71
+ const parsed = JSON.parse(buffer) as { type: string } & Record<string, unknown>;
72
+ receivedEventTypes.push(parsed.type);
73
+ if (parsed.type === "result") {
74
+ result = parsed as unknown as DoctorResult;
75
+ }
76
+ }
77
+
78
+ if (!result) {
79
+ throw new Error(
80
+ `No result received from doctor. ` +
81
+ `HTTP ${response.status}, ${chunkCount} chunks read, ` +
82
+ `events received: [${receivedEventTypes.join(", ")}], ` +
83
+ `trailing buffer: ${buffer.trim() ? JSON.stringify(buffer.trim().slice(0, 200)) : "(empty)"}`,
84
+ );
85
+ }
86
+
87
+ return result;
88
+ }
89
+
90
+ async function callDoctorDaemon(
91
+ assistantId: string,
92
+ project?: string,
93
+ zone?: string,
94
+ userPrompt?: string,
95
+ onProgress?: DoctorProgressCallback,
96
+ sessionId?: string,
97
+ chatContext?: ChatLogEntry[],
98
+ onLog?: DoctorLogCallback,
99
+ ): Promise<DoctorResult> {
100
+ const MAX_RETRIES = 2;
101
+ let lastError: unknown;
102
+
103
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
104
+ try {
105
+ const response = await fetch(`${DOCTOR_URL}/doctor`, {
106
+ method: "POST",
107
+ headers: { "Content-Type": "application/json" },
108
+ body: JSON.stringify({ assistantId, project, zone, userPrompt, sessionId, chatContext }),
109
+ });
110
+ return await streamDoctorResponse(response, onProgress, onLog);
111
+ } catch (err) {
112
+ lastError = err;
113
+ const errMsg = err instanceof Error ? err.message : String(err);
114
+ const logMsg =
115
+ `[doctor-client] Attempt ${attempt + 1}/${MAX_RETRIES} failed: ${errMsg}`;
116
+ onLog?.(logMsg);
117
+ if (attempt < MAX_RETRIES - 1) {
118
+ await new Promise((resolve) => setTimeout(resolve, 500));
119
+ }
120
+ }
121
+ }
122
+
123
+ throw lastError;
124
+ }
125
+
126
+ export { callDoctorDaemon };
127
+ export type { DoctorProgressCallback, DoctorResult };