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.
- package/bun.lock +6 -2
- package/package.json +2 -2
- package/src/__tests__/gateway-only-enforcement.test.ts +9 -35
- package/src/__tests__/oauth2-gateway-transport.test.ts +14 -33
- package/src/__tests__/skills.test.ts +2 -2
- package/src/__tests__/twilio-routes.test.ts +78 -153
- package/src/__tests__/twitter-auth-handler.test.ts +1 -1
- package/src/cli/main-screen.tsx +15 -117
- package/src/config/bundled-skills/macos-automation/SKILL.md +66 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +334 -0
- package/src/config/system-prompt.ts +9 -59
- package/src/daemon/lifecycle.ts +36 -7
- package/src/home-base/prebuilt/seed.ts +1 -1
- package/src/memory/db.ts +36 -0
- package/src/security/oauth2.ts +8 -8
- package/src/util/logger.ts +4 -4
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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)`);
|
package/src/security/oauth2.ts
CHANGED
|
@@ -103,7 +103,7 @@ async function exchangeCodeForTokens(
|
|
|
103
103
|
|
|
104
104
|
if (!tokenResp.ok) {
|
|
105
105
|
const rawBody = await tokenResp.text().catch(() => '');
|
|
106
|
-
|
|
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 } =
|
|
153
|
-
const { getOAuthCallbackUrl } =
|
|
154
|
-
const { registerPendingCallback } =
|
|
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
|
-
|
|
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 } =
|
|
206
|
-
const { getPublicBaseUrl } =
|
|
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
|
-
|
|
251
|
+
const safeDetail: Record<string, unknown> = {};
|
|
252
252
|
let errorCode = '';
|
|
253
253
|
try {
|
|
254
254
|
const parsed = JSON.parse(rawBody) as Record<string, unknown>;
|
package/src/util/logger.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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' },
|
|
152
|
+
rootLogger = pino({ level: process.env.VELLUM_DEBUG === '1' ? 'debug' : 'info' }, pinoPretty({ destination: 2 }));
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
return rootLogger;
|