@vellumai/cli 0.4.5 → 0.4.7
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 +52 -5
- package/src/commands/hatch.ts +17 -2
- package/src/components/DefaultMainScreen.tsx +97 -13
- package/src/lib/local.ts +65 -21
package/package.json
CHANGED
package/src/adapters/install.sh
CHANGED
|
@@ -24,11 +24,58 @@ ensure_git() {
|
|
|
24
24
|
|
|
25
25
|
info "Installing git..."
|
|
26
26
|
if [ "$(uname -s)" = "Darwin" ]; then
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
# On macOS, the standard way to get git is via Xcode Command Line Tools.
|
|
28
|
+
# Try installing CLT first before falling back to Homebrew.
|
|
29
|
+
if ! xcode-select -p >/dev/null 2>&1; then
|
|
30
|
+
info "Installing Xcode Command Line Tools (includes git)..."
|
|
31
|
+
|
|
32
|
+
# Use softwareupdate to install CLT non-interactively instead of
|
|
33
|
+
# xcode-select --install which opens a GUI dialog requiring manual
|
|
34
|
+
# confirmation.
|
|
35
|
+
touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress
|
|
36
|
+
local clt_package
|
|
37
|
+
clt_package=$(softwareupdate -l 2>/dev/null \
|
|
38
|
+
| grep -o '.*Command Line Tools.*' \
|
|
39
|
+
| grep -v "^\*" \
|
|
40
|
+
| sed 's/^ *//' \
|
|
41
|
+
| sort -V \
|
|
42
|
+
| tail -1)
|
|
43
|
+
|
|
44
|
+
if [ -n "$clt_package" ]; then
|
|
45
|
+
info "Found package: $clt_package"
|
|
46
|
+
softwareupdate -i "$clt_package" --verbose 2>&1 | while IFS= read -r line; do
|
|
47
|
+
printf " %s\n" "$line"
|
|
48
|
+
done
|
|
49
|
+
else
|
|
50
|
+
# Fallback: if softwareupdate can't find the package, try
|
|
51
|
+
# xcode-select --install and wait for user interaction.
|
|
52
|
+
info "Could not find CLT package via softwareupdate, falling back to xcode-select..."
|
|
53
|
+
xcode-select --install 2>/dev/null || true
|
|
54
|
+
info "Please follow the on-screen dialog to install. Waiting..."
|
|
55
|
+
local waited=0
|
|
56
|
+
while ! xcode-select -p >/dev/null 2>&1; do
|
|
57
|
+
sleep 5
|
|
58
|
+
waited=$((waited + 5))
|
|
59
|
+
if [ "$waited" -ge 600 ]; then
|
|
60
|
+
error "Timed out waiting for Xcode Command Line Tools. Please install manually and re-run."
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
done
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
rm -f /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress
|
|
67
|
+
hash -r 2>/dev/null || true
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# If git still doesn't work after CLT, try Homebrew as a fallback.
|
|
71
|
+
if ! git --version >/dev/null 2>&1; then
|
|
72
|
+
hash -r 2>/dev/null || true
|
|
73
|
+
if command -v brew >/dev/null 2>&1; then
|
|
74
|
+
brew install git
|
|
75
|
+
else
|
|
76
|
+
error "git is still not available. Please install manually: xcode-select --install"
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
32
79
|
fi
|
|
33
80
|
elif command -v apt-get >/dev/null 2>&1; then
|
|
34
81
|
sudo apt-get update -qq && sudo apt-get install -y -qq git
|
package/src/commands/hatch.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
2
|
-
import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync } from "fs";
|
|
2
|
+
import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync } from "fs";
|
|
3
3
|
import { homedir, hostname, userInfo } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
|
|
6
|
+
import QRCode from "qrcode";
|
|
6
7
|
import qrcode from "qrcode-terminal";
|
|
7
8
|
|
|
8
9
|
// Direct import — bun embeds this at compile time so it works in compiled binaries.
|
|
@@ -508,7 +509,7 @@ async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | un
|
|
|
508
509
|
const pairingRequestId = randomUUID();
|
|
509
510
|
const pairingSecret = randomBytes(32).toString("hex");
|
|
510
511
|
|
|
511
|
-
const registerRes = await fetch(`${runtimeUrl}/
|
|
512
|
+
const registerRes = await fetch(`${runtimeUrl}/pairing/register`, {
|
|
512
513
|
method: "POST",
|
|
513
514
|
headers: {
|
|
514
515
|
"Content-Type": "application/json",
|
|
@@ -539,6 +540,20 @@ async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | un
|
|
|
539
540
|
});
|
|
540
541
|
});
|
|
541
542
|
|
|
543
|
+
// Save QR code as PNG to a well-known location so it can be retrieved
|
|
544
|
+
// (e.g. via SCP) for pairing through the Desktop app.
|
|
545
|
+
const qrDir = join(homedir(), ".vellum", "pairing-qr");
|
|
546
|
+
mkdirSync(qrDir, { recursive: true });
|
|
547
|
+
const qrPngPath = join(qrDir, "initial.png");
|
|
548
|
+
try {
|
|
549
|
+
const pngBuffer = await QRCode.toBuffer(payload, { type: "png", width: 512 });
|
|
550
|
+
writeFileSync(qrPngPath, pngBuffer);
|
|
551
|
+
console.log(`QR code PNG saved to ${qrPngPath}\n`);
|
|
552
|
+
} catch (pngErr) {
|
|
553
|
+
const pngReason = pngErr instanceof Error ? pngErr.message : String(pngErr);
|
|
554
|
+
console.warn(`\u26A0 Could not save QR code PNG: ${pngReason}\n`);
|
|
555
|
+
}
|
|
556
|
+
|
|
542
557
|
console.log("Scan this QR code with the Vellum iOS app to pair:\n");
|
|
543
558
|
console.log(qrString);
|
|
544
559
|
console.log("This pairing request expires in 5 minutes.");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
3
|
-
import { hostname, userInfo } from "os";
|
|
3
|
+
import { hostname, platform, userInfo } from "os";
|
|
4
4
|
import { basename } from "path";
|
|
5
5
|
import qrcode from "qrcode-terminal";
|
|
6
6
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from "react";
|
|
@@ -135,6 +135,7 @@ async function runtimeRequest<T>(
|
|
|
135
135
|
path: string,
|
|
136
136
|
init?: RequestInit,
|
|
137
137
|
bearerToken?: string,
|
|
138
|
+
actorToken?: string,
|
|
138
139
|
): Promise<T> {
|
|
139
140
|
const url = `${baseUrl}/v1/assistants/${assistantId}${path}`;
|
|
140
141
|
const response = await fetch(url, {
|
|
@@ -142,6 +143,7 @@ async function runtimeRequest<T>(
|
|
|
142
143
|
headers: {
|
|
143
144
|
"Content-Type": "application/json",
|
|
144
145
|
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
|
|
146
|
+
...(actorToken ? { "X-Actor-Token": actorToken } : {}),
|
|
145
147
|
...(init?.headers as Record<string, string> | undefined),
|
|
146
148
|
},
|
|
147
149
|
});
|
|
@@ -177,10 +179,47 @@ async function checkHealthRuntime(baseUrl: string): Promise<HealthResponse> {
|
|
|
177
179
|
return response.json() as Promise<HealthResponse>;
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
async function bootstrapActorToken(baseUrl: string, bearerToken?: string): Promise<string> {
|
|
183
|
+
if (!bearerToken) {
|
|
184
|
+
throw new Error("Missing bearer token; cannot bootstrap actor identity");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const deviceId = `vellum-cli:${platform()}:${hostname()}:${userInfo().username}`;
|
|
188
|
+
const url = `${baseUrl}/v1/integrations/guardian/vellum/bootstrap`;
|
|
189
|
+
|
|
190
|
+
const response = await fetch(url, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers: {
|
|
193
|
+
"Content-Type": "application/json",
|
|
194
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
195
|
+
},
|
|
196
|
+
body: JSON.stringify({ platform: "cli", deviceId }),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
const body = await response.text().catch(() => "");
|
|
201
|
+
throw new Error(`HTTP ${response.status}: ${body || response.statusText}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const json: unknown = await response.json();
|
|
205
|
+
|
|
206
|
+
if (typeof json !== "object" || json === null) {
|
|
207
|
+
throw new Error("Invalid bootstrap response from gateway/runtime");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const actorToken = (json as Record<string, unknown>).actorToken;
|
|
211
|
+
if (typeof actorToken !== "string" || actorToken.length === 0) {
|
|
212
|
+
throw new Error("Invalid bootstrap response from gateway/runtime");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return actorToken;
|
|
216
|
+
}
|
|
217
|
+
|
|
180
218
|
async function pollMessages(
|
|
181
219
|
baseUrl: string,
|
|
182
220
|
assistantId: string,
|
|
183
221
|
bearerToken?: string,
|
|
222
|
+
actorToken?: string,
|
|
184
223
|
): Promise<ListMessagesResponse> {
|
|
185
224
|
const params = new URLSearchParams({ conversationKey: assistantId });
|
|
186
225
|
return runtimeRequest<ListMessagesResponse>(
|
|
@@ -189,6 +228,7 @@ async function pollMessages(
|
|
|
189
228
|
`/messages?${params.toString()}`,
|
|
190
229
|
undefined,
|
|
191
230
|
bearerToken,
|
|
231
|
+
actorToken,
|
|
192
232
|
);
|
|
193
233
|
}
|
|
194
234
|
|
|
@@ -198,6 +238,7 @@ async function sendMessage(
|
|
|
198
238
|
content: string,
|
|
199
239
|
signal?: AbortSignal,
|
|
200
240
|
bearerToken?: string,
|
|
241
|
+
actorToken?: string,
|
|
201
242
|
): Promise<SendMessageResponse> {
|
|
202
243
|
return runtimeRequest<SendMessageResponse>(
|
|
203
244
|
baseUrl,
|
|
@@ -209,6 +250,7 @@ async function sendMessage(
|
|
|
209
250
|
signal,
|
|
210
251
|
},
|
|
211
252
|
bearerToken,
|
|
253
|
+
actorToken,
|
|
212
254
|
);
|
|
213
255
|
}
|
|
214
256
|
|
|
@@ -218,6 +260,7 @@ async function submitDecision(
|
|
|
218
260
|
requestId: string,
|
|
219
261
|
decision: "allow" | "deny",
|
|
220
262
|
bearerToken?: string,
|
|
263
|
+
actorToken?: string,
|
|
221
264
|
): Promise<SubmitDecisionResponse> {
|
|
222
265
|
return runtimeRequest<SubmitDecisionResponse>(
|
|
223
266
|
baseUrl,
|
|
@@ -228,6 +271,7 @@ async function submitDecision(
|
|
|
228
271
|
body: JSON.stringify({ requestId, decision }),
|
|
229
272
|
},
|
|
230
273
|
bearerToken,
|
|
274
|
+
actorToken,
|
|
231
275
|
);
|
|
232
276
|
}
|
|
233
277
|
|
|
@@ -239,6 +283,7 @@ async function addTrustRule(
|
|
|
239
283
|
scope: string,
|
|
240
284
|
decision: "allow" | "deny",
|
|
241
285
|
bearerToken?: string,
|
|
286
|
+
actorToken?: string,
|
|
242
287
|
): Promise<AddTrustRuleResponse> {
|
|
243
288
|
return runtimeRequest<AddTrustRuleResponse>(
|
|
244
289
|
baseUrl,
|
|
@@ -249,6 +294,7 @@ async function addTrustRule(
|
|
|
249
294
|
body: JSON.stringify({ requestId, pattern, scope, decision }),
|
|
250
295
|
},
|
|
251
296
|
bearerToken,
|
|
297
|
+
actorToken,
|
|
252
298
|
);
|
|
253
299
|
}
|
|
254
300
|
|
|
@@ -256,6 +302,7 @@ async function pollPendingInteractions(
|
|
|
256
302
|
baseUrl: string,
|
|
257
303
|
assistantId: string,
|
|
258
304
|
bearerToken?: string,
|
|
305
|
+
actorToken?: string,
|
|
259
306
|
): Promise<PendingInteractionsResponse> {
|
|
260
307
|
const params = new URLSearchParams({ conversationKey: assistantId });
|
|
261
308
|
return runtimeRequest<PendingInteractionsResponse>(
|
|
@@ -264,6 +311,7 @@ async function pollPendingInteractions(
|
|
|
264
311
|
`/pending-interactions?${params.toString()}`,
|
|
265
312
|
undefined,
|
|
266
313
|
bearerToken,
|
|
314
|
+
actorToken,
|
|
267
315
|
);
|
|
268
316
|
}
|
|
269
317
|
|
|
@@ -301,6 +349,7 @@ async function handleConfirmationPrompt(
|
|
|
301
349
|
confirmation: PendingConfirmation,
|
|
302
350
|
chatApp: ChatAppHandle,
|
|
303
351
|
bearerToken?: string,
|
|
352
|
+
actorToken?: string,
|
|
304
353
|
): Promise<void> {
|
|
305
354
|
const preview = formatConfirmationPreview(confirmation.toolName, confirmation.input);
|
|
306
355
|
const allowlistOptions = confirmation.allowlistOptions ?? [];
|
|
@@ -320,7 +369,7 @@ async function handleConfirmationPrompt(
|
|
|
320
369
|
const index = await chatApp.showSelection("Tool Approval", options);
|
|
321
370
|
|
|
322
371
|
if (index === 0) {
|
|
323
|
-
await submitDecision(baseUrl, assistantId, requestId, "allow", bearerToken);
|
|
372
|
+
await submitDecision(baseUrl, assistantId, requestId, "allow", bearerToken, actorToken);
|
|
324
373
|
chatApp.addStatus("\u2714 Allowed", "green");
|
|
325
374
|
return;
|
|
326
375
|
}
|
|
@@ -333,6 +382,7 @@ async function handleConfirmationPrompt(
|
|
|
333
382
|
chatApp,
|
|
334
383
|
"always_allow",
|
|
335
384
|
bearerToken,
|
|
385
|
+
actorToken,
|
|
336
386
|
);
|
|
337
387
|
return;
|
|
338
388
|
}
|
|
@@ -345,11 +395,12 @@ async function handleConfirmationPrompt(
|
|
|
345
395
|
chatApp,
|
|
346
396
|
"always_deny",
|
|
347
397
|
bearerToken,
|
|
398
|
+
actorToken,
|
|
348
399
|
);
|
|
349
400
|
return;
|
|
350
401
|
}
|
|
351
402
|
|
|
352
|
-
await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken);
|
|
403
|
+
await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken, actorToken);
|
|
353
404
|
chatApp.addStatus("\u2718 Denied", "yellow");
|
|
354
405
|
}
|
|
355
406
|
|
|
@@ -361,6 +412,7 @@ async function handlePatternSelection(
|
|
|
361
412
|
chatApp: ChatAppHandle,
|
|
362
413
|
trustDecision: TrustDecision,
|
|
363
414
|
bearerToken?: string,
|
|
415
|
+
actorToken?: string,
|
|
364
416
|
): Promise<void> {
|
|
365
417
|
const allowlistOptions = confirmation.allowlistOptions ?? [];
|
|
366
418
|
const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
|
|
@@ -379,11 +431,12 @@ async function handlePatternSelection(
|
|
|
379
431
|
selectedPattern,
|
|
380
432
|
trustDecision,
|
|
381
433
|
bearerToken,
|
|
434
|
+
actorToken,
|
|
382
435
|
);
|
|
383
436
|
return;
|
|
384
437
|
}
|
|
385
438
|
|
|
386
|
-
await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken);
|
|
439
|
+
await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken, actorToken);
|
|
387
440
|
chatApp.addStatus("\u2718 Denied", "yellow");
|
|
388
441
|
}
|
|
389
442
|
|
|
@@ -396,6 +449,7 @@ async function handleScopeSelection(
|
|
|
396
449
|
selectedPattern: string,
|
|
397
450
|
trustDecision: TrustDecision,
|
|
398
451
|
bearerToken?: string,
|
|
452
|
+
actorToken?: string,
|
|
399
453
|
): Promise<void> {
|
|
400
454
|
const scopeOptions = confirmation.scopeOptions ?? [];
|
|
401
455
|
const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
|
|
@@ -413,6 +467,7 @@ async function handleScopeSelection(
|
|
|
413
467
|
scopeOptions[index].scope,
|
|
414
468
|
ruleDecision,
|
|
415
469
|
bearerToken,
|
|
470
|
+
actorToken,
|
|
416
471
|
);
|
|
417
472
|
await submitDecision(
|
|
418
473
|
baseUrl,
|
|
@@ -420,6 +475,7 @@ async function handleScopeSelection(
|
|
|
420
475
|
requestId,
|
|
421
476
|
ruleDecision === "deny" ? "deny" : "allow",
|
|
422
477
|
bearerToken,
|
|
478
|
+
actorToken,
|
|
423
479
|
);
|
|
424
480
|
const ruleLabel = trustDecision === "always_deny" ? "Denylisted" : "Allowlisted";
|
|
425
481
|
const ruleColor = trustDecision === "always_deny" ? "yellow" : "green";
|
|
@@ -430,7 +486,7 @@ async function handleScopeSelection(
|
|
|
430
486
|
return;
|
|
431
487
|
}
|
|
432
488
|
|
|
433
|
-
await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken);
|
|
489
|
+
await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken, actorToken);
|
|
434
490
|
chatApp.addStatus("\u2718 Denied", "yellow");
|
|
435
491
|
}
|
|
436
492
|
|
|
@@ -1025,6 +1081,7 @@ function ChatApp({
|
|
|
1025
1081
|
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
1026
1082
|
const doctorSessionIdRef = useRef(randomUUID());
|
|
1027
1083
|
const handleRef_ = useRef<ChatAppHandle | null>(null);
|
|
1084
|
+
const actorTokenRef = useRef<string | undefined>(undefined);
|
|
1028
1085
|
|
|
1029
1086
|
const { stdout } = useStdout();
|
|
1030
1087
|
const terminalRows = stdout.rows || DEFAULT_TERMINAL_ROWS;
|
|
@@ -1250,6 +1307,9 @@ function ChatApp({
|
|
|
1250
1307
|
|
|
1251
1308
|
try {
|
|
1252
1309
|
const health = await checkHealthRuntime(runtimeUrl);
|
|
1310
|
+
if (!actorTokenRef.current) {
|
|
1311
|
+
actorTokenRef.current = await bootstrapActorToken(runtimeUrl, bearerToken);
|
|
1312
|
+
}
|
|
1253
1313
|
h.hideSpinner();
|
|
1254
1314
|
h.updateHealthStatus(health.status);
|
|
1255
1315
|
if (health.status === "healthy" || health.status === "ok") {
|
|
@@ -1265,7 +1325,12 @@ function ChatApp({
|
|
|
1265
1325
|
h.showSpinner("Loading conversation history...");
|
|
1266
1326
|
|
|
1267
1327
|
try {
|
|
1268
|
-
const historyResponse = await pollMessages(
|
|
1328
|
+
const historyResponse = await pollMessages(
|
|
1329
|
+
runtimeUrl,
|
|
1330
|
+
assistantId,
|
|
1331
|
+
bearerToken,
|
|
1332
|
+
actorTokenRef.current,
|
|
1333
|
+
);
|
|
1269
1334
|
h.hideSpinner();
|
|
1270
1335
|
if (historyResponse.messages.length > 0) {
|
|
1271
1336
|
for (const msg of historyResponse.messages) {
|
|
@@ -1279,7 +1344,12 @@ function ChatApp({
|
|
|
1279
1344
|
|
|
1280
1345
|
pollTimerRef.current = setInterval(async () => {
|
|
1281
1346
|
try {
|
|
1282
|
-
const response = await pollMessages(
|
|
1347
|
+
const response = await pollMessages(
|
|
1348
|
+
runtimeUrl,
|
|
1349
|
+
assistantId,
|
|
1350
|
+
bearerToken,
|
|
1351
|
+
actorTokenRef.current,
|
|
1352
|
+
);
|
|
1283
1353
|
for (const msg of response.messages) {
|
|
1284
1354
|
if (!seenMessageIdsRef.current.has(msg.id)) {
|
|
1285
1355
|
seenMessageIdsRef.current.add(msg.id);
|
|
@@ -1296,11 +1366,12 @@ function ChatApp({
|
|
|
1296
1366
|
connectedRef.current = true;
|
|
1297
1367
|
connectingRef.current = false;
|
|
1298
1368
|
return true;
|
|
1299
|
-
} catch {
|
|
1369
|
+
} catch (err) {
|
|
1300
1370
|
h.hideSpinner();
|
|
1301
1371
|
connectingRef.current = false;
|
|
1302
1372
|
h.updateHealthStatus("unreachable");
|
|
1303
|
-
|
|
1373
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1374
|
+
h.addStatus(`${statusEmoji("unreachable")} Failed to connect: ${msg}`, "red");
|
|
1304
1375
|
return false;
|
|
1305
1376
|
}
|
|
1306
1377
|
}, [runtimeUrl, assistantId, bearerToken]);
|
|
@@ -1347,8 +1418,8 @@ function ChatApp({
|
|
|
1347
1418
|
const pairingSecret = randomBytes(32).toString("hex");
|
|
1348
1419
|
const gatewayUrl = runtimeUrl;
|
|
1349
1420
|
|
|
1350
|
-
// Call /
|
|
1351
|
-
const registerUrl = `${runtimeUrl}/
|
|
1421
|
+
// Call /pairing/register on the gateway (dedicated pairing proxy route)
|
|
1422
|
+
const registerUrl = `${runtimeUrl}/pairing/register`;
|
|
1352
1423
|
const registerRes = await fetch(registerUrl, {
|
|
1353
1424
|
method: "POST",
|
|
1354
1425
|
headers: {
|
|
@@ -1561,6 +1632,7 @@ function ChatApp({
|
|
|
1561
1632
|
trimmed,
|
|
1562
1633
|
controller.signal,
|
|
1563
1634
|
bearerToken,
|
|
1635
|
+
actorTokenRef.current,
|
|
1564
1636
|
);
|
|
1565
1637
|
clearTimeout(timeoutId);
|
|
1566
1638
|
if (!sendResult.accepted) {
|
|
@@ -1586,7 +1658,12 @@ function ChatApp({
|
|
|
1586
1658
|
|
|
1587
1659
|
// Check for pending confirmations/secrets
|
|
1588
1660
|
try {
|
|
1589
|
-
const pending = await pollPendingInteractions(
|
|
1661
|
+
const pending = await pollPendingInteractions(
|
|
1662
|
+
runtimeUrl,
|
|
1663
|
+
assistantId,
|
|
1664
|
+
bearerToken,
|
|
1665
|
+
actorTokenRef.current,
|
|
1666
|
+
);
|
|
1590
1667
|
|
|
1591
1668
|
if (pending.pendingConfirmation) {
|
|
1592
1669
|
h.hideSpinner();
|
|
@@ -1597,6 +1674,7 @@ function ChatApp({
|
|
|
1597
1674
|
pending.pendingConfirmation,
|
|
1598
1675
|
h,
|
|
1599
1676
|
bearerToken,
|
|
1677
|
+
actorTokenRef.current,
|
|
1600
1678
|
);
|
|
1601
1679
|
h.showSpinner("Working...");
|
|
1602
1680
|
continue;
|
|
@@ -1615,6 +1693,7 @@ function ChatApp({
|
|
|
1615
1693
|
body: JSON.stringify({ requestId: secretRequestId, value, delivery }),
|
|
1616
1694
|
},
|
|
1617
1695
|
bearerToken,
|
|
1696
|
+
actorTokenRef.current,
|
|
1618
1697
|
);
|
|
1619
1698
|
});
|
|
1620
1699
|
h.showSpinner("Working...");
|
|
@@ -1626,7 +1705,12 @@ function ChatApp({
|
|
|
1626
1705
|
|
|
1627
1706
|
// Poll for new messages to detect completion
|
|
1628
1707
|
try {
|
|
1629
|
-
const pollResult = await pollMessages(
|
|
1708
|
+
const pollResult = await pollMessages(
|
|
1709
|
+
runtimeUrl,
|
|
1710
|
+
assistantId,
|
|
1711
|
+
bearerToken,
|
|
1712
|
+
actorTokenRef.current,
|
|
1713
|
+
);
|
|
1630
1714
|
for (const msg of pollResult.messages) {
|
|
1631
1715
|
if (!seenMessageIdsRef.current.has(msg.id)) {
|
|
1632
1716
|
seenMessageIdsRef.current.add(msg.id);
|
package/src/lib/local.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync, spawn } from "child_process";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
3
3
|
import { createRequire } from "module";
|
|
4
4
|
import { createConnection } from "net";
|
|
5
5
|
import { homedir } from "os";
|
|
@@ -11,6 +11,33 @@ import { stopProcessByPidFile } from "./process.js";
|
|
|
11
11
|
|
|
12
12
|
const _require = createRequire(import.meta.url);
|
|
13
13
|
|
|
14
|
+
/** Returns the XDG-compatible log directory for Vellum hatch logs. */
|
|
15
|
+
function getHatchLogDir(): string {
|
|
16
|
+
const configHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
17
|
+
return join(configHome, "vellum", "logs");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Open (or create) a log file in append mode, returning the file descriptor.
|
|
21
|
+
* Creates the parent directory if it doesn't exist. Returns "ignore" if the
|
|
22
|
+
* directory or file cannot be created (permissions, read-only filesystem, etc.)
|
|
23
|
+
* so that spawn falls back to discarding output instead of aborting startup. */
|
|
24
|
+
function openHatchLogFile(name: string): number | "ignore" {
|
|
25
|
+
try {
|
|
26
|
+
const dir = getHatchLogDir();
|
|
27
|
+
mkdirSync(dir, { recursive: true });
|
|
28
|
+
return openSync(join(dir, name), "a");
|
|
29
|
+
} catch {
|
|
30
|
+
return "ignore";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Close a file descriptor returned by openHatchLogFile (no-op for "ignore"). */
|
|
35
|
+
function closeHatchLogFile(fd: number | "ignore"): void {
|
|
36
|
+
if (typeof fd === "number") {
|
|
37
|
+
try { closeSync(fd); } catch { /* best-effort */ }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
14
41
|
function isAssistantSourceDir(dir: string): boolean {
|
|
15
42
|
const pkgPath = join(dir, "package.json");
|
|
16
43
|
if (!existsSync(pkgPath) || !existsSync(join(dir, "src", "index.ts"))) return false;
|
|
@@ -376,18 +403,25 @@ export async function startLocalDaemon(): Promise<void> {
|
|
|
376
403
|
}
|
|
377
404
|
}
|
|
378
405
|
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
406
|
+
const daemonLogFd = openHatchLogFile("daemon.log");
|
|
407
|
+
let daemonPid: number | undefined;
|
|
408
|
+
try {
|
|
409
|
+
const child = spawn(daemonBinary, [], {
|
|
410
|
+
cwd: dirname(daemonBinary),
|
|
411
|
+
detached: true,
|
|
412
|
+
stdio: ["ignore", daemonLogFd, daemonLogFd],
|
|
413
|
+
env: daemonEnv,
|
|
414
|
+
});
|
|
415
|
+
child.unref();
|
|
416
|
+
daemonPid = child.pid;
|
|
417
|
+
} finally {
|
|
418
|
+
closeHatchLogFile(daemonLogFd);
|
|
419
|
+
}
|
|
386
420
|
|
|
387
421
|
// Write PID file immediately so the health monitor can find the process
|
|
388
422
|
// and concurrent hatch() calls see it as alive.
|
|
389
|
-
if (
|
|
390
|
-
writeFileSync(pidFile, String(
|
|
423
|
+
if (daemonPid) {
|
|
424
|
+
writeFileSync(pidFile, String(daemonPid), "utf-8");
|
|
391
425
|
}
|
|
392
426
|
}
|
|
393
427
|
|
|
@@ -534,20 +568,30 @@ export async function startGateway(assistantId?: string): Promise<string> {
|
|
|
534
568
|
);
|
|
535
569
|
}
|
|
536
570
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
571
|
+
const gatewayLogFd = openHatchLogFile("gateway.log");
|
|
572
|
+
try {
|
|
573
|
+
gateway = spawn(gatewayBinary, [], {
|
|
574
|
+
detached: true,
|
|
575
|
+
stdio: ["ignore", gatewayLogFd, gatewayLogFd],
|
|
576
|
+
env: gatewayEnv,
|
|
577
|
+
});
|
|
578
|
+
} finally {
|
|
579
|
+
closeHatchLogFile(gatewayLogFd);
|
|
580
|
+
}
|
|
542
581
|
} else {
|
|
543
582
|
// Source tree / bunx: resolve the gateway source directory and run via bun.
|
|
544
583
|
const gatewayDir = resolveGatewayDir();
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
584
|
+
const gwLogFd = openHatchLogFile("gateway.log");
|
|
585
|
+
try {
|
|
586
|
+
gateway = spawn("bun", ["run", "src/index.ts"], {
|
|
587
|
+
cwd: gatewayDir,
|
|
588
|
+
detached: true,
|
|
589
|
+
stdio: ["ignore", gwLogFd, gwLogFd],
|
|
590
|
+
env: gatewayEnv,
|
|
591
|
+
});
|
|
592
|
+
} finally {
|
|
593
|
+
closeHatchLogFile(gwLogFd);
|
|
594
|
+
}
|
|
551
595
|
}
|
|
552
596
|
|
|
553
597
|
gateway.unref();
|