feat(setup): add memory, automation, security, and gateway setup flows

This commit is contained in:
William Valentin
2026-02-10 09:34:04 -08:00
parent b673632b0f
commit 182d86957b
5 changed files with 196 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
import type { Prompter } from './prompts.js';
import type { ConfigBuilder } from './config.js';
export async function setupAutomation(p: Prompter, builder: ConfigBuilder): Promise<void> {
const cron = await p.confirm('Enable cron scheduler?', false);
if (cron) {
builder.setCronEnabled();
p.println('✓ Cron enabled — add jobs to config.yaml later');
}
const webhooks = await p.confirm('Enable webhook receiver?', false);
if (webhooks) {
const secret = await p.ask('Webhook shared secret (optional)', '');
builder.setWebhooksEnabled(secret || undefined);
p.println('✓ Webhooks enabled');
}
const gmail = await p.confirm('Enable Gmail watcher?', false);
if (gmail) {
const creds = await p.ask('OAuth credentials file', '~/.config/flynn/gmail-credentials.json');
builder.setGmailEnabled(creds, 'webchat', 'gmail');
p.println('✓ Gmail watcher enabled');
}
}
+26
View File
@@ -0,0 +1,26 @@
import type { Prompter } from './prompts.js';
import type { ConfigBuilder } from './config.js';
export async function setupGateway(p: Prompter, builder: ConfigBuilder): Promise<void> {
const port = await p.ask('Gateway port', '18800');
builder.setGatewayPort(parseInt(port, 10) || 18800);
const wantAuth = await p.confirm('Set auth token?', false);
if (wantAuth) {
const token = await p.password('Auth token');
builder.setGatewayToken(token);
p.println('✓ Gateway auth token set');
}
const tailscale = await p.confirm('Enable Tailscale Serve?', false);
if (tailscale) {
builder.setTailscaleServe(true);
p.println('✓ Tailscale Serve enabled');
}
const lock = await p.confirm('Enable gateway lock (single client)?', false);
if (lock) {
builder.setGatewayLock(true);
p.println('✓ Gateway lock enabled');
}
}
+45
View File
@@ -0,0 +1,45 @@
import type { Prompter } from './prompts.js';
import type { ConfigBuilder } from './config.js';
const EMBEDDING_PROVIDERS = [
{ label: 'OpenAI (recommended)', value: 'openai' },
{ label: 'Gemini', value: 'gemini' },
{ label: 'Ollama (local)', value: 'ollama' },
{ label: 'Voyage AI', value: 'voyage' },
];
const REUSABLE_PROVIDERS = ['openai', 'gemini'];
export async function setupMemory(p: Prompter, builder: ConfigBuilder): Promise<void> {
const enable = await p.confirm('Enable vector search for semantic memory?', false);
if (!enable) return;
const provider = await p.choose('Embedding provider:', EMBEDDING_PROVIDERS);
const config = builder.build();
const modelApiKey = findReusableApiKey(config, provider);
let apiKey: string | undefined;
if (REUSABLE_PROVIDERS.includes(provider) && modelApiKey) {
const reuse = await p.confirm('Reuse API key from model provider?', true);
apiKey = reuse ? modelApiKey : await p.password('API key');
} else if (provider !== 'ollama') {
apiKey = await p.password('API key');
}
builder.setMemoryEmbedding({
provider,
api_key: apiKey,
endpoint: provider === 'ollama' ? 'http://localhost:11434' : undefined,
});
p.println('✓ Vector search enabled');
}
function findReusableApiKey(config: Record<string, any>, embeddingProvider: string): string | undefined {
const models = config.models ?? {};
for (const tier of ['default', 'fast', 'complex', 'local']) {
const m = models[tier];
if (m?.provider === embeddingProvider && m?.api_key) return m.api_key;
}
return undefined;
}
+75
View File
@@ -0,0 +1,75 @@
import { describe, it, expect } from 'vitest';
import { createPrompter } from './prompts.js';
import { ConfigBuilder } from './config.js';
import { setupMemory } from './memory.js';
import { setupSecurity } from './security.js';
import { setupGateway } from './gateway.js';
function mockReadline(inputs: string[]) {
let questionIdx = 0;
return {
async question(query: string) {
const answer = inputs[questionIdx++];
return answer ?? '';
},
close() {
// no-op
},
[Symbol.asyncIterator]() {
return this;
},
async next() {
return { done: true };
},
} as any;
}
describe('setupMemory', () => {
it('enables vector search with openai embeddings', async () => {
const rl = mockReadline(['y', '1', 'y']);
const p = createPrompter(rl);
const builder = new ConfigBuilder();
builder.setProvider('default', { provider: 'openai', model: 'gpt-4.1', api_key: 'sk-test' });
await setupMemory(p, builder);
const config = builder.build();
expect(config.memory.embedding.enabled).toBe(true);
expect(config.memory.embedding.provider).toBe('openai');
});
it('skips vector search when declined', async () => {
const rl = mockReadline(['n']);
const p = createPrompter(rl);
const builder = new ConfigBuilder();
await setupMemory(p, builder);
const config = builder.build();
expect(config.memory?.embedding?.enabled).toBeFalsy();
});
});
describe('setupSecurity', () => {
it('enables sandbox and pairing', async () => {
const rl = mockReadline(['y', 'y', '']);
const p = createPrompter(rl);
const builder = new ConfigBuilder();
await setupSecurity(p, builder);
const config = builder.build();
expect(config.sandbox.enabled).toBe(true);
expect(config.pairing.enabled).toBe(true);
});
});
describe('setupGateway', () => {
it('configures port and auth token', async () => {
const rl = mockReadline(['9999', 'y', 'my-token', 'n', 'n']);
const p = createPrompter(rl);
const builder = new ConfigBuilder();
await setupGateway(p, builder);
const config = builder.build();
expect(config.server.port).toBe(9999);
expect(config.server.token).toBe('my-token');
});
});
+26
View File
@@ -0,0 +1,26 @@
import type { Prompter } from './prompts.js';
import type { ConfigBuilder } from './config.js';
const TOOL_PROFILES = [
{ label: 'full (unrestricted)', value: 'full' },
{ label: 'coding (fs + runtime + sessions + memory)', value: 'coding' },
{ label: 'messaging (send only)', value: 'messaging' },
{ label: 'minimal (status only)', value: 'minimal' },
];
export async function setupSecurity(p: Prompter, builder: ConfigBuilder): Promise<void> {
const sandbox = await p.confirm('Enable Docker sandboxing?', false);
if (sandbox) {
builder.setSandboxEnabled(true);
p.println('✓ Docker sandboxing enabled');
}
const pairing = await p.confirm('Enable DM pairing for unknown senders?', false);
if (pairing) {
builder.setPairingEnabled(true);
p.println('✓ DM pairing enabled');
}
const profile = await p.choose('Tool policy profile:', TOOL_PROFILES);
builder.setToolProfile(profile);
}