vellum 0.2.9 → 0.2.11
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 +2 -2
- package/package.json +2 -2
- package/scripts/capture-x-graphql.ts +1 -18
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +110 -0
- package/src/__tests__/call-bridge.test.ts +40 -0
- package/src/__tests__/call-state.test.ts +41 -0
- package/src/__tests__/forbidden-legacy-symbols.test.ts +8 -6
- package/src/__tests__/gateway-only-enforcement.test.ts +13 -89
- package/src/__tests__/home-base-bootstrap.test.ts +13 -8
- package/src/__tests__/intent-routing.test.ts +2 -5
- package/src/__tests__/ipc-snapshot.test.ts +49 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +12 -2
- package/src/__tests__/prebuilt-home-base-seed.test.ts +9 -5
- package/src/__tests__/relay-server.test.ts +55 -0
- package/src/__tests__/skills.test.ts +83 -0
- package/src/__tests__/system-prompt.test.ts +2 -24
- package/src/__tests__/twilio-provider.test.ts +36 -0
- package/src/__tests__/twilio-routes.test.ts +108 -0
- package/src/calls/call-orchestrator.ts +25 -5
- package/src/calls/call-state.ts +23 -0
- package/src/calls/relay-server.ts +56 -1
- package/src/calls/twilio-config.ts +9 -13
- package/src/calls/twilio-provider.ts +6 -1
- package/src/calls/twilio-routes.ts +10 -1
- package/src/cli/core-commands.ts +12 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +57 -1
- package/src/config/bundled-skills/document/SKILL.md +11 -3
- package/src/config/bundled-skills/followups/icon.svg +24 -0
- package/src/config/bundled-skills/messaging/SKILL.md +7 -3
- package/src/config/bundled-skills/public-ingress/SKILL.md +183 -0
- package/src/config/bundled-skills/self-upgrade/SKILL.md +4 -10
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +4 -7
- package/src/config/system-prompt.ts +64 -360
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +5 -1
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +5 -1
- package/src/config/vellum-skills/telegram-setup/SKILL.md +2 -1
- package/src/daemon/handlers/config.ts +20 -9
- package/src/daemon/handlers/home-base.ts +3 -2
- package/src/daemon/handlers/identity.ts +127 -0
- package/src/daemon/handlers/index.ts +4 -0
- package/src/daemon/handlers/workspace-files.ts +75 -0
- package/src/daemon/ipc-contract-inventory.json +16 -4
- package/src/daemon/ipc-contract.ts +62 -2
- package/src/daemon/lifecycle.ts +16 -0
- package/src/daemon/session-notifiers.ts +29 -0
- package/src/daemon/session-surfaces.ts +5 -2
- package/src/daemon/session-tool-setup.ts +15 -4
- package/src/home-base/bootstrap.ts +3 -1
- package/src/home-base/prebuilt/seed.ts +16 -5
- package/src/inbound/public-ingress-urls.ts +15 -4
- package/src/runtime/http-server.ts +123 -20
- package/src/security/oauth2.ts +19 -161
- package/src/tools/browser/auto-navigate.ts +2 -2
- package/src/tools/browser/x-auto-navigate.ts +1 -1
- package/src/tools/claude-code/claude-code.ts +1 -1
- package/src/tools/system/version.ts +43 -0
- package/src/tools/tasks/work-item-run.ts +1 -1
- package/src/tools/terminal/parser.ts +29 -7
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +9 -2
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as net from 'node:net';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { getWorkspacePromptPath } from '../../util/platform.js';
|
|
6
|
+
import { log, defineHandlers, type HandlerContext } from './shared.js';
|
|
7
|
+
|
|
8
|
+
function handleIdentityGet(socket: net.Socket, ctx: HandlerContext): void {
|
|
9
|
+
const identityPath = getWorkspacePromptPath('IDENTITY.md');
|
|
10
|
+
|
|
11
|
+
if (!existsSync(identityPath)) {
|
|
12
|
+
ctx.send(socket, {
|
|
13
|
+
type: 'identity_get_response',
|
|
14
|
+
found: false,
|
|
15
|
+
name: '',
|
|
16
|
+
role: '',
|
|
17
|
+
personality: '',
|
|
18
|
+
emoji: '',
|
|
19
|
+
home: '',
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const content = readFileSync(identityPath, 'utf-8');
|
|
26
|
+
const fields: Record<string, string> = {};
|
|
27
|
+
for (const line of content.split('\n')) {
|
|
28
|
+
const trimmed = line.trim();
|
|
29
|
+
const lower = trimmed.toLowerCase();
|
|
30
|
+
const extract = (prefix: string): string | null => {
|
|
31
|
+
if (!lower.startsWith(prefix)) return null;
|
|
32
|
+
return trimmed.split(':**').pop()?.trim() ?? null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const name = extract('- **name:**');
|
|
36
|
+
if (name) { fields.name = name; continue; }
|
|
37
|
+
const role = extract('- **role:**');
|
|
38
|
+
if (role) { fields.role = role; continue; }
|
|
39
|
+
const personality = extract('- **personality:**') ?? extract('- **vibe:**');
|
|
40
|
+
if (personality) { fields.personality = personality; continue; }
|
|
41
|
+
const emoji = extract('- **emoji:**');
|
|
42
|
+
if (emoji) { fields.emoji = emoji; continue; }
|
|
43
|
+
const home = extract('- **home:**');
|
|
44
|
+
if (home) { fields.home = home; continue; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Read version from package.json
|
|
48
|
+
let version: string | undefined;
|
|
49
|
+
try {
|
|
50
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '../../../package.json');
|
|
51
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
52
|
+
version = pkg.version;
|
|
53
|
+
} catch {
|
|
54
|
+
// ignore
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Read createdAt from IDENTITY.md file birthtime
|
|
58
|
+
let createdAt: string | undefined;
|
|
59
|
+
try {
|
|
60
|
+
const stats = statSync(identityPath);
|
|
61
|
+
createdAt = stats.birthtime.toISOString();
|
|
62
|
+
} catch {
|
|
63
|
+
// ignore
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Read lockfile for assistantId, cloud, and originSystem
|
|
67
|
+
let assistantId: string | undefined;
|
|
68
|
+
let cloud: string | undefined;
|
|
69
|
+
let originSystem: string | undefined;
|
|
70
|
+
try {
|
|
71
|
+
const homedir = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
72
|
+
const lockfilePaths = [
|
|
73
|
+
join(homedir, '.vellum.lock.json'),
|
|
74
|
+
join(homedir, '.vellum.lockfile.json'),
|
|
75
|
+
];
|
|
76
|
+
for (const lockPath of lockfilePaths) {
|
|
77
|
+
if (!existsSync(lockPath)) continue;
|
|
78
|
+
const lockData = JSON.parse(readFileSync(lockPath, 'utf-8'));
|
|
79
|
+
const assistants = lockData.assistants as Array<Record<string, unknown>> | undefined;
|
|
80
|
+
if (assistants && assistants.length > 0) {
|
|
81
|
+
// Use the most recently hatched assistant
|
|
82
|
+
const sorted = [...assistants].sort((a, b) => {
|
|
83
|
+
const dateA = new Date(a.hatchedAt as string || 0).getTime();
|
|
84
|
+
const dateB = new Date(b.hatchedAt as string || 0).getTime();
|
|
85
|
+
return dateB - dateA;
|
|
86
|
+
});
|
|
87
|
+
const latest = sorted[0];
|
|
88
|
+
assistantId = latest.assistantId as string | undefined;
|
|
89
|
+
cloud = latest.cloud as string | undefined;
|
|
90
|
+
originSystem = cloud === 'local' ? 'local' : cloud;
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// ignore — lockfile may not exist
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
ctx.send(socket, {
|
|
99
|
+
type: 'identity_get_response',
|
|
100
|
+
found: true,
|
|
101
|
+
name: fields.name ?? '',
|
|
102
|
+
role: fields.role ?? '',
|
|
103
|
+
personality: fields.personality ?? '',
|
|
104
|
+
emoji: fields.emoji ?? '',
|
|
105
|
+
home: fields.home ?? '',
|
|
106
|
+
version,
|
|
107
|
+
assistantId,
|
|
108
|
+
createdAt,
|
|
109
|
+
originSystem,
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
log.error({ err }, 'Failed to read identity');
|
|
113
|
+
ctx.send(socket, {
|
|
114
|
+
type: 'identity_get_response',
|
|
115
|
+
found: false,
|
|
116
|
+
name: '',
|
|
117
|
+
role: '',
|
|
118
|
+
personality: '',
|
|
119
|
+
emoji: '',
|
|
120
|
+
home: '',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const identityHandlers = defineHandlers({
|
|
126
|
+
identity_get: (_msg, socket, ctx) => handleIdentityGet(socket, ctx),
|
|
127
|
+
});
|
|
@@ -20,6 +20,8 @@ import { subagentHandlers } from './subagents.js';
|
|
|
20
20
|
import { browserHandlers } from './browser.js';
|
|
21
21
|
import { signingHandlers } from './signing.js';
|
|
22
22
|
import { twitterAuthHandlers } from './twitter-auth.js';
|
|
23
|
+
import { workspaceFileHandlers } from './workspace-files.js';
|
|
24
|
+
import { identityHandlers } from './identity.js';
|
|
23
25
|
|
|
24
26
|
// Re-export types and utilities for backwards compatibility
|
|
25
27
|
export type {
|
|
@@ -102,6 +104,8 @@ const handlers = {
|
|
|
102
104
|
...browserHandlers,
|
|
103
105
|
...signingHandlers,
|
|
104
106
|
...twitterAuthHandlers,
|
|
107
|
+
...workspaceFileHandlers,
|
|
108
|
+
...identityHandlers,
|
|
105
109
|
...inlineHandlers,
|
|
106
110
|
} satisfies DispatchMap;
|
|
107
111
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as net from 'node:net';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve, sep } from 'node:path';
|
|
4
|
+
import { getWorkspaceDir } from '../../util/platform.js';
|
|
5
|
+
import { log, defineHandlers, type HandlerContext } from './shared.js';
|
|
6
|
+
import type { WorkspaceFileReadRequest } from '../ipc-protocol.js';
|
|
7
|
+
|
|
8
|
+
/** Well-known workspace prompt files shown in the Identity panel. */
|
|
9
|
+
const WORKSPACE_FILES = ['IDENTITY.md', 'SOUL.md', 'USER.md', 'skills/'];
|
|
10
|
+
|
|
11
|
+
function handleWorkspaceFilesList(socket: net.Socket, ctx: HandlerContext): void {
|
|
12
|
+
const base = getWorkspaceDir();
|
|
13
|
+
const files = WORKSPACE_FILES.map((name) => ({
|
|
14
|
+
path: name,
|
|
15
|
+
name,
|
|
16
|
+
exists: existsSync(join(base, name)),
|
|
17
|
+
}));
|
|
18
|
+
ctx.send(socket, { type: 'workspace_files_list_response', files });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function handleWorkspaceFileRead(
|
|
22
|
+
msg: WorkspaceFileReadRequest,
|
|
23
|
+
socket: net.Socket,
|
|
24
|
+
ctx: HandlerContext,
|
|
25
|
+
): void {
|
|
26
|
+
const base = getWorkspaceDir();
|
|
27
|
+
const requested = msg.path;
|
|
28
|
+
|
|
29
|
+
// Prevent path traversal — reject paths that escape the workspace root.
|
|
30
|
+
// Use resolve() to canonicalize and check with trailing separator to prevent
|
|
31
|
+
// sibling directory prefix matches (e.g. "../workspace-evil/secret").
|
|
32
|
+
const resolved = resolve(base, requested);
|
|
33
|
+
if (!resolved.startsWith(base + sep) && resolved !== base) {
|
|
34
|
+
log.warn({ path: requested }, 'Workspace file read blocked: path traversal attempt');
|
|
35
|
+
ctx.send(socket, {
|
|
36
|
+
type: 'workspace_file_read_response',
|
|
37
|
+
path: requested,
|
|
38
|
+
content: null,
|
|
39
|
+
error: 'Invalid path',
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
if (!existsSync(resolved)) {
|
|
46
|
+
ctx.send(socket, {
|
|
47
|
+
type: 'workspace_file_read_response',
|
|
48
|
+
path: requested,
|
|
49
|
+
content: null,
|
|
50
|
+
error: 'File not found',
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const content = readFileSync(resolved, 'utf-8');
|
|
55
|
+
ctx.send(socket, {
|
|
56
|
+
type: 'workspace_file_read_response',
|
|
57
|
+
path: requested,
|
|
58
|
+
content,
|
|
59
|
+
});
|
|
60
|
+
} catch (err) {
|
|
61
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
62
|
+
log.error({ err, path: requested }, 'Failed to read workspace file');
|
|
63
|
+
ctx.send(socket, {
|
|
64
|
+
type: 'workspace_file_read_response',
|
|
65
|
+
path: requested,
|
|
66
|
+
content: null,
|
|
67
|
+
error: message,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const workspaceFileHandlers = defineHandlers({
|
|
73
|
+
workspace_files_list: (_msg, socket, ctx) => handleWorkspaceFilesList(socket, ctx),
|
|
74
|
+
workspace_file_read: handleWorkspaceFileRead,
|
|
75
|
+
});
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"GetSigningIdentityResponse",
|
|
32
32
|
"HistoryRequest",
|
|
33
33
|
"HomeBaseGetRequest",
|
|
34
|
+
"IdentityGetRequest",
|
|
34
35
|
"ImageGenModelSetRequest",
|
|
35
36
|
"IngressConfigRequest",
|
|
36
37
|
"IntegrationConnectRequest",
|
|
@@ -103,7 +104,9 @@
|
|
|
103
104
|
"WorkItemPreflightRequest",
|
|
104
105
|
"WorkItemRunTaskRequest",
|
|
105
106
|
"WorkItemUpdateRequest",
|
|
106
|
-
"WorkItemsListRequest"
|
|
107
|
+
"WorkItemsListRequest",
|
|
108
|
+
"WorkspaceFileReadRequest",
|
|
109
|
+
"WorkspaceFilesListRequest"
|
|
107
110
|
],
|
|
108
111
|
"serverMessageTypes": [
|
|
109
112
|
"AcceptStarterBundleResponse",
|
|
@@ -143,6 +146,7 @@
|
|
|
143
146
|
"GetSigningIdentityRequest",
|
|
144
147
|
"HistoryResponse",
|
|
145
148
|
"HomeBaseGetResponse",
|
|
149
|
+
"IdentityGetResponse",
|
|
146
150
|
"IngressConfigResponse",
|
|
147
151
|
"IntegrationConnectResult",
|
|
148
152
|
"IntegrationListResponse",
|
|
@@ -222,7 +226,9 @@
|
|
|
222
226
|
"WorkItemRunTaskResponse",
|
|
223
227
|
"WorkItemStatusChanged",
|
|
224
228
|
"WorkItemUpdateResponse",
|
|
225
|
-
"WorkItemsListResponse"
|
|
229
|
+
"WorkItemsListResponse",
|
|
230
|
+
"WorkspaceFileReadResponse",
|
|
231
|
+
"WorkspaceFilesListResponse"
|
|
226
232
|
],
|
|
227
233
|
"clientWireTypes": [
|
|
228
234
|
"accept_starter_bundle",
|
|
@@ -256,6 +262,7 @@
|
|
|
256
262
|
"get_signing_identity_response",
|
|
257
263
|
"history_request",
|
|
258
264
|
"home_base_get",
|
|
265
|
+
"identity_get",
|
|
259
266
|
"image_gen_model_set",
|
|
260
267
|
"ingress_config",
|
|
261
268
|
"integration_connect",
|
|
@@ -328,7 +335,9 @@
|
|
|
328
335
|
"work_item_preflight",
|
|
329
336
|
"work_item_run_task",
|
|
330
337
|
"work_item_update",
|
|
331
|
-
"work_items_list"
|
|
338
|
+
"work_items_list",
|
|
339
|
+
"workspace_file_read",
|
|
340
|
+
"workspace_files_list"
|
|
332
341
|
],
|
|
333
342
|
"serverWireTypes": [
|
|
334
343
|
"accept_starter_bundle_response",
|
|
@@ -368,6 +377,7 @@
|
|
|
368
377
|
"get_signing_identity",
|
|
369
378
|
"history_response",
|
|
370
379
|
"home_base_get_response",
|
|
380
|
+
"identity_get_response",
|
|
371
381
|
"ingress_config_response",
|
|
372
382
|
"integration_connect_result",
|
|
373
383
|
"integration_list_response",
|
|
@@ -446,6 +456,8 @@
|
|
|
446
456
|
"work_item_run_task_response",
|
|
447
457
|
"work_item_status_changed",
|
|
448
458
|
"work_item_update_response",
|
|
449
|
-
"work_items_list_response"
|
|
459
|
+
"work_items_list_response",
|
|
460
|
+
"workspace_file_read_response",
|
|
461
|
+
"workspace_files_list_response"
|
|
450
462
|
]
|
|
451
463
|
}
|
|
@@ -476,6 +476,7 @@ export interface IngressConfigRequest {
|
|
|
476
476
|
type: 'ingress_config';
|
|
477
477
|
action: 'get' | 'set';
|
|
478
478
|
publicBaseUrl?: string;
|
|
479
|
+
enabled?: boolean;
|
|
479
480
|
}
|
|
480
481
|
|
|
481
482
|
export interface VercelApiConfigRequest {
|
|
@@ -875,6 +876,22 @@ export interface WorkItemCancelRequest {
|
|
|
875
876
|
id: string;
|
|
876
877
|
}
|
|
877
878
|
|
|
879
|
+
// === Workspace File IPC ─────────────────────────────────────────────────────
|
|
880
|
+
|
|
881
|
+
export interface WorkspaceFilesListRequest {
|
|
882
|
+
type: 'workspace_files_list';
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export interface WorkspaceFileReadRequest {
|
|
886
|
+
type: 'workspace_file_read';
|
|
887
|
+
/** Relative path within the workspace directory (e.g. "IDENTITY.md"). */
|
|
888
|
+
path: string;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
export interface IdentityGetRequest {
|
|
892
|
+
type: 'identity_get';
|
|
893
|
+
}
|
|
894
|
+
|
|
878
895
|
export type ClientMessage =
|
|
879
896
|
| AuthMessage
|
|
880
897
|
| UserMessage
|
|
@@ -979,7 +996,10 @@ export type ClientMessage =
|
|
|
979
996
|
| SubagentAbortRequest
|
|
980
997
|
| SubagentStatusRequest
|
|
981
998
|
| SubagentMessageRequest
|
|
982
|
-
| SubagentDetailRequest
|
|
999
|
+
| SubagentDetailRequest
|
|
1000
|
+
| WorkspaceFilesListRequest
|
|
1001
|
+
| WorkspaceFileReadRequest
|
|
1002
|
+
| IdentityGetRequest;
|
|
983
1003
|
|
|
984
1004
|
export interface IntegrationListRequest {
|
|
985
1005
|
type: 'integration_list';
|
|
@@ -1690,6 +1710,7 @@ export interface SlackWebhookConfigResponse {
|
|
|
1690
1710
|
|
|
1691
1711
|
export interface IngressConfigResponse {
|
|
1692
1712
|
type: 'ingress_config_response';
|
|
1713
|
+
enabled: boolean;
|
|
1693
1714
|
publicBaseUrl: string;
|
|
1694
1715
|
/** Read-only gateway target computed from GATEWAY_PORT env var (default 7830) + loopback host. */
|
|
1695
1716
|
localGatewayTarget: string;
|
|
@@ -2103,6 +2124,42 @@ export interface TaskRunThreadCreated {
|
|
|
2103
2124
|
title: string;
|
|
2104
2125
|
}
|
|
2105
2126
|
|
|
2127
|
+
// === Workspace File Responses ────────────────────────────────────────────────
|
|
2128
|
+
|
|
2129
|
+
export interface WorkspaceFilesListResponse {
|
|
2130
|
+
type: 'workspace_files_list_response';
|
|
2131
|
+
files: Array<{
|
|
2132
|
+
/** Relative path within the workspace (e.g. "IDENTITY.md", "skills/my-skill"). */
|
|
2133
|
+
path: string;
|
|
2134
|
+
/** Display name (e.g. "IDENTITY.md"). */
|
|
2135
|
+
name: string;
|
|
2136
|
+
/** Whether the file/directory exists. */
|
|
2137
|
+
exists: boolean;
|
|
2138
|
+
}>;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
export interface WorkspaceFileReadResponse {
|
|
2142
|
+
type: 'workspace_file_read_response';
|
|
2143
|
+
path: string;
|
|
2144
|
+
content: string | null;
|
|
2145
|
+
error?: string;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
export interface IdentityGetResponse {
|
|
2149
|
+
type: 'identity_get_response';
|
|
2150
|
+
/** Whether an IDENTITY.md file was found. When false, all fields are empty defaults. Optional for backwards compat with older daemons. */
|
|
2151
|
+
found?: boolean;
|
|
2152
|
+
name: string;
|
|
2153
|
+
role: string;
|
|
2154
|
+
personality: string;
|
|
2155
|
+
emoji: string;
|
|
2156
|
+
home: string;
|
|
2157
|
+
version?: string;
|
|
2158
|
+
assistantId?: string;
|
|
2159
|
+
createdAt?: string;
|
|
2160
|
+
originSystem?: string;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2106
2163
|
export type ServerMessage =
|
|
2107
2164
|
| AuthResult
|
|
2108
2165
|
| UserMessageEcho
|
|
@@ -2220,7 +2277,10 @@ export type ServerMessage =
|
|
|
2220
2277
|
| SubagentSpawned
|
|
2221
2278
|
| SubagentStatusChanged
|
|
2222
2279
|
| SubagentEvent
|
|
2223
|
-
| SubagentDetailResponse
|
|
2280
|
+
| SubagentDetailResponse
|
|
2281
|
+
| WorkspaceFilesListResponse
|
|
2282
|
+
| WorkspaceFileReadResponse
|
|
2283
|
+
| IdentityGetResponse;
|
|
2224
2284
|
|
|
2225
2285
|
// === Subagent IPC ─────────────────────────────────────────────────────
|
|
2226
2286
|
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { initializeProviders } from '../providers/registry.js';
|
|
|
21
21
|
import { initializeTools } from '../tools/registry.js';
|
|
22
22
|
import { loadConfig } from '../config/loader.js';
|
|
23
23
|
import { ensurePromptFiles } from '../config/system-prompt.js';
|
|
24
|
+
import { loadPrebuiltHtml } from '../home-base/prebuilt/seed.js';
|
|
24
25
|
import { DaemonServer } from './server.js';
|
|
25
26
|
import { listWorkItems, updateWorkItem } from '../work-items/work-item-store.js';
|
|
26
27
|
import { getLogger, initLogger } from '../util/logger.js';
|
|
@@ -278,6 +279,21 @@ export async function runDaemon(): Promise<void> {
|
|
|
278
279
|
log.info({ seedDir, interfacesDir }, 'Seeded initial interface files');
|
|
279
280
|
}
|
|
280
281
|
|
|
282
|
+
// Seed the vellum-desktop interface from the prebuilt Home Base HTML if it
|
|
283
|
+
// doesn't already exist. This ensures the Home tab renders immediately
|
|
284
|
+
// on first launch for both local and remote hatches.
|
|
285
|
+
const desktopIndexPath = join(getInterfacesDir(), 'vellum-desktop', 'index.html');
|
|
286
|
+
if (!existsSync(desktopIndexPath)) {
|
|
287
|
+
const prebuiltHtml = loadPrebuiltHtml();
|
|
288
|
+
if (prebuiltHtml) {
|
|
289
|
+
mkdirSync(join(getInterfacesDir(), 'vellum-desktop'), { recursive: true });
|
|
290
|
+
writeFileSync(desktopIndexPath, prebuiltHtml);
|
|
291
|
+
log.info('Seeded vellum-desktop/index.html from prebuilt Home Base');
|
|
292
|
+
} else {
|
|
293
|
+
log.warn('Could not seed vellum-desktop/index.html — prebuilt HTML not found (missing embedded index.html in home-base/prebuilt/)');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
281
297
|
log.info('Daemon startup: installing templates and initializing DB');
|
|
282
298
|
installTemplates();
|
|
283
299
|
ensurePromptFiles();
|
|
@@ -25,6 +25,8 @@ import { lastCommentaryBySession, lastSummaryBySession } from './watch-handler.j
|
|
|
25
25
|
import {
|
|
26
26
|
registerCallQuestionNotifier,
|
|
27
27
|
unregisterCallQuestionNotifier,
|
|
28
|
+
registerCallTranscriptNotifier,
|
|
29
|
+
unregisterCallTranscriptNotifier,
|
|
28
30
|
registerCallCompletionNotifier,
|
|
29
31
|
unregisterCallCompletionNotifier,
|
|
30
32
|
} from '../calls/call-state.js';
|
|
@@ -114,6 +116,32 @@ export function registerSessionNotifiers(
|
|
|
114
116
|
});
|
|
115
117
|
});
|
|
116
118
|
|
|
119
|
+
registerCallTranscriptNotifier(
|
|
120
|
+
conversationId,
|
|
121
|
+
(_callSessionId: string, speaker: 'caller' | 'assistant', text: string) => {
|
|
122
|
+
const speakerLabel = speaker === 'caller' ? 'Caller' : 'Assistant';
|
|
123
|
+
const transcriptText = `**Live call transcript**\n${speakerLabel}: ${text}`;
|
|
124
|
+
|
|
125
|
+
conversationStore.addMessage(
|
|
126
|
+
conversationId,
|
|
127
|
+
'assistant',
|
|
128
|
+
JSON.stringify([{ type: 'text', text: transcriptText }]),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
ctx.messages.push(createAssistantMessage(transcriptText));
|
|
132
|
+
|
|
133
|
+
ctx.sendToClient({
|
|
134
|
+
type: 'assistant_text_delta',
|
|
135
|
+
text: transcriptText,
|
|
136
|
+
sessionId: conversationId,
|
|
137
|
+
});
|
|
138
|
+
ctx.sendToClient({
|
|
139
|
+
type: 'message_complete',
|
|
140
|
+
sessionId: conversationId,
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
117
145
|
registerCallCompletionNotifier(conversationId, (callSessionId: string) => {
|
|
118
146
|
const callSession = getCallSession(callSessionId);
|
|
119
147
|
const events = getCallEvents(callSessionId);
|
|
@@ -160,5 +188,6 @@ export function unregisterWatchNotifiers(conversationId: string): void {
|
|
|
160
188
|
*/
|
|
161
189
|
export function unregisterCallNotifiers(conversationId: string): void {
|
|
162
190
|
unregisterCallQuestionNotifier(conversationId);
|
|
191
|
+
unregisterCallTranscriptNotifier(conversationId);
|
|
163
192
|
unregisterCallCompletionNotifier(conversationId);
|
|
164
193
|
}
|
|
@@ -407,10 +407,11 @@ export function handleSurfaceAction(ctx: SurfaceSessionContext, surfaceId: strin
|
|
|
407
407
|
/**
|
|
408
408
|
* After an app_update, refresh any active surface that displays the updated app.
|
|
409
409
|
*/
|
|
410
|
-
export function refreshSurfacesForApp(ctx: SurfaceSessionContext, appId: string, opts?: { fileChange?: boolean; status?: string }):
|
|
410
|
+
export function refreshSurfacesForApp(ctx: SurfaceSessionContext, appId: string, opts?: { fileChange?: boolean; status?: string }): boolean {
|
|
411
411
|
const app = getApp(appId);
|
|
412
|
-
if (!app) return;
|
|
412
|
+
if (!app) return false;
|
|
413
413
|
|
|
414
|
+
let refreshed = false;
|
|
414
415
|
for (const [surfaceId, stored] of ctx.surfaceState.entries()) {
|
|
415
416
|
if (stored.surfaceType !== 'dynamic_page') continue;
|
|
416
417
|
const data = stored.data as DynamicPageSurfaceData;
|
|
@@ -436,8 +437,10 @@ export function refreshSurfacesForApp(ctx: SurfaceSessionContext, appId: string,
|
|
|
436
437
|
data: updatedData,
|
|
437
438
|
});
|
|
438
439
|
|
|
440
|
+
refreshed = true;
|
|
439
441
|
log.info({ conversationId: ctx.conversationId, surfaceId, appId }, 'Auto-refreshed surface after app_update');
|
|
440
442
|
}
|
|
443
|
+
return refreshed;
|
|
441
444
|
}
|
|
442
445
|
|
|
443
446
|
export function buildCompletionSummary(surfaceType: string | undefined, actionId: string, data?: Record<string, unknown>): string {
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from './session-surfaces.js';
|
|
28
28
|
import type { SurfaceSessionContext } from './session-surfaces.js';
|
|
29
29
|
import { updatePublishedAppDeployment } from '../services/published-app-updater.js';
|
|
30
|
+
import { openAppViaSurface } from '../tools/apps/open-proxy.js';
|
|
30
31
|
import { registerSessionSender } from '../tools/browser/browser-screencast.js';
|
|
31
32
|
import type { ProxyApprovalCallback, ProxyApprovalRequest } from '../tools/network/script-proxy/index.js';
|
|
32
33
|
import { projectSkillTools, type SkillProjectionCache } from './session-skill-tools.js';
|
|
@@ -265,13 +266,18 @@ export function createToolExecutor(
|
|
|
265
266
|
},
|
|
266
267
|
});
|
|
267
268
|
|
|
268
|
-
// Auto-refresh workspace surfaces when a persisted app is updated
|
|
269
|
+
// Auto-refresh workspace surfaces when a persisted app is updated.
|
|
270
|
+
// If no surface is currently showing the app, auto-open it.
|
|
269
271
|
if (name === 'app_update' && !result.isError) {
|
|
270
272
|
const appId = input.app_id as string | undefined;
|
|
271
273
|
if (appId) {
|
|
272
|
-
refreshSurfacesForApp(ctx, appId);
|
|
274
|
+
const refreshed = refreshSurfacesForApp(ctx, appId);
|
|
273
275
|
broadcastToAllClients?.({ type: 'app_files_changed', appId });
|
|
274
276
|
void updatePublishedAppDeployment(appId);
|
|
277
|
+
if (!refreshed && !ctx.hasNoClient && !ctx.headlessLock) {
|
|
278
|
+
const resolver = (tn: string, pi: Record<string, unknown>) => surfaceProxyResolver(ctx, tn, pi);
|
|
279
|
+
void openAppViaSurface(appId, resolver);
|
|
280
|
+
}
|
|
275
281
|
}
|
|
276
282
|
}
|
|
277
283
|
|
|
@@ -286,14 +292,19 @@ export function createToolExecutor(
|
|
|
286
292
|
broadcastToAllClients?.({ type: 'tasks_changed' });
|
|
287
293
|
}
|
|
288
294
|
|
|
289
|
-
// Auto-refresh workspace surfaces when app files are edited
|
|
295
|
+
// Auto-refresh workspace surfaces when app files are edited.
|
|
296
|
+
// If no surface is currently showing the app, auto-open it.
|
|
290
297
|
if ((name === 'app_file_edit' || name === 'app_file_write') && !result.isError) {
|
|
291
298
|
const appId = input.app_id as string | undefined;
|
|
292
299
|
const status = input.status as string | undefined;
|
|
293
300
|
if (appId) {
|
|
294
|
-
refreshSurfacesForApp(ctx, appId, { fileChange: true, status });
|
|
301
|
+
const refreshed = refreshSurfacesForApp(ctx, appId, { fileChange: true, status });
|
|
295
302
|
broadcastToAllClients?.({ type: 'app_files_changed', appId });
|
|
296
303
|
void updatePublishedAppDeployment(appId);
|
|
304
|
+
if (!refreshed && !ctx.hasNoClient && !ctx.headlessLock) {
|
|
305
|
+
const resolver = (tn: string, pi: Record<string, unknown>) => surfaceProxyResolver(ctx, tn, pi);
|
|
306
|
+
void openAppViaSurface(appId, resolver);
|
|
307
|
+
}
|
|
297
308
|
}
|
|
298
309
|
}
|
|
299
310
|
|
|
@@ -23,7 +23,7 @@ export interface HomeBaseBootstrapResult {
|
|
|
23
23
|
created: boolean;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function bootstrapHomeBaseAppLink(): HomeBaseBootstrapResult {
|
|
26
|
+
export function bootstrapHomeBaseAppLink(): HomeBaseBootstrapResult | null {
|
|
27
27
|
const linked = resolveExistingLink();
|
|
28
28
|
if (linked) {
|
|
29
29
|
return {
|
|
@@ -46,6 +46,8 @@ export function bootstrapHomeBaseAppLink(): HomeBaseBootstrapResult {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const seeded = ensurePrebuiltHomeBaseSeeded();
|
|
49
|
+
if (!seeded) return null;
|
|
50
|
+
|
|
49
51
|
const next = setHomeBaseAppLink(seeded.appId, 'prebuilt_seed');
|
|
50
52
|
log.info({ appId: next.appId, created: seeded.created }, 'Bootstrapped Home Base app link');
|
|
51
53
|
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
HOME_BASE_PREBUILT_DESCRIPTION_PREFIX,
|
|
7
7
|
isPrebuiltHomeBaseApp,
|
|
8
8
|
} from '../prebuilt-home-base-updater.js';
|
|
9
|
+
// Static import so the JSON is bundled into compiled binaries (avoids ENOENT on $bunfs)
|
|
10
|
+
import seedMetadataJson from './seed-metadata.json' with { type: 'json' };
|
|
9
11
|
|
|
10
12
|
const log = getLogger('home-base-seed');
|
|
11
13
|
|
|
@@ -26,12 +28,16 @@ function getPrebuiltDir(): string {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
function loadSeedMetadata(): SeedMetadata {
|
|
29
|
-
|
|
30
|
-
return JSON.parse(raw) as SeedMetadata;
|
|
31
|
+
return seedMetadataJson as SeedMetadata;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function loadPrebuiltHtml(): string {
|
|
34
|
-
|
|
34
|
+
export function loadPrebuiltHtml(): string | null {
|
|
35
|
+
try {
|
|
36
|
+
return readFileSync(join(getPrebuiltDir(), 'index.html'), 'utf-8');
|
|
37
|
+
} catch {
|
|
38
|
+
log.warn('Could not load prebuilt index.html (expected in compiled binary)');
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
function buildDescription(metadata: SeedMetadata): string {
|
|
@@ -80,7 +86,7 @@ export function getPrebuiltHomeBaseTaskPayload(): PrebuiltHomeBaseTaskPayload {
|
|
|
80
86
|
};
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
export function ensurePrebuiltHomeBaseSeeded(): { appId: string; created: boolean } {
|
|
89
|
+
export function ensurePrebuiltHomeBaseSeeded(): { appId: string; created: boolean } | null {
|
|
84
90
|
const existing = findSeededHomeBaseApp();
|
|
85
91
|
if (existing) {
|
|
86
92
|
return { appId: existing.id, created: false };
|
|
@@ -88,6 +94,11 @@ export function ensurePrebuiltHomeBaseSeeded(): { appId: string; created: boolea
|
|
|
88
94
|
|
|
89
95
|
const metadata = loadSeedMetadata();
|
|
90
96
|
const html = loadPrebuiltHtml();
|
|
97
|
+
if (html === null) {
|
|
98
|
+
log.warn('Skipping Home Base seed — prebuilt HTML not available');
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
const created = createApp({
|
|
92
103
|
name: metadata.appName,
|
|
93
104
|
description: buildDescription(metadata),
|
|
@@ -20,14 +20,15 @@
|
|
|
20
20
|
* - The assistant's outbound callback URLs (Twilio webhooks, OAuth
|
|
21
21
|
* redirect URIs, etc.) match the gateway's inbound signature
|
|
22
22
|
* reconstruction URL.
|
|
23
|
-
* - Changing the URL in Settings
|
|
24
|
-
*
|
|
23
|
+
* - Changing the URL in Settings immediately updates outbound callback
|
|
24
|
+
* registration, while the gateway can validate inbound Twilio signatures
|
|
25
|
+
* using forwarded public URL headers from tunnels/proxies.
|
|
25
26
|
*
|
|
26
27
|
* All public-facing ingress URL construction is centralized here.
|
|
27
28
|
*/
|
|
28
29
|
|
|
29
30
|
export interface IngressConfig {
|
|
30
|
-
ingress?: { publicBaseUrl?: string };
|
|
31
|
+
ingress?: { enabled?: boolean; publicBaseUrl?: string };
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -41,9 +42,19 @@ function normalizeUrl(url: string): string {
|
|
|
41
42
|
* Resolve the canonical public base URL using the precedence chain
|
|
42
43
|
* documented at the top of this module.
|
|
43
44
|
*
|
|
44
|
-
*
|
|
45
|
+
* When `ingress.enabled` is explicitly `false`, the public ingress is
|
|
46
|
+
* considered disabled regardless of whether a URL is configured. This
|
|
47
|
+
* allows the user to toggle ingress off without clearing the URL value.
|
|
48
|
+
*
|
|
49
|
+
* Throws if no source provides a non-empty value or if ingress is disabled.
|
|
45
50
|
*/
|
|
46
51
|
export function getPublicBaseUrl(config: IngressConfig): string {
|
|
52
|
+
if (config.ingress?.enabled === false) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
'Public ingress is disabled. Enable it in Settings to use public-facing webhooks.',
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
const ingressValue = config.ingress?.publicBaseUrl;
|
|
48
59
|
if (ingressValue) {
|
|
49
60
|
const normalized = normalizeUrl(ingressValue);
|