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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, existsSync, copyFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { getWorkspaceDir, getWorkspacePromptPath } from '../util/platform.js';
|
|
3
|
+
import { getWorkspaceDir, getWorkspacePromptPath, isMacOS } from '../util/platform.js';
|
|
4
4
|
import { getLogger } from '../util/logger.js';
|
|
5
5
|
import { loadSkillCatalog, type SkillSummary } from './skills.js';
|
|
6
6
|
import { getConfig } from './loader.js';
|
|
@@ -108,13 +108,11 @@ export function buildSystemPrompt(): string {
|
|
|
108
108
|
);
|
|
109
109
|
}
|
|
110
110
|
parts.push(buildConfigSection());
|
|
111
|
-
parts.push(buildToolRoutingSection());
|
|
112
111
|
parts.push(buildTaskScheduleReminderRoutingSection());
|
|
113
112
|
parts.push(buildAttachmentSection());
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
parts.push(buildStarterTaskPlaybookSection());
|
|
113
|
+
if (!isOnboardingComplete()) {
|
|
114
|
+
parts.push(buildStarterTaskPlaybookSection());
|
|
115
|
+
}
|
|
118
116
|
parts.push(buildToolPermissionSection());
|
|
119
117
|
parts.push(buildSystemPermissionSection());
|
|
120
118
|
parts.push(buildChannelAwarenessSection());
|
|
@@ -128,72 +126,6 @@ export function buildSystemPrompt(): string {
|
|
|
128
126
|
return appendSkillsCatalog(parts.join('\n\n'));
|
|
129
127
|
}
|
|
130
128
|
|
|
131
|
-
function buildToolRoutingSection(): string {
|
|
132
|
-
return [
|
|
133
|
-
'## Tool Routing by Content Type',
|
|
134
|
-
'',
|
|
135
|
-
'Choose the right tool based on what the user is asking for:',
|
|
136
|
-
'',
|
|
137
|
-
'### Writing Text Content → `document_create` + multiple `document_update` calls',
|
|
138
|
-
'',
|
|
139
|
-
'Use when the user wants to **write or compose** long-form text:',
|
|
140
|
-
'- Blog posts, articles, essays',
|
|
141
|
-
'- Reports, documentation, guides',
|
|
142
|
-
'- Any long-form writing (500+ words)',
|
|
143
|
-
'',
|
|
144
|
-
'**CRITICAL ACTION REQUIRED:**',
|
|
145
|
-
'1. User says "write a blog post" → You MUST call `document_create` tool IMMEDIATELY',
|
|
146
|
-
'2. Do NOT just say "I\'ll create a document" - actually call the tool!',
|
|
147
|
-
'3. After creating the document, call `document_update` MULTIPLE times to stream content in chunks',
|
|
148
|
-
'- Content that benefits from editing and markdown formatting',
|
|
149
|
-
'',
|
|
150
|
-
'**Key indicators:** User says "write", "draft", "compose", "create a blog/article/essay"',
|
|
151
|
-
'**Workflow:** Call `document_create` with title, then stream content using `document_update`',
|
|
152
|
-
'**Output:** Opens the Documents tab with built-in rich text editor',
|
|
153
|
-
'',
|
|
154
|
-
'### Building Interactive Apps → `app_create`',
|
|
155
|
-
'',
|
|
156
|
-
'Use when the user wants to **build or create** an interactive application:',
|
|
157
|
-
'- Dashboards, calculators, tools, utilities',
|
|
158
|
-
'- Games, trackers, timers, counters',
|
|
159
|
-
'- Data visualizations with user interaction',
|
|
160
|
-
'- Forms, CRUD apps, admin panels',
|
|
161
|
-
'- Presentational sites (portfolios, landing pages, resumes)',
|
|
162
|
-
'',
|
|
163
|
-
'**Key indicators:** User says "build", "create", "make an app/dashboard/calculator"',
|
|
164
|
-
'**Workflow:** Call `app_create` with HTML/CSS/JS and schema',
|
|
165
|
-
'**Output:** Opens a dynamic_page surface with full interactivity',
|
|
166
|
-
'',
|
|
167
|
-
'### Showing Structured Data → `ui_show`',
|
|
168
|
-
'',
|
|
169
|
-
'Use when the user wants to **display or visualize** existing data:',
|
|
170
|
-
'- Weather forecasts, flight results, stock prices',
|
|
171
|
-
'- Quick tables, cards, lists from API/tool data',
|
|
172
|
-
'- Temporary displays (no persistence needed)',
|
|
173
|
-
'',
|
|
174
|
-
'**Key indicators:** User says "show", "display", "what\'s the weather/stock price"',
|
|
175
|
-
'**Workflow:** Gather data via tools, then call `ui_show` with formatted HTML',
|
|
176
|
-
'**Output:** Shows a temporary dynamic page in chat context',
|
|
177
|
-
'',
|
|
178
|
-
'### Decision Framework',
|
|
179
|
-
'',
|
|
180
|
-
'**Ask yourself:**',
|
|
181
|
-
'1. Is this primarily **text composition**? → `document_create`',
|
|
182
|
-
'2. Is this an **interactive app** with state/logic? → `app_create`',
|
|
183
|
-
'3. Is this **displaying data** from tools/APIs? → `ui_show`',
|
|
184
|
-
'',
|
|
185
|
-
'**Examples:**',
|
|
186
|
-
'- "Write a blog post about AI" → `document_create` (text composition)',
|
|
187
|
-
'- "Build a todo list app" → `app_create` (interactive, stateful)',
|
|
188
|
-
'- "Show me the weather in NYC" → `ui_show` (data display)',
|
|
189
|
-
'- "Create a markdown editor" → `app_create` (interactive tool)',
|
|
190
|
-
'- "Draft an essay on philosophy" → `document_create` (text composition)',
|
|
191
|
-
'- "Make a countdown timer" → `app_create` (interactive, dynamic)',
|
|
192
|
-
'',
|
|
193
|
-
'**All three tools are equally important — use the right one for the task.**',
|
|
194
|
-
].join('\n');
|
|
195
|
-
}
|
|
196
|
-
|
|
197
129
|
function buildTaskScheduleReminderRoutingSection(): string {
|
|
198
130
|
return [
|
|
199
131
|
'## Tool Routing: Tasks vs Schedules vs Reminders',
|
|
@@ -308,294 +240,6 @@ function buildAttachmentSection(): string {
|
|
|
308
240
|
].join('\n');
|
|
309
241
|
}
|
|
310
242
|
|
|
311
|
-
function buildDynamicUiSection(): string {
|
|
312
|
-
return [
|
|
313
|
-
'## Dynamic UI',
|
|
314
|
-
'',
|
|
315
|
-
'### When to use',
|
|
316
|
-
'Use dynamic UI when the response involves structured data, visual metrics, comparisons, multi-item results, charts, weather, flights, financial data, dashboards, or anything better presented visually than as plain text. Do NOT use for simple text answers, short factual replies, or casual conversation.',
|
|
317
|
-
'',
|
|
318
|
-
'**Important:** Dynamic UI availability is channel-dependent. Use the active channel onboarding playbook + transport guidance to decide whether to render UI directly or provide channel-safe text output with a clean dashboard handoff.',
|
|
319
|
-
'',
|
|
320
|
-
'### Routing rules',
|
|
321
|
-
'- **Blog posts, articles, essays, reports** → `document_create` (NOT app_create)',
|
|
322
|
-
'- **Tool auto-emissions** (e.g. `get_weather`): handled automatically — do nothing extra',
|
|
323
|
-
'- **Predefined domain data** (flights, stocks): `ui_show` with `surface_type: "dynamic_page"` and domain component classes',
|
|
324
|
-
'- **Simple structured data** (key-value, table, list): `ui_show` with `card`/`table`/`list`/`form` surface_type',
|
|
325
|
-
'- **Multi-step tasks** (ordering food, booking, purchasing, multi-phase workflows): `ui_show` with `card` surface_type + `data.template: "task_progress"` (see below)',
|
|
326
|
-
'- **Interactive apps only**: `app_create` (calculators, dashboards, tools - NOT text content)',
|
|
327
|
-
'',
|
|
328
|
-
'### Loading app tools',
|
|
329
|
-
'Most `app_*` tools (`app_create`, `app_update`, `app_file_edit`, `app_file_write`, `app_file_read`, `app_file_list`, `app_delete`, `app_list`, `app_query`) are provided by the `app-builder` skill. If they are not yet available, call `skill_load` with `id: "app-builder"` to load them. You only need to load the skill once per session. Note: `app_open` is always available as a core tool and does not require skill_load.',
|
|
330
|
-
'',
|
|
331
|
-
'### App type selection',
|
|
332
|
-
'When using `app_create`, set the `type` parameter:',
|
|
333
|
-
'- `"app"` (default) — interactive apps with data/state (calculators, dashboards, games)',
|
|
334
|
-
'- `"site"` — presentational sites (portfolios, landing pages, resumes)',
|
|
335
|
-
'',
|
|
336
|
-
'**app_create will ERROR if you try to create blog posts or articles. Use document_create instead.**',
|
|
337
|
-
'',
|
|
338
|
-
'### Using app_create (default for custom UIs)',
|
|
339
|
-
'When your user asks you to build, create, or visualize something that requires custom HTML, use `app_create`:',
|
|
340
|
-
'- Provide `name`, `html`, and `preview`. For apps, include `schema_json`; for sites (`type: "site"`), it defaults to `"{}"`',
|
|
341
|
-
'- `auto_open` defaults to true — the app opens immediately after creation',
|
|
342
|
-
'- Always include `preview`: `{ title, icon (emoji), metrics: [{ label, value }] }` for an inline chat card',
|
|
343
|
-
'- For iteration on an existing app: use `app_file_edit` for targeted code changes or `app_file_write` to rewrite a file. The surface refreshes automatically — no need to call `app_open`',
|
|
344
|
-
'- Home Base starts from a prebuilt scaffold. When updating Home Base, preserve required task-lane anchors and apply changes through `app_file_edit` or `app_file_write`',
|
|
345
|
-
'',
|
|
346
|
-
'### Quality standards',
|
|
347
|
-
'- Build immediately — make creative decisions, deliver polished output',
|
|
348
|
-
'- Anti-AI-slop rules: no flat cards with zero depth, no zero animations, 3+ text hierarchy levels, no plain backgrounds, hover+active states required on interactive elements',
|
|
349
|
-
'- Always: tight letter-spacing on headings, `clamp()` for display text, at least one accent gradient, distinct visual personality',
|
|
350
|
-
'- Toast notifications for all CRUD operations, `window.vellum.confirm()` for destructive actions',
|
|
351
|
-
'',
|
|
352
|
-
'### Using ui_show with domain components',
|
|
353
|
-
'For predefined domain data (flights, weather, stocks), write a self-contained HTML string using the domain component classes. The CSS design system (`vellum-design-system.css`) and JS widget library (`vellum-widgets.js`) are auto-injected. Call `ui_show` with `surface_type: "dynamic_page"` and `data: { html: "<your html>", preview: { title, subtitle?, description?, icon?, metrics? } }`.',
|
|
354
|
-
'',
|
|
355
|
-
'### Design system tokens',
|
|
356
|
-
'Semantic colors: `--v-bg`, `--v-surface`, `--v-surface-border`, `--v-text`, `--v-text-secondary`, `--v-text-muted`, `--v-accent`, `--v-success`, `--v-danger`, `--v-warning`.',
|
|
357
|
-
'Palettes: `--v-slate-{950..50}`, `--v-violet-*`, `--v-emerald-*`, `--v-rose-*`, `--v-amber-*`, `--v-indigo-*`.',
|
|
358
|
-
'Spacing: `--v-spacing-xs` through `--v-spacing-xxxl`. Radius: `--v-radius-sm`/`md`/`lg`/`pill`.',
|
|
359
|
-
'',
|
|
360
|
-
'### Component classes',
|
|
361
|
-
'Layout: `.v-card`, `.v-card-grid`, `.v-metric-card`, `.v-metric-grid`, `.v-data-table`, `.v-stat-row`, `.v-tabs`, `.v-accordion`, `.v-timeline`, `.v-divider`, `.v-page`, `.v-hero`, `.v-section-header`, `.v-pullquote`, `.v-comparison`, `.v-feature-grid`, `.v-feature-card`, `.v-gradient-text`, `.v-animate-in`.',
|
|
362
|
-
'Domain: `.v-flight-card`, `.v-weather-card`, `.v-stock-ticker`, `.v-billing-chart`, `.v-itinerary`, `.v-boarding-pass`, `.v-receipt`, `.v-invoice`.',
|
|
363
|
-
'UI: `.v-button` (`.secondary`/`.danger`/`.ghost`), `.v-badge`, `.v-status-badge` (`.success`/`.error`/`.warning`), `.v-progress-bar`, `.v-search-bar`, `.v-empty-state`, `.v-action-list`.',
|
|
364
|
-
'Action UI: `.v-action-bar` (`.v-action-bar-count`, `.v-action-bar-buttons`), `.v-action-progress`, `.v-group-header`, `.v-group-body`, `.v-row-removing`.',
|
|
365
|
-
'',
|
|
366
|
-
'### Widget JS APIs',
|
|
367
|
-
'```',
|
|
368
|
-
'vellum.widgets.sparkline(container, number[], {width, height, color})',
|
|
369
|
-
'vellum.widgets.barChart(container, [{label, value, color?}], {width, height, horizontal?})',
|
|
370
|
-
'vellum.widgets.lineChart(container, [{label, value}], {width, height, showDots?, showGrid?})',
|
|
371
|
-
'vellum.widgets.progressRing(container, value0to100, {size, color, label?})',
|
|
372
|
-
'vellum.widgets.sortTable(tableId) — make .v-data-table sortable',
|
|
373
|
-
'vellum.widgets.tabs(tabsId) — wire .v-tabs click behavior',
|
|
374
|
-
'vellum.openExternal(url) — open URL in default browser',
|
|
375
|
-
'vellum.openLink(url, metadata?) — open URL in user\'s browser (works everywhere incl. shared apps)',
|
|
376
|
-
'vellum.sendAction(actionId, data) — send interaction back to assistant',
|
|
377
|
-
'vellum.widgets.groupedSelect(containerId, {actionBarId?, countId?}) — wire grouped multi-select with action bar',
|
|
378
|
-
'vellum.widgets.removeItems(ids, containerId, onComplete?) — animate-remove processed items, auto-clean empty groups',
|
|
379
|
-
'```',
|
|
380
|
-
'',
|
|
381
|
-
'### Home Base interaction prompts',
|
|
382
|
-
'Home Base buttons send prefilled natural-language prompts through `vellum.sendAction`.',
|
|
383
|
-
'Treat these as normal user messages, not as direct execution commands.',
|
|
384
|
-
'- For appearance changes: keep customization color-first, ask for explicit confirmation before applying a full-dashboard update.',
|
|
385
|
-
'- For optional capability setup tasks (voice/computer control/ambient): keep them user-initiated and request permissions only when required for the chosen path.',
|
|
386
|
-
'- If a prompt is underspecified, ask one brief follow-up and continue.',
|
|
387
|
-
'',
|
|
388
|
-
'### External Links',
|
|
389
|
-
'When building apps with linkable items (search results, product cards, bookings), use `vellum.openLink(url, metadata)` to make them clickable.',
|
|
390
|
-
'Construct deep-link URLs when possible (airline booking pages, product pages, hotel reservations).',
|
|
391
|
-
'Include `metadata.provider` and `metadata.type` for context: `vellum.openLink("https://delta.com/book?flight=DL123", {provider: "delta", type: "booking"})`.',
|
|
392
|
-
'',
|
|
393
|
-
'### Example — Flight results page (ui_show with domain components)',
|
|
394
|
-
'```html',
|
|
395
|
-
'<div style="max-width:500px;margin:0 auto;display:flex;flex-direction:column;gap:12px">',
|
|
396
|
-
' <h2 style="color:var(--v-text);margin:0">Flights: IAH → LGA</h2>',
|
|
397
|
-
' <div class="v-flight-card">',
|
|
398
|
-
' <div class="v-flight-header">',
|
|
399
|
-
' <span class="v-flight-airline">Spirit</span>',
|
|
400
|
-
' <span class="v-flight-price">$120</span>',
|
|
401
|
-
' </div>',
|
|
402
|
-
' <div class="v-flight-route">',
|
|
403
|
-
' <div class="v-flight-endpoint"><div class="v-flight-time">6:38 PM</div><div class="v-flight-code">IAH</div></div>',
|
|
404
|
-
' <div class="v-flight-duration"><span>3h 28m</span><div class="v-flight-line"></div><span>Nonstop</span></div>',
|
|
405
|
-
' <div class="v-flight-endpoint"><div class="v-flight-time">11:06 PM</div><div class="v-flight-code">LGA</div></div>',
|
|
406
|
-
' </div>',
|
|
407
|
-
' </div>',
|
|
408
|
-
'</div>',
|
|
409
|
-
'```',
|
|
410
|
-
'',
|
|
411
|
-
'### Branding',
|
|
412
|
-
'A "Built on Vellum" badge is auto-injected into every dynamic page and app at the bottom-right corner. Do NOT add your own "Built on Vellum" or "Powered by Vellum" text — the badge is handled automatically by the rendering layer.',
|
|
413
|
-
'',
|
|
414
|
-
'### Tool chaining',
|
|
415
|
-
'After gathering data via tools (web search, browser, `get_weather`, APIs), synthesize results into a visual output rather than displaying raw tool outputs.',
|
|
416
|
-
'- **Weather**: `get_weather` automatically renders a dynamic page with a compact preview card. Do NOT call `ui_show` or `app_create` after `get_weather` — the weather surface is emitted directly. Just respond with a brief natural-language summary.',
|
|
417
|
-
'- **Research → Render**: When using browser/web search to research something visual (flights, hotels, products, comparisons), gather the data first, then compose it into a polished output — use `app_create` for custom UIs, or `ui_show` with domain component classes for predefined data types.',
|
|
418
|
-
'',
|
|
419
|
-
'### Presenting choices to your user',
|
|
420
|
-
'When you need your user to make a choice or provide structured input, prefer interactive UI surfaces over plain text:',
|
|
421
|
-
'- **Simple option selection** (2-8 choices): Use a `list` surface with `selectionMode: "single"`',
|
|
422
|
-
'- **Structured input** (names, settings, config): Use a `form` surface with typed fields',
|
|
423
|
-
'- **Complex configuration** (many fields, logical grouping): Use a multi-page `form` with `pages` array',
|
|
424
|
-
'- **Destructive or important actions**: Use a `confirmation` surface',
|
|
425
|
-
'- **Data review/selection**: Use a `table` surface with selectable rows',
|
|
426
|
-
'',
|
|
427
|
-
'Interactive surfaces provide a better user experience than asking your user to type their choice. Only fall back to plain text when the interaction is conversational or doesn\'t fit a structured format.',
|
|
428
|
-
'',
|
|
429
|
-
'### Task progress for multi-step workflows',
|
|
430
|
-
'When executing a multi-step task (booking a flight, purchasing, research with multiple phases), show live progress using the `task_progress` card template. Note: DoorDash ordering auto-emits a task_progress card on the first `vellum doordash` command (surface ID `doordash-progress`), so you only need `ui_update` for DoorDash flows.',
|
|
431
|
-
'',
|
|
432
|
-
'1. **Before starting**, call `ui_show` with `surface_type: "card"` and put `template: "task_progress"` inside `data`:',
|
|
433
|
-
' ```json',
|
|
434
|
-
' {',
|
|
435
|
-
' "surface_type": "card",',
|
|
436
|
-
' "data": {',
|
|
437
|
-
' "title": "Booking Flight",',
|
|
438
|
-
' "body": "",',
|
|
439
|
-
' "template": "task_progress",',
|
|
440
|
-
' "templateData": {',
|
|
441
|
-
' "title": "Booking Flight",',
|
|
442
|
-
' "status": "in_progress",',
|
|
443
|
-
' "steps": [',
|
|
444
|
-
' { "label": "Search flights", "status": "in_progress" },',
|
|
445
|
-
' { "label": "Compare options", "status": "pending" },',
|
|
446
|
-
' { "label": "Select flight", "status": "pending" },',
|
|
447
|
-
' { "label": "Complete booking", "status": "pending" }',
|
|
448
|
-
' ]',
|
|
449
|
-
' }',
|
|
450
|
-
' }',
|
|
451
|
-
' }',
|
|
452
|
-
' ```',
|
|
453
|
-
'2. **As each step completes**, call `ui_update` with the same surface ID and patch `data.templateData` (not top-level `status`/`steps`):',
|
|
454
|
-
' ```json',
|
|
455
|
-
' {',
|
|
456
|
-
' "surface_id": "<surface-id>",',
|
|
457
|
-
' "data": {',
|
|
458
|
-
' "templateData": {',
|
|
459
|
-
' "status": "in_progress",',
|
|
460
|
-
' "steps": [',
|
|
461
|
-
' { "label": "Search flights", "status": "completed", "detail": "Found 12 options" },',
|
|
462
|
-
' { "label": "Compare options", "status": "in_progress" },',
|
|
463
|
-
' { "label": "Select flight", "status": "pending" },',
|
|
464
|
-
' { "label": "Complete booking", "status": "pending" }',
|
|
465
|
-
' ]',
|
|
466
|
-
' }',
|
|
467
|
-
' }',
|
|
468
|
-
' }',
|
|
469
|
-
' ```',
|
|
470
|
-
'3. **On completion**, set `data.templateData.status` to `"completed"`. On failure, set it to `"failed"` and mark the failing step accordingly.',
|
|
471
|
-
'',
|
|
472
|
-
'Use this for ANY multi-step workflow where the user benefits from seeing structured progress instead of just "Running a command...".',
|
|
473
|
-
].join('\n');
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function buildActionableUiSection(): string {
|
|
477
|
-
return [
|
|
478
|
-
'## Actionable UI',
|
|
479
|
-
'',
|
|
480
|
-
'### When to use',
|
|
481
|
-
'When the user wants to triage, manage, or bulk-act on a collection of items (emails, files, notifications, tasks, subscriptions, contacts), generate an interactive UI that lets them review, select, and act on items directly.',
|
|
482
|
-
'',
|
|
483
|
-
'### Pattern',
|
|
484
|
-
'1. **Fetch data** — use the relevant tools to gather the items (e.g. `gmail_search`, file listing, etc.)',
|
|
485
|
-
'2. **Generate interactive UI** — render a `dynamic_page` with selectable items and action buttons',
|
|
486
|
-
'3. **User selects + clicks action** — the UI sends a `surfaceAction` with an action ID and selected item IDs',
|
|
487
|
-
'4. **Execute tools** — parse the action, call the appropriate tools (e.g. `gmail_batch_archive`, `gmail_unsubscribe`)',
|
|
488
|
-
'5. **Update UI** — use `ui_update` to remove processed items and show feedback via `widgets.toast()`',
|
|
489
|
-
'',
|
|
490
|
-
'### HTML structure',
|
|
491
|
-
'Choose the best layout for the data. Pick whatever fits the context:',
|
|
492
|
-
'- Grouped cards with checkboxes (e.g. email senders with message counts)',
|
|
493
|
-
'- Data tables with selectable rows (e.g. file listings)',
|
|
494
|
-
'- Kanban-style columns (e.g. triage into categories)',
|
|
495
|
-
'- Stacked list items with inline action buttons (e.g. notification feed)',
|
|
496
|
-
'- Any creative layout that makes sense for the data',
|
|
497
|
-
'',
|
|
498
|
-
'The key constraint: items must be selectable and action buttons must call `sendAction` with the selected item IDs.',
|
|
499
|
-
'',
|
|
500
|
-
'### CSS building blocks',
|
|
501
|
-
'- `.v-action-bar` — sticky bar at top, auto-hidden when nothing selected. Contains `.v-action-bar-count` ("N selected" label) and `.v-action-bar-buttons` (action button container)',
|
|
502
|
-
'- `.v-action-progress` — inline progress bar that replaces the action bar during bulk operations',
|
|
503
|
-
'- `.v-group-header` — collapsible section header with checkbox, title, count badge, and chevron',
|
|
504
|
-
'- `.v-group-body` — indented container for group items',
|
|
505
|
-
'- `.v-row-removing` — fade-out + slide animation class for processed items',
|
|
506
|
-
'',
|
|
507
|
-
'### Action data conventions',
|
|
508
|
-
'- Use semantic action IDs: `archive`, `unsubscribe`, `delete`, `move`, `mark_read`, etc.',
|
|
509
|
-
'- Always include selected item IDs: `sendAction("archive", { ids: ["msg_1", "msg_2"] })`',
|
|
510
|
-
'- For actions needing extra context, include it: `sendAction("move", { ids: [...], destination: "folder" })`',
|
|
511
|
-
'',
|
|
512
|
-
'### Processing flow',
|
|
513
|
-
'1. Parse the `surfaceAction` to get the action ID and data',
|
|
514
|
-
'2. Use `vellum.confirm(title, message)` for destructive actions (delete, unsubscribe) before executing',
|
|
515
|
-
'3. Call the relevant tools with the item IDs',
|
|
516
|
-
'4. Use `ui_update` to update the surface HTML (remove processed items, update counts)',
|
|
517
|
-
'5. Show `widgets.toast()` for feedback: success count, partial failure info',
|
|
518
|
-
'',
|
|
519
|
-
'### Error handling',
|
|
520
|
-
'- Handle partial failures: if 8 of 10 items succeed, remove the 8 successful ones and toast "Archived 8 items. 2 failed — try again."',
|
|
521
|
-
'- Keep failed items visible and selectable so the user can retry',
|
|
522
|
-
'',
|
|
523
|
-
'### Surface lifecycle',
|
|
524
|
-
'- Use `ui_show` with `display: "panel"` to keep the surface open alongside chat',
|
|
525
|
-
'- The surface stays alive for multiple action rounds (select → act → select more → act again)',
|
|
526
|
-
'- Use `widgets.groupedSelect()` to wire up grouped multi-select with action bar auto-show/hide',
|
|
527
|
-
'- Use `widgets.removeItems()` to animate processed items out and auto-clean empty groups',
|
|
528
|
-
].join('\n');
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function buildDocumentCreationSection(): string {
|
|
532
|
-
return [
|
|
533
|
-
'## Document Creation Workflow',
|
|
534
|
-
'',
|
|
535
|
-
'When creating documents with `document_create` (see Tool Routing for when to use this):',
|
|
536
|
-
'',
|
|
537
|
-
'**IMPORTANT: Call tools immediately, not after conversational preamble.**',
|
|
538
|
-
'Example: User says "write a blog post about X" → You immediately call `document_create`, then explain what you\'re doing.',
|
|
539
|
-
'',
|
|
540
|
-
'### Workflow Steps',
|
|
541
|
-
'1. **Create the document**: Call `document_create` with a title (inferred from the request)',
|
|
542
|
-
' - The editor opens in full-screen workspace mode with chat docked to the side',
|
|
543
|
-
' - The user sees a rich text editor powered by Toast UI Editor',
|
|
544
|
-
'',
|
|
545
|
-
'2. **Write content**: Generate the content in Markdown format',
|
|
546
|
-
' - Write naturally and continuously',
|
|
547
|
-
' - Use proper Markdown structure: `#` for titles, `##` for sections, `###` for subsections',
|
|
548
|
-
' - Use **bold** and *italic* for emphasis',
|
|
549
|
-
' - Include code blocks with ` ```language ` syntax',
|
|
550
|
-
' - Add tables, lists, blockquotes as appropriate',
|
|
551
|
-
'',
|
|
552
|
-
'3. **CRITICAL - Stream content in chunks**: You MUST call `document_update` multiple times, NOT just once',
|
|
553
|
-
' - Break your content into logical chunks (paragraphs, sections, or every 200-300 words)',
|
|
554
|
-
' - Call `document_update` with `mode: "append"` for EACH chunk separately',
|
|
555
|
-
' - DO NOT generate all content and send it in one call - this defeats the purpose of streaming',
|
|
556
|
-
' - Think: "First paragraph" → call document_update → "Second paragraph" → call document_update → etc.',
|
|
557
|
-
' - The user experiences real-time content appearing as you write, not a dump at the end',
|
|
558
|
-
'',
|
|
559
|
-
'4. **Respond to edits**: When the user requests changes via the docked chat',
|
|
560
|
-
' - Listen for edit requests like "make the intro shorter", "add a section about X"',
|
|
561
|
-
' - Generate the updated content',
|
|
562
|
-
' - Use `document_update` with appropriate mode (replace for full rewrites, append for additions)',
|
|
563
|
-
'',
|
|
564
|
-
'### Content quality standards',
|
|
565
|
-
'- Write in clear, engaging prose appropriate for the content type',
|
|
566
|
-
'- Use active voice and vary sentence structure',
|
|
567
|
-
'- Break content into logical sections with descriptive headings',
|
|
568
|
-
'- Include transitions between sections',
|
|
569
|
-
'- For technical content: use code blocks with syntax highlighting',
|
|
570
|
-
'- For data-heavy content: use Markdown tables',
|
|
571
|
-
'',
|
|
572
|
-
'### Example flow',
|
|
573
|
-
'```',
|
|
574
|
-
'User: "Write a blog post about the future of AI"',
|
|
575
|
-
'',
|
|
576
|
-
'Assistant: "I\'ll create a document for your blog post about the future of AI."',
|
|
577
|
-
' → Calls document_create with title: "The Future of AI"',
|
|
578
|
-
'',
|
|
579
|
-
'Assistant: (generates content in SEPARATE chunks - one document_update call per chunk)',
|
|
580
|
-
' → document_update with mode: "append", content: "# The Future of AI\\n\\nArtificial intelligence..."',
|
|
581
|
-
' → document_update with mode: "append", content: "## Current State\\n\\nToday\'s AI landscape..."',
|
|
582
|
-
' → document_update with mode: "append", content: "The past decade has witnessed..."',
|
|
583
|
-
' → document_update with mode: "append", content: "## Emerging Trends\\n\\nLooking ahead..."',
|
|
584
|
-
'',
|
|
585
|
-
'User (via docked chat): "Add a section about ethical considerations"',
|
|
586
|
-
'',
|
|
587
|
-
'Assistant: "I\'ll add a section on AI ethics."',
|
|
588
|
-
' → document_update with mode: "append", content: "## Ethical Considerations\\n\\n..."',
|
|
589
|
-
'```',
|
|
590
|
-
'',
|
|
591
|
-
'### Important notes',
|
|
592
|
-
'- Documents are automatically saved and accessible via the Generated panel',
|
|
593
|
-
'- Users can manually edit documents at any time - your role is to help generate and refine content',
|
|
594
|
-
'- The editor supports drag-and-drop images, which are converted to base64 inline',
|
|
595
|
-
'- Word count is tracked automatically and displayed to the user',
|
|
596
|
-
'- Acknowledge the document creation in chat before opening the editor',
|
|
597
|
-
].join('\n');
|
|
598
|
-
}
|
|
599
243
|
|
|
600
244
|
export function buildStarterTaskPlaybookSection(): string {
|
|
601
245
|
return [
|
|
@@ -764,6 +408,66 @@ function buildAccessPreferenceSection(): string {
|
|
|
764
408
|
'- Can I get the data via web_fetch?',
|
|
765
409
|
'',
|
|
766
410
|
'If yes to any of these, use that path instead of the browser.',
|
|
411
|
+
...(isMacOS() ? [
|
|
412
|
+
'',
|
|
413
|
+
'### macOS App Automation',
|
|
414
|
+
'',
|
|
415
|
+
'When interacting with native macOS apps or performing system-level actions, prefer **osascript**',
|
|
416
|
+
'via host_bash over browser automation or computer-use.',
|
|
417
|
+
'',
|
|
418
|
+
'The following apps support AppleScript and should be automated via osascript:',
|
|
419
|
+
'',
|
|
420
|
+
'**Communication:** Messages, Mail, Microsoft Outlook, FaceTime',
|
|
421
|
+
'**Contacts & Calendar:** Contacts, Calendar, Reminders',
|
|
422
|
+
'**Notes & Writing:** Notes, TextEdit, Pages, BBEdit, CotEditor',
|
|
423
|
+
'**Files & Finder:** Finder, Path Finder',
|
|
424
|
+
'**Browsers:** Safari, Google Chrome',
|
|
425
|
+
'**Music & Media:** Music (iTunes), Spotify, VLC, Podcasts, TV',
|
|
426
|
+
'**Productivity:** OmniFocus, Things 3, OmniOutliner, OmniPlan, OmniGraffle',
|
|
427
|
+
'**Office:** Microsoft Word, Microsoft Excel, Numbers, Keynote',
|
|
428
|
+
'**Developer tools:** Xcode, Terminal, iTerm2, Script Editor',
|
|
429
|
+
'**System:** Finder, System Events (UI scripting for any app), System Settings',
|
|
430
|
+
'**Automation:** Keyboard Maestro, Alfred, Automator',
|
|
431
|
+
'**Creative:** Adobe Photoshop, Final Cut Pro',
|
|
432
|
+
'',
|
|
433
|
+
'For any other app, try osascript first — check scriptability with:',
|
|
434
|
+
'```bash',
|
|
435
|
+
'osascript -e \'tell application "AppName" to get name\'',
|
|
436
|
+
'```',
|
|
437
|
+
'',
|
|
438
|
+
'Common examples:',
|
|
439
|
+
'```bash',
|
|
440
|
+
'# Send an iMessage',
|
|
441
|
+
'osascript -e \'tell application "Messages" to send "Hello!" to buddy "user@example.com"\'',
|
|
442
|
+
'',
|
|
443
|
+
'# Look up a contact',
|
|
444
|
+
'osascript -e \'tell application "Contacts" to get {name, phones} of every person whose name contains "Marina"\'',
|
|
445
|
+
'',
|
|
446
|
+
'# Read upcoming calendar events',
|
|
447
|
+
'osascript -e \'tell application "Calendar" to get summary of every event of calendar "Home" whose start date > (current date)\'',
|
|
448
|
+
'',
|
|
449
|
+
'# Create a reminder',
|
|
450
|
+
'osascript -e \'tell application "Reminders" to make new reminder with properties {name:"Buy milk", due date:((current date) + 1 * hours)}\'',
|
|
451
|
+
'',
|
|
452
|
+
'# Send an email',
|
|
453
|
+
'osascript -e \'tell application "Mail" to send (make new outgoing message with properties {subject:"Hi", content:"Hello", visible:true})\'',
|
|
454
|
+
'',
|
|
455
|
+
'# Create a note',
|
|
456
|
+
'osascript -e \'tell application "Notes" to make new note at folder "Notes" with properties {body:"My note"}\'',
|
|
457
|
+
'',
|
|
458
|
+
'# Open a URL in Safari',
|
|
459
|
+
'osascript -e \'tell application "Safari" to open location "https://example.com"\'',
|
|
460
|
+
'',
|
|
461
|
+
'# Play/pause Music',
|
|
462
|
+
'osascript -e \'tell application "Music" to playpause\'',
|
|
463
|
+
'',
|
|
464
|
+
'# Display a system notification',
|
|
465
|
+
'osascript -e \'display notification "Done!" with title "Vellum"\'',
|
|
466
|
+
'```',
|
|
467
|
+
'',
|
|
468
|
+
'osascript (AppleScript/JXA) has direct, reliable access to macOS app APIs and system events.',
|
|
469
|
+
'Use it whenever the task involves a native macOS app or system-level interaction.',
|
|
470
|
+
] : []),
|
|
767
471
|
].join('\n');
|
|
768
472
|
}
|
|
769
473
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: "Google OAuth Setup"
|
|
3
3
|
description: "Create Google Cloud OAuth credentials for Gmail integration using browser automation"
|
|
4
4
|
user-invocable: true
|
|
5
|
-
includes: ["browser"]
|
|
5
|
+
includes: ["browser", "public-ingress"]
|
|
6
6
|
metadata: {"vellum": {"emoji": "\ud83d\udd11"}}
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -10,6 +10,10 @@ You are helping your user create Google Cloud OAuth credentials so the Gmail and
|
|
|
10
10
|
|
|
11
11
|
**Tone:** Be friendly and reassuring throughout. Narrate what you're doing in plain language so the user always knows what's happening. After each step, briefly confirm what was accomplished before moving on.
|
|
12
12
|
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
Before starting, check that `ingress.publicBaseUrl` is configured (Settings > Public Ingress, or `INGRESS_PUBLIC_BASE_URL` env var). If it is not set, load and execute the **public-ingress** skill first (`skill_load` with `skill: "public-ingress"`) to set up an ngrok tunnel and persist the public URL. The OAuth redirect URI depends on this value.
|
|
16
|
+
|
|
13
17
|
## Before You Start
|
|
14
18
|
|
|
15
19
|
Tell the user:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: "Slack OAuth Setup"
|
|
3
3
|
description: "Create Slack App and OAuth credentials for Slack integration using browser automation"
|
|
4
4
|
user-invocable: true
|
|
5
|
-
includes: ["browser"]
|
|
5
|
+
includes: ["browser", "public-ingress"]
|
|
6
6
|
metadata: {"vellum": {"emoji": "🔑"}}
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -10,6 +10,10 @@ You are helping your user create a Slack App and OAuth credentials so the Messag
|
|
|
10
10
|
|
|
11
11
|
**Tone:** Be friendly and reassuring throughout. Narrate what you're doing in plain language so the user always knows what's happening. After each step, briefly confirm what was accomplished before moving on.
|
|
12
12
|
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
Before starting, check that `ingress.publicBaseUrl` is configured (Settings > Public Ingress, or `INGRESS_PUBLIC_BASE_URL` env var). If it is not set, load and execute the **public-ingress** skill first (`skill_load` with `skill: "public-ingress"`) to set up an ngrok tunnel and persist the public URL. The OAuth redirect URI depends on this value.
|
|
16
|
+
|
|
13
17
|
## Before You Start
|
|
14
18
|
|
|
15
19
|
Tell the user:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: "Telegram Setup"
|
|
3
3
|
description: "Connect a Telegram bot to the Vellum Assistant gateway with automated webhook registration and credential storage"
|
|
4
4
|
user-invocable: true
|
|
5
|
+
includes: ["public-ingress"]
|
|
5
6
|
metadata: {"vellum": {"emoji": "\ud83e\udd16"}}
|
|
6
7
|
---
|
|
7
8
|
|
|
@@ -10,7 +11,7 @@ You are helping your user connect a Telegram bot to the Vellum Assistant gateway
|
|
|
10
11
|
## What You Need
|
|
11
12
|
|
|
12
13
|
1. **Bot token** from Telegram's @BotFather (the user provides this)
|
|
13
|
-
2. **Gateway webhook URL** — derived from the canonical ingress setting: `${ingress.publicBaseUrl}/webhooks/telegram`. The gateway is the only publicly reachable endpoint; Telegram sends webhooks to the gateway, which validates and forwards them to the assistant runtime internally. If `ingress.publicBaseUrl` is configured
|
|
14
|
+
2. **Gateway webhook URL** — derived from the canonical ingress setting: `${ingress.publicBaseUrl}/webhooks/telegram`. The gateway is the only publicly reachable endpoint; Telegram sends webhooks to the gateway, which validates and forwards them to the assistant runtime internally. If `ingress.publicBaseUrl` is not configured, load and execute the **public-ingress** skill first (`skill_load` with `skill: "public-ingress"`) to set up an ngrok tunnel and persist the URL before continuing.
|
|
14
15
|
|
|
15
16
|
If the user has already provided the bot token in the conversation, use it directly. Otherwise, ask for it.
|
|
16
17
|
|
|
@@ -26,6 +26,10 @@ import type {
|
|
|
26
26
|
import { log, CONFIG_RELOAD_DEBOUNCE_MS, defineHandlers, type HandlerContext } from './shared.js';
|
|
27
27
|
import { MODEL_TO_PROVIDER } from '../session-slash.js';
|
|
28
28
|
|
|
29
|
+
// Snapshot the env-provided value at module load time so we can restore it
|
|
30
|
+
// when the user clears a Settings-set override.
|
|
31
|
+
const ORIGINAL_INGRESS_ENV = process.env.INGRESS_PUBLIC_BASE_URL;
|
|
32
|
+
|
|
29
33
|
export function handleModelGet(socket: net.Socket, ctx: HandlerContext): void {
|
|
30
34
|
const config = getConfig();
|
|
31
35
|
const configured = Object.keys(config.apiKeys).filter((k) => !!config.apiKeys[k]);
|
|
@@ -416,7 +420,8 @@ export function handleIngressConfig(
|
|
|
416
420
|
const raw = loadRawConfig();
|
|
417
421
|
const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
|
|
418
422
|
const publicBaseUrl = (ingress.publicBaseUrl as string) ?? '';
|
|
419
|
-
|
|
423
|
+
const enabled = (ingress.enabled as boolean) ?? false;
|
|
424
|
+
ctx.send(socket, { type: 'ingress_config_response', enabled, publicBaseUrl, localGatewayTarget, success: true });
|
|
420
425
|
} else if (msg.action === 'set') {
|
|
421
426
|
const value = (msg.publicBaseUrl ?? '').trim().replace(/\/+$/, '');
|
|
422
427
|
const raw = loadRawConfig();
|
|
@@ -424,10 +429,13 @@ export function handleIngressConfig(
|
|
|
424
429
|
// Update ingress.publicBaseUrl — this is the single source of truth for
|
|
425
430
|
// the canonical public ingress URL. The gateway receives this value via
|
|
426
431
|
// the INGRESS_PUBLIC_BASE_URL env var at spawn time (see hatch.ts).
|
|
427
|
-
//
|
|
428
|
-
//
|
|
432
|
+
// The gateway also validates Twilio signatures against forwarded public
|
|
433
|
+
// URL headers, so local tunnel updates generally apply without restarts.
|
|
429
434
|
const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
|
|
430
435
|
ingress.publicBaseUrl = value || undefined;
|
|
436
|
+
if (msg.enabled !== undefined) {
|
|
437
|
+
ingress.enabled = msg.enabled;
|
|
438
|
+
}
|
|
431
439
|
|
|
432
440
|
const wasSuppressed = ctx.suppressConfigReload;
|
|
433
441
|
ctx.setSuppressConfigReload(true);
|
|
@@ -443,24 +451,27 @@ export function handleIngressConfig(
|
|
|
443
451
|
ctx.debounceTimers.set('__suppress_reset__', resetTimer);
|
|
444
452
|
|
|
445
453
|
// Propagate to the gateway's process environment so it picks up the
|
|
446
|
-
// new URL
|
|
454
|
+
// new URL when it is restarted. For the local-deployment path the
|
|
447
455
|
// gateway runs as a child process that inherited the assistant's env,
|
|
448
456
|
// so updating process.env here ensures the value is visible when the
|
|
449
457
|
// gateway is restarted (e.g. by the self-upgrade skill or a manual
|
|
450
458
|
// `pkill -f gateway`).
|
|
451
459
|
if (value) {
|
|
452
460
|
process.env.INGRESS_PUBLIC_BASE_URL = value;
|
|
461
|
+
} else if (ORIGINAL_INGRESS_ENV !== undefined) {
|
|
462
|
+
process.env.INGRESS_PUBLIC_BASE_URL = ORIGINAL_INGRESS_ENV;
|
|
463
|
+
} else {
|
|
464
|
+
delete process.env.INGRESS_PUBLIC_BASE_URL;
|
|
453
465
|
}
|
|
454
|
-
// When cleared, do NOT delete the env var — the original env-provided
|
|
455
|
-
// value (if any) serves as a fallback per the documented precedence model.
|
|
456
466
|
|
|
457
|
-
|
|
467
|
+
const enabled = (ingress.enabled as boolean) ?? false;
|
|
468
|
+
ctx.send(socket, { type: 'ingress_config_response', enabled, publicBaseUrl: value, localGatewayTarget, success: true });
|
|
458
469
|
} else {
|
|
459
|
-
ctx.send(socket, { type: 'ingress_config_response', publicBaseUrl: '', localGatewayTarget, success: false, error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}` });
|
|
470
|
+
ctx.send(socket, { type: 'ingress_config_response', enabled: false, publicBaseUrl: '', localGatewayTarget, success: false, error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}` });
|
|
460
471
|
}
|
|
461
472
|
} catch (err) {
|
|
462
473
|
const message = err instanceof Error ? err.message : String(err);
|
|
463
|
-
ctx.send(socket, { type: 'ingress_config_response', publicBaseUrl: '', localGatewayTarget, success: false, error: message });
|
|
474
|
+
ctx.send(socket, { type: 'ingress_config_response', enabled: false, publicBaseUrl: '', localGatewayTarget, success: false, error: message });
|
|
464
475
|
}
|
|
465
476
|
}
|
|
466
477
|
|
|
@@ -66,9 +66,10 @@ export function handleHomeBaseGet(
|
|
|
66
66
|
},
|
|
67
67
|
});
|
|
68
68
|
} catch (err) {
|
|
69
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
70
69
|
log.error({ err }, 'Failed to resolve home base metadata');
|
|
71
|
-
|
|
70
|
+
// Return null rather than surfacing an error banner to the user —
|
|
71
|
+
// the chat still works fine without home base metadata.
|
|
72
|
+
ctx.send(socket, { type: 'home_base_get_response', homeBase: null });
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
|