diff --git a/src/cli/setup/automation.ts b/src/cli/setup/automation.ts new file mode 100644 index 0000000..3c58db4 --- /dev/null +++ b/src/cli/setup/automation.ts @@ -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 { + 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'); + } +} diff --git a/src/cli/setup/gateway.ts b/src/cli/setup/gateway.ts new file mode 100644 index 0000000..7a99184 --- /dev/null +++ b/src/cli/setup/gateway.ts @@ -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 { + 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'); + } +} diff --git a/src/cli/setup/memory.ts b/src/cli/setup/memory.ts new file mode 100644 index 0000000..3e5860c --- /dev/null +++ b/src/cli/setup/memory.ts @@ -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 { + 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, 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; +} diff --git a/src/cli/setup/sections.test.ts b/src/cli/setup/sections.test.ts new file mode 100644 index 0000000..5d3927e --- /dev/null +++ b/src/cli/setup/sections.test.ts @@ -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'); + }); +}); diff --git a/src/cli/setup/security.ts b/src/cli/setup/security.ts new file mode 100644 index 0000000..5cb9623 --- /dev/null +++ b/src/cli/setup/security.ts @@ -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 { + 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); +}