feat: claude one-shot port from nanobot python codebase (v0.1.4.post4)
This commit is contained in:
65
src/config/loader.ts
Normal file
65
src/config/loader.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { type Config, ConfigSchema } from './types.ts';
|
||||
|
||||
const DEFAULT_CONFIG_PATH = resolve(homedir(), '.nanobot', 'config.json');
|
||||
|
||||
export function getConfigPath(override?: string): string {
|
||||
return override ?? process.env['NANOBOT_CONFIG'] ?? DEFAULT_CONFIG_PATH;
|
||||
}
|
||||
|
||||
export function loadConfig(configPath?: string): Config {
|
||||
const path = getConfigPath(configPath);
|
||||
|
||||
if (!existsSync(path)) {
|
||||
return ConfigSchema.parse({});
|
||||
}
|
||||
|
||||
const raw = readFileSync(path, 'utf8');
|
||||
let json: unknown;
|
||||
try {
|
||||
json = JSON.parse(raw);
|
||||
} catch {
|
||||
console.error(`Failed to parse config at ${path}`);
|
||||
return ConfigSchema.parse({});
|
||||
}
|
||||
|
||||
// Apply NANOBOT_ env var overrides before validation
|
||||
const merged = applyEnvOverrides(json as Record<string, unknown>);
|
||||
return ConfigSchema.parse(merged);
|
||||
}
|
||||
|
||||
export function saveConfig(config: Config, configPath?: string): void {
|
||||
const path = getConfigPath(configPath);
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, JSON.stringify(config, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
/** Resolve `~` in workspace path to the real home directory. */
|
||||
export function resolveWorkspacePath(raw: string): string {
|
||||
if (raw.startsWith('~/') || raw === '~') {
|
||||
return resolve(homedir(), raw.slice(2));
|
||||
}
|
||||
return resolve(raw);
|
||||
}
|
||||
|
||||
function applyEnvOverrides(json: Record<string, unknown>): Record<string, unknown> {
|
||||
const out = structuredClone(json);
|
||||
|
||||
const model = process.env['NANOBOT_MODEL'];
|
||||
if (model) {
|
||||
const agent = (out['agent'] as Record<string, unknown> | undefined) ?? {};
|
||||
agent['model'] = model;
|
||||
out['agent'] = agent;
|
||||
}
|
||||
|
||||
const workspace = process.env['NANOBOT_WORKSPACE'];
|
||||
if (workspace) {
|
||||
const agent = (out['agent'] as Record<string, unknown> | undefined) ?? {};
|
||||
agent['workspacePath'] = workspace;
|
||||
out['agent'] = agent;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
128
src/config/types.ts
Normal file
128
src/config/types.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mattermost
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const MattermostDmConfigSchema = z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
allowFrom: z.array(z.string()).default([]),
|
||||
});
|
||||
export type MattermostDmConfig = z.infer<typeof MattermostDmConfigSchema>;
|
||||
|
||||
export const MattermostConfigSchema = z.object({
|
||||
serverUrl: z.string(),
|
||||
token: z.string(),
|
||||
scheme: z.enum(['https', 'http']).default('https'),
|
||||
port: z.number().int().default(443),
|
||||
basePath: z.string().default(''),
|
||||
allowFrom: z.array(z.string()).default([]),
|
||||
groupPolicy: z.enum(['open', 'mention', 'allowlist']).default('mention'),
|
||||
groupAllowFrom: z.array(z.string()).default([]),
|
||||
dm: MattermostDmConfigSchema.default(() => ({ enabled: true, allowFrom: [] })),
|
||||
replyInThread: z.boolean().default(true),
|
||||
});
|
||||
export type MattermostConfig = z.infer<typeof MattermostConfigSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Channels
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const ChannelsConfigSchema = z.object({
|
||||
mattermost: MattermostConfigSchema.optional(),
|
||||
sendProgress: z.boolean().default(true),
|
||||
sendToolHints: z.boolean().default(true),
|
||||
});
|
||||
export type ChannelsConfig = z.infer<typeof ChannelsConfigSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agent
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const AgentConfigSchema = z.object({
|
||||
model: z.string().default('anthropic/claude-sonnet-4-5'),
|
||||
workspacePath: z.string().default('~/.nanobot'),
|
||||
maxTokens: z.number().int().default(4096),
|
||||
contextWindowTokens: z.number().int().default(65536),
|
||||
temperature: z.number().default(0.7),
|
||||
maxToolIterations: z.number().int().default(40),
|
||||
});
|
||||
export type AgentConfig = z.infer<typeof AgentConfigSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Providers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const ProviderConfigSchema = z.object({
|
||||
apiKey: z.string().optional(),
|
||||
apiBase: z.string().optional(),
|
||||
});
|
||||
export type ProviderConfig = z.infer<typeof ProviderConfigSchema>;
|
||||
|
||||
export const ProvidersConfigSchema = z.object({
|
||||
anthropic: ProviderConfigSchema.optional(),
|
||||
openai: ProviderConfigSchema.optional(),
|
||||
google: ProviderConfigSchema.optional(),
|
||||
openrouter: ProviderConfigSchema.optional(),
|
||||
ollama: ProviderConfigSchema.optional(),
|
||||
});
|
||||
export type ProvidersConfig = z.infer<typeof ProvidersConfigSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tools
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const ExecToolConfigSchema = z.object({
|
||||
timeout: z.number().int().default(120),
|
||||
pathAppend: z.string().optional(),
|
||||
denyPatterns: z.array(z.string()).default([]),
|
||||
restrictToWorkspace: z.boolean().default(false),
|
||||
});
|
||||
export type ExecToolConfig = z.infer<typeof ExecToolConfigSchema>;
|
||||
|
||||
export const WebToolConfigSchema = z.object({
|
||||
braveApiKey: z.string().optional(),
|
||||
proxy: z.string().optional(),
|
||||
});
|
||||
export type WebToolConfig = z.infer<typeof WebToolConfigSchema>;
|
||||
|
||||
export const ToolsConfigSchema = z.object({
|
||||
exec: ExecToolConfigSchema.default(() => ({ timeout: 120, denyPatterns: [], restrictToWorkspace: false })),
|
||||
web: WebToolConfigSchema.default(() => ({})),
|
||||
restrictToWorkspace: z.boolean().default(false),
|
||||
});
|
||||
export type ToolsConfig = z.infer<typeof ToolsConfigSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Heartbeat
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const HeartbeatConfigSchema = z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
intervalMinutes: z.number().int().default(30),
|
||||
});
|
||||
export type HeartbeatConfig = z.infer<typeof HeartbeatConfigSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Root config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const ConfigSchema = z.object({
|
||||
agent: AgentConfigSchema.default(() => ({
|
||||
model: 'anthropic/claude-sonnet-4-5',
|
||||
workspacePath: '~/.nanobot',
|
||||
maxTokens: 4096,
|
||||
contextWindowTokens: 65536,
|
||||
temperature: 0.7,
|
||||
maxToolIterations: 40,
|
||||
})),
|
||||
providers: ProvidersConfigSchema.default(() => ({})),
|
||||
channels: ChannelsConfigSchema.default(() => ({ sendProgress: true, sendToolHints: true })),
|
||||
tools: ToolsConfigSchema.default(() => ({
|
||||
exec: { timeout: 120, denyPatterns: [], restrictToWorkspace: false },
|
||||
web: {},
|
||||
restrictToWorkspace: false,
|
||||
})),
|
||||
heartbeat: HeartbeatConfigSchema.default(() => ({ enabled: false, intervalMinutes: 30 })),
|
||||
});
|
||||
export type Config = z.infer<typeof ConfigSchema>;
|
||||
Reference in New Issue
Block a user