feat(setup): add memory, automation, security, and gateway setup flows
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user