vellum 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { randomBytes } from 'node:crypto';
3
- import { cpSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, existsSync, openSync, closeSync, chmodSync } from 'node:fs';
4
- import { join, resolve } from 'node:path';
3
+ import { mkdirSync, readFileSync, writeFileSync, unlinkSync, existsSync, openSync, closeSync, chmodSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import { dirname, join, resolve } from 'node:path';
5
6
  import { config as dotenvConfig } from 'dotenv';
6
7
  import * as Sentry from '@sentry/node';
7
8
  import {
@@ -21,6 +22,7 @@ import { initializeProviders } from '../providers/registry.js';
21
22
  import { initializeTools } from '../tools/registry.js';
22
23
  import { loadConfig } from '../config/loader.js';
23
24
  import { ensurePromptFiles } from '../config/system-prompt.js';
25
+ import { loadPrebuiltHtml } from '../home-base/prebuilt/seed.js';
24
26
  import { DaemonServer } from './server.js';
25
27
  import { listWorkItems, updateWorkItem } from '../work-items/work-item-store.js';
26
28
  import { getLogger, initLogger } from '../util/logger.js';
@@ -271,11 +273,38 @@ export async function runDaemon(): Promise<void> {
271
273
 
272
274
  log.info('Daemon startup: migrations complete');
273
275
 
274
- const seedDir = process.env.INTERFACES_SEED_DIR?.trim();
275
- if (seedDir && existsSync(seedDir)) {
276
- const interfacesDir = getInterfacesDir();
277
- cpSync(seedDir, interfacesDir, { recursive: true });
278
- log.info({ seedDir, interfacesDir }, 'Seeded initial interface files');
276
+ // Seed the TUI main-window interface from the CLI package's DefaultMainScreen
277
+ // component so the remote runtime can serve it without the old INTERFACES_SEED
278
+ // environment variable.
279
+ const tuiDir = join(getInterfacesDir(), 'tui');
280
+ const mainWindowPath = join(tuiDir, 'main-window.tsx');
281
+ if (!existsSync(mainWindowPath)) {
282
+ try {
283
+ const require = createRequire(import.meta.url);
284
+ const cliPkgPath = require.resolve('@vellumai/cli/package.json');
285
+ const cliRoot = dirname(cliPkgPath);
286
+ const source = readFileSync(join(cliRoot, 'src', 'components', 'DefaultMainScreen.tsx'), 'utf-8');
287
+ mkdirSync(tuiDir, { recursive: true });
288
+ writeFileSync(mainWindowPath, source);
289
+ log.info('Seeded tui/main-window.tsx from @vellumai/cli');
290
+ } catch (err) {
291
+ log.warn({ err }, 'Could not seed tui/main-window.tsx from CLI package');
292
+ }
293
+ }
294
+
295
+ // Seed the vellum-desktop interface from the prebuilt Home Base HTML if it
296
+ // doesn't already exist. This ensures the Home tab renders immediately
297
+ // on first launch for both local and remote hatches.
298
+ const desktopIndexPath = join(getInterfacesDir(), 'vellum-desktop', 'index.html');
299
+ if (!existsSync(desktopIndexPath)) {
300
+ const prebuiltHtml = loadPrebuiltHtml();
301
+ if (prebuiltHtml) {
302
+ mkdirSync(join(getInterfacesDir(), 'vellum-desktop'), { recursive: true });
303
+ writeFileSync(desktopIndexPath, prebuiltHtml);
304
+ log.info('Seeded vellum-desktop/index.html from prebuilt Home Base');
305
+ } else {
306
+ log.warn('Could not seed vellum-desktop/index.html — prebuilt HTML not found (missing embedded index.html in home-base/prebuilt/)');
307
+ }
279
308
  }
280
309
 
281
310
  log.info('Daemon startup: installing templates and initializing DB');
@@ -31,7 +31,7 @@ function loadSeedMetadata(): SeedMetadata {
31
31
  return seedMetadataJson as SeedMetadata;
32
32
  }
33
33
 
34
- function loadPrebuiltHtml(): string | null {
34
+ export function loadPrebuiltHtml(): string | null {
35
35
  try {
36
36
  return readFileSync(join(getPrebuiltDir(), 'index.html'), 'utf-8');
37
37
  } catch {
package/src/memory/db.ts CHANGED
@@ -592,6 +592,42 @@ export function initializeDb(): void {
592
592
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_items_scope_id ON memory_items(scope_id)`);
593
593
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_memory_summaries_scope_id ON memory_summaries(scope_id)`);
594
594
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_conversation_keys_key ON conversation_keys(conversation_key)`);
595
+ // Deduplicate before creating unique index — existing DBs may have duplicate content_hash values.
596
+ // Re-point message_attachments to the survivor (MIN rowid per content_hash), then delete dupes.
597
+ {
598
+ const raw = (database as unknown as { $client: Database }).$client;
599
+ raw.exec(/*sql*/ `
600
+ UPDATE message_attachments
601
+ SET attachment_id = (
602
+ SELECT a_survivor.id
603
+ FROM attachments a_survivor
604
+ WHERE a_survivor.content_hash = (
605
+ SELECT a_dup.content_hash FROM attachments a_dup
606
+ WHERE a_dup.id = message_attachments.attachment_id
607
+ )
608
+ ORDER BY a_survivor.rowid
609
+ LIMIT 1
610
+ )
611
+ WHERE attachment_id IN (
612
+ SELECT id FROM attachments
613
+ WHERE content_hash IS NOT NULL
614
+ AND rowid NOT IN (
615
+ SELECT MIN(rowid) FROM attachments
616
+ WHERE content_hash IS NOT NULL
617
+ GROUP BY content_hash
618
+ )
619
+ )
620
+ `);
621
+ raw.exec(/*sql*/ `
622
+ DELETE FROM attachments
623
+ WHERE content_hash IS NOT NULL
624
+ AND rowid NOT IN (
625
+ SELECT MIN(rowid) FROM attachments
626
+ WHERE content_hash IS NOT NULL
627
+ GROUP BY content_hash
628
+ )
629
+ `);
630
+ }
595
631
  database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_attachments_content_dedup ON attachments(content_hash) WHERE content_hash IS NOT NULL`);
596
632
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_message_attachments_message_id ON message_attachments(message_id)`);
597
633
  database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_message_attachments_attachment_id ON message_attachments(attachment_id)`);
@@ -103,7 +103,7 @@ async function exchangeCodeForTokens(
103
103
 
104
104
  if (!tokenResp.ok) {
105
105
  const rawBody = await tokenResp.text().catch(() => '');
106
- let safeDetail: Record<string, unknown> = {};
106
+ const safeDetail: Record<string, unknown> = {};
107
107
  let errorCode = '';
108
108
  try {
109
109
  const parsed = JSON.parse(rawBody) as Record<string, unknown>;
@@ -149,9 +149,9 @@ async function runGatewayFlow(
149
149
  codeChallenge: string,
150
150
  state: string,
151
151
  ): Promise<OAuth2FlowResult> {
152
- const { loadConfig } = require('../config/loader.js') as typeof import('../config/loader.js');
153
- const { getOAuthCallbackUrl } = require('../inbound/public-ingress-urls.js') as typeof import('../inbound/public-ingress-urls.js');
154
- const { registerPendingCallback } = require('./oauth-callback-registry.js') as typeof import('./oauth-callback-registry.js');
152
+ const { loadConfig } = await import('../config/loader.js');
153
+ const { getOAuthCallbackUrl } = await import('../inbound/public-ingress-urls.js');
154
+ const { registerPendingCallback } = await import('./oauth-callback-registry.js');
155
155
 
156
156
  const appConfig = loadConfig();
157
157
  const redirectUri = getOAuthCallbackUrl(appConfig);
@@ -193,7 +193,7 @@ async function runGatewayFlow(
193
193
  export async function startOAuth2Flow(
194
194
  config: OAuth2Config,
195
195
  callbacks: OAuth2FlowCallbacks,
196
- options?: OAuth2FlowOptions,
196
+ _options?: OAuth2FlowOptions,
197
197
  ): Promise<OAuth2FlowResult> {
198
198
  const codeVerifier = generateCodeVerifier();
199
199
  const codeChallenge = generateCodeChallenge(codeVerifier);
@@ -202,8 +202,8 @@ export async function startOAuth2Flow(
202
202
  // Always enforce gateway transport and require a public ingress URL
203
203
  let hasPublicUrl = false;
204
204
  try {
205
- const { loadConfig } = require('../config/loader.js') as typeof import('../config/loader.js');
206
- const { getPublicBaseUrl } = require('../inbound/public-ingress-urls.js') as typeof import('../inbound/public-ingress-urls.js');
205
+ const { loadConfig } = await import('../config/loader.js');
206
+ const { getPublicBaseUrl } = await import('../inbound/public-ingress-urls.js');
207
207
  getPublicBaseUrl(loadConfig());
208
208
  hasPublicUrl = true;
209
209
  } catch {
@@ -248,7 +248,7 @@ export async function refreshOAuth2Token(
248
248
 
249
249
  if (!resp.ok) {
250
250
  const rawBody = await resp.text().catch(() => '');
251
- let safeDetail: Record<string, unknown> = {};
251
+ const safeDetail: Record<string, unknown> = {};
252
252
  let errorCode = '';
253
253
  try {
254
254
  const parsed = JSON.parse(rawBody) as Record<string, unknown>;
@@ -55,7 +55,7 @@ let activeLogFileConfig: LogFileConfig | null = null;
55
55
 
56
56
  function buildRotatingLogger(config: LogFileConfig): pino.Logger {
57
57
  if (!config.dir) {
58
- return pino({ name: 'assistant' });
58
+ return pino({ name: 'assistant' }, pinoPretty({ destination: 1 }));
59
59
  }
60
60
 
61
61
  if (!existsSync(config.dir)) {
@@ -86,7 +86,7 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
86
86
  { name: 'assistant', level },
87
87
  pino.multistream([
88
88
  { stream: fileStream, level: 'info' as const },
89
- { stream: pino.destination(1), level: 'info' as const },
89
+ { stream: pinoPretty({ destination: 1 }), level: 'info' as const },
90
90
  ]),
91
91
  );
92
92
  }
@@ -142,14 +142,14 @@ function getRootLogger(): pino.Logger {
142
142
  { level: 'info' },
143
143
  pino.multistream([
144
144
  { stream: fileStream, level: 'info' as const },
145
- { stream: pino.destination(1), level: 'info' as const },
145
+ { stream: pinoPretty({ destination: 1 }), level: 'info' as const },
146
146
  ]),
147
147
  );
148
148
  } else {
149
149
  rootLogger = pino({ level: 'info' }, fileStream);
150
150
  }
151
151
  } catch {
152
- rootLogger = pino({ level: process.env.VELLUM_DEBUG === '1' ? 'debug' : 'info' }, pino.destination(2));
152
+ rootLogger = pino({ level: process.env.VELLUM_DEBUG === '1' ? 'debug' : 'info' }, pinoPretty({ destination: 2 }));
153
153
  }
154
154
  }
155
155
  return rootLogger;