v2.9.1 — Config centralization, settings route extraction, web-ui split
- tools/*.ts: Replace fs.readFileSync config reads with getConfig() + resolveSecret() (pubmed, legal, scholar, weather, email, web, files — 7 files) - tools: Remove direct getVault() calls in email.ts and weather.ts, use resolveSecret() - gateway: Extract 21 /api/settings/* routes from server-v2.ts into routes/settings.ts (server-v2.ts reduced by ~375 lines) - types: Add missing config fields (legal, semantic_scholar, openweather, weather, email, search.pubmed_api_key) - web-ui: Split index.html (13,879 lines) into index.html (1,934) + styles.css (2,530) + app.js (9,416) - web-ui: Allow static assets to bypass auth middleware - Remove empty index2.html template stub Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+14
-38
@@ -1,8 +1,7 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { ToolResult } from '../types.js';
|
||||
import { getVault } from '../security/vault.js';
|
||||
import { getConfig } from '../config/config.js';
|
||||
|
||||
// ── Config ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -24,30 +23,10 @@ interface EmailAccount {
|
||||
config: EmailConfig;
|
||||
}
|
||||
|
||||
function resolveConfigPath(workspacePath?: string): string | null {
|
||||
const perUserPath = workspacePath
|
||||
? path.join(workspacePath, '.smallclaw', 'email-config.json')
|
||||
: null;
|
||||
const globalPath = path.join(os.homedir(), '.smallclaw', 'config.json');
|
||||
const projectCfg = path.join(process.cwd(), '.smallclaw', 'config.json');
|
||||
if (perUserPath && fs.existsSync(perUserPath)) return perUserPath;
|
||||
if (fs.existsSync(projectCfg)) return projectCfg;
|
||||
if (fs.existsSync(globalPath)) return globalPath;
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseAccount(raw: any, cfgPath: string): EmailConfig | null {
|
||||
function parseAccount(raw: any): EmailConfig | null {
|
||||
if (!raw?.imap_host || !raw?.username) return null;
|
||||
let password = raw.password || '';
|
||||
if (password.startsWith('vault:')) {
|
||||
try {
|
||||
const vault = getVault(path.dirname(cfgPath));
|
||||
const secret = vault.get(password.slice(6), 'email:resolve');
|
||||
password = secret ? secret.expose() : '';
|
||||
} catch {
|
||||
password = '';
|
||||
}
|
||||
}
|
||||
const cm = getConfig();
|
||||
let password = cm.resolveSecret(raw.password || '') || '';
|
||||
return {
|
||||
imap_host: raw.imap_host,
|
||||
imap_port: Number(raw.imap_port ?? 993),
|
||||
@@ -64,10 +43,9 @@ function parseAccount(raw: any, cfgPath: string): EmailConfig | null {
|
||||
|
||||
function getEmailConfig(accountId?: string, workspacePath?: string): EmailConfig | null {
|
||||
try {
|
||||
const cfgPath = resolveConfigPath(workspacePath);
|
||||
if (!cfgPath) return null;
|
||||
const data = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
||||
const emailCfg = data.email;
|
||||
const cm = getConfig();
|
||||
const cfg = cm.getConfig();
|
||||
const emailCfg = cfg.email;
|
||||
if (!emailCfg) return null;
|
||||
|
||||
let e: any;
|
||||
@@ -79,7 +57,7 @@ function getEmailConfig(accountId?: string, workspacePath?: string): EmailConfig
|
||||
} else {
|
||||
e = emailCfg;
|
||||
}
|
||||
return parseAccount(e, cfgPath);
|
||||
return parseAccount(e);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -87,21 +65,20 @@ function getEmailConfig(accountId?: string, workspacePath?: string): EmailConfig
|
||||
|
||||
function getAllEmailConfigs(workspacePath?: string): EmailAccount[] {
|
||||
try {
|
||||
const cfgPath = resolveConfigPath(workspacePath);
|
||||
if (!cfgPath) return [];
|
||||
const data = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
||||
const emailCfg = data.email;
|
||||
const cm = getConfig();
|
||||
const cfg = cm.getConfig();
|
||||
const emailCfg = cfg.email;
|
||||
if (!emailCfg) return [];
|
||||
|
||||
if (Array.isArray(emailCfg.accounts)) {
|
||||
const accounts: EmailAccount[] = [];
|
||||
for (const a of emailCfg.accounts) {
|
||||
const config = parseAccount(a, cfgPath);
|
||||
const config = parseAccount(a);
|
||||
if (config) accounts.push({ id: a.id, config });
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
const config = parseAccount(emailCfg, cfgPath);
|
||||
const config = parseAccount(emailCfg);
|
||||
const id = emailCfg.username ? emailCfg.username.split('@')[0] : 'default';
|
||||
return config ? [{ id, config }] : [];
|
||||
} catch {
|
||||
@@ -355,8 +332,7 @@ export const emailReadTool = {
|
||||
if (injectedWorkspace) {
|
||||
resolvedWorkspacePath = path.resolve(String(injectedWorkspace));
|
||||
} else {
|
||||
const cfgPath = path.join(process.cwd(), '.smallclaw', 'config.json');
|
||||
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
||||
const cfg = getConfig().getConfig();
|
||||
resolvedWorkspacePath = path.resolve(cfg?.workspace?.path || path.join(process.cwd(), 'workspace'));
|
||||
}
|
||||
const relDir = path.join('attachments', `uid-${uid}`);
|
||||
|
||||
+3
-11
@@ -177,11 +177,6 @@ export interface ReadToolArgs {
|
||||
|
||||
type RetrievalMode = 'fast' | 'standard' | 'deep';
|
||||
|
||||
function getLocalConfigFilePath(): string {
|
||||
const projectCfg = path.join(process.cwd(), '.smallclaw', 'config.json');
|
||||
return fsSync.existsSync(projectCfg) ? projectCfg : path.join(os.homedir(), '.smallclaw', 'config.json');
|
||||
}
|
||||
|
||||
let _retrievalModeCache: { mode: RetrievalMode; expiresAt: number } | null = null;
|
||||
|
||||
function getRetrievalMode(): RetrievalMode {
|
||||
@@ -189,12 +184,9 @@ function getRetrievalMode(): RetrievalMode {
|
||||
if (_retrievalModeCache && now < _retrievalModeCache.expiresAt) return _retrievalModeCache.mode;
|
||||
let mode: RetrievalMode = 'standard';
|
||||
try {
|
||||
const p = getLocalConfigFilePath();
|
||||
if (fsSync.existsSync(p)) {
|
||||
const raw = JSON.parse(fsSync.readFileSync(p, 'utf-8'));
|
||||
const m = String(raw?.agent_policy?.retrieval_mode || 'standard').toLowerCase();
|
||||
if (m === 'fast' || m === 'deep') mode = m;
|
||||
}
|
||||
const cfg = getConfig().getConfig();
|
||||
const m = String(cfg?.agent_policy?.retrieval_mode || 'standard').toLowerCase();
|
||||
if (m === 'fast' || m === 'deep') mode = m;
|
||||
} catch {}
|
||||
_retrievalModeCache = { mode, expiresAt: now + 60_000 };
|
||||
return mode;
|
||||
|
||||
+4
-11
@@ -1,21 +1,14 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { ToolResult } from '../types.js';
|
||||
import { getConfig } from '../config/config.js';
|
||||
|
||||
const LAW_GO_KR_BASE = 'https://www.law.go.kr/DRF';
|
||||
const COURT_LISTENER_BASE = 'https://www.courtlistener.com/api/rest/v3';
|
||||
|
||||
function getLawGoKrApiKey(): string | undefined {
|
||||
try {
|
||||
const projectCfg = path.join(process.cwd(), '.smallclaw', 'config.json');
|
||||
const cfgPath = fs.existsSync(projectCfg)
|
||||
? projectCfg
|
||||
: path.join(os.homedir(), '.smallclaw', 'config.json');
|
||||
if (fs.existsSync(cfgPath)) {
|
||||
const data = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
||||
return data.legal?.law_go_kr_api_key;
|
||||
}
|
||||
const cm = getConfig();
|
||||
const cfg = cm.getConfig();
|
||||
return cm.resolveSecret(cfg.legal?.law_go_kr_api_key) || undefined;
|
||||
} catch {}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
+4
-10
@@ -1,6 +1,5 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
import { execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { ToolResult } from '../types.js';
|
||||
@@ -13,14 +12,9 @@ const PMC_OA_BASE = 'https://www.ncbi.nlm.nih.gov/pmc/utils/oa/oa.fcgi';
|
||||
|
||||
function getPubMedApiKey(): string | undefined {
|
||||
try {
|
||||
const projectCfg = path.join(process.cwd(), '.smallclaw', 'config.json');
|
||||
const cfgPath = fs.existsSync(projectCfg)
|
||||
? projectCfg
|
||||
: path.join(os.homedir(), '.smallclaw', 'config.json');
|
||||
if (fs.existsSync(cfgPath)) {
|
||||
const data = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
||||
return data.pubmed?.api_key || data.search?.pubmed_api_key;
|
||||
}
|
||||
const cm = getConfig();
|
||||
const cfg = cm.getConfig();
|
||||
return cm.resolveSecret(cfg.pubmed?.api_key || cfg.search?.pubmed_api_key) || undefined;
|
||||
} catch {}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
+5
-11
@@ -1,7 +1,5 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { ToolResult } from '../types.js';
|
||||
import { getConfig } from '../config/config.js';
|
||||
|
||||
const OPENALEX_BASE = 'https://api.openalex.org';
|
||||
const S2_BASE = 'https://api.semanticscholar.org/graph/v1';
|
||||
@@ -9,14 +7,10 @@ const POLITE_EMAIL = 'research@smallclaw.local';
|
||||
|
||||
function getScholarConfig(): { s2Key?: string } {
|
||||
try {
|
||||
const projectCfg = path.join(process.cwd(), '.smallclaw', 'config.json');
|
||||
const cfgPath = fs.existsSync(projectCfg)
|
||||
? projectCfg
|
||||
: path.join(os.homedir(), '.smallclaw', 'config.json');
|
||||
if (fs.existsSync(cfgPath)) {
|
||||
const data = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
||||
return { s2Key: data.semantic_scholar?.api_key || '' };
|
||||
}
|
||||
const cm = getConfig();
|
||||
const cfg = cm.getConfig();
|
||||
const key = cm.resolveSecret(cfg.semantic_scholar?.api_key);
|
||||
return { s2Key: key || '' };
|
||||
} catch {}
|
||||
return {};
|
||||
}
|
||||
|
||||
+10
-22
@@ -1,32 +1,20 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { ToolResult } from '../types.js';
|
||||
import { getConfig } from '../config/config.js';
|
||||
import { getVault } from '../security/vault.js';
|
||||
|
||||
const OW_BASE = 'https://api.openweathermap.org/data/2.5';
|
||||
|
||||
function getApiKey(): string | undefined {
|
||||
try {
|
||||
const projectCfg = path.join(process.cwd(), '.smallclaw', 'config.json');
|
||||
const cfgPath = fs.existsSync(projectCfg)
|
||||
? projectCfg
|
||||
: path.join(os.homedir(), '.smallclaw', 'config.json');
|
||||
const configDir = path.dirname(cfgPath);
|
||||
if (fs.existsSync(cfgPath)) {
|
||||
const data = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
||||
let raw = data.openweather?.api_key || data.weather?.api_key;
|
||||
// Resolve vault reference (e.g. "vault:openweather.api_key")
|
||||
if (typeof raw === 'string' && raw.startsWith('vault:')) {
|
||||
const vaultKey = raw.slice('vault:'.length);
|
||||
const entry = getVault(configDir).get(vaultKey, 'weather:getkey');
|
||||
return entry?.expose();
|
||||
}
|
||||
if (raw) return raw;
|
||||
// Key may be in vault without a config reference
|
||||
const entry = getVault(configDir).get('openweather.api_key', 'weather:getkey');
|
||||
return entry?.expose();
|
||||
}
|
||||
const cm = getConfig();
|
||||
const cfg = cm.getConfig();
|
||||
const resolved = cm.resolveSecret(cfg.openweather?.api_key || cfg.weather?.api_key);
|
||||
if (resolved) return resolved;
|
||||
// Fallback: key may be in vault without a config reference
|
||||
const cm2 = getConfig();
|
||||
const vault = getVault(cm2.getConfigDir());
|
||||
const entry = vault.get('openweather.api_key', 'weather:getkey');
|
||||
return entry?.expose();
|
||||
} catch {}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
+20
-28
@@ -1,7 +1,5 @@
|
||||
import { ToolResult } from '../types.js';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getConfig } from '../config/config.js';
|
||||
|
||||
type SearchResultItem = { title: string; url: string; snippet: string };
|
||||
|
||||
@@ -463,7 +461,7 @@ function rankResults(query: string, results: SearchResultItem[]) {
|
||||
.map(x => x.r);
|
||||
}
|
||||
|
||||
// ── Load optional API keys from ~/.smallclaw/config.json ─────────────────────
|
||||
// ── Load optional API keys from config ─────────────────────────────────────
|
||||
// Cached with 5-minute TTL so config changes are picked up without restart.
|
||||
type SearchConfig = { preferred: 'tavily' | 'google' | 'brave' | 'ddg' | 'searxng'; tavilyKey?: string; googleKey?: string; googleCx?: string; braveKey?: string; searxngUrl?: string };
|
||||
let _searchConfigCache: { value: SearchConfig; expiresAt: number } | null = null;
|
||||
@@ -473,22 +471,19 @@ function getSearchConfig(): SearchConfig {
|
||||
if (_searchConfigCache && now < _searchConfigCache.expiresAt) return _searchConfigCache.value;
|
||||
let value: SearchConfig = { preferred: 'ddg' };
|
||||
try {
|
||||
const projectCfg = path.join(process.cwd(), '.smallclaw', 'config.json');
|
||||
const cfg = fs.existsSync(projectCfg) ? projectCfg : path.join(os.homedir(), '.smallclaw', 'config.json');
|
||||
if (fs.existsSync(cfg)) {
|
||||
const data = JSON.parse(fs.readFileSync(cfg, 'utf-8'));
|
||||
const preferredRaw = String(data.search?.preferred_provider || 'ddg').toLowerCase();
|
||||
const preferred = (['tavily', 'google', 'brave', 'ddg', 'searxng'].includes(preferredRaw) ? preferredRaw : 'ddg') as SearchConfig['preferred'];
|
||||
const searxngRaw = typeof data.search?.searxng_url === 'string' ? data.search.searxng_url.trim().replace(/\/+$/, '') : '';
|
||||
value = {
|
||||
preferred,
|
||||
tavilyKey: data.search?.tavily_api_key,
|
||||
googleKey: data.search?.google_api_key,
|
||||
googleCx: data.search?.google_cx,
|
||||
braveKey: data.search?.brave_api_key,
|
||||
searxngUrl: searxngRaw || undefined,
|
||||
};
|
||||
}
|
||||
const cm = getConfig();
|
||||
const data = cm.getConfig();
|
||||
const preferredRaw = String(data.search?.preferred_provider || 'ddg').toLowerCase();
|
||||
const preferred = (['tavily', 'google', 'brave', 'ddg', 'searxng'].includes(preferredRaw) ? preferredRaw : 'ddg') as SearchConfig['preferred'];
|
||||
const searxngRaw = typeof data.search?.searxng_url === 'string' ? data.search.searxng_url.trim().replace(/\/+$/, '') : '';
|
||||
value = {
|
||||
preferred,
|
||||
tavilyKey: cm.resolveSecret(data.search?.tavily_api_key),
|
||||
googleKey: cm.resolveSecret(data.search?.google_api_key),
|
||||
googleCx: data.search?.google_cx,
|
||||
braveKey: cm.resolveSecret(data.search?.brave_api_key),
|
||||
searxngUrl: searxngRaw || undefined,
|
||||
};
|
||||
} catch {}
|
||||
_searchConfigCache = { value, expiresAt: now + 5 * 60_000 };
|
||||
return value;
|
||||
@@ -998,14 +993,11 @@ export const webFetchTool = {
|
||||
|
||||
function getOllamaConfig(): { endpoint: string; model: string } {
|
||||
try {
|
||||
const projectCfg = path.join(process.cwd(), '.smallclaw', 'config.json');
|
||||
const cfgPath = fs.existsSync(projectCfg) ? projectCfg : path.join(os.homedir(), '.smallclaw', 'config.json');
|
||||
if (fs.existsSync(cfgPath)) {
|
||||
const data = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
||||
const endpoint = String(data.ollama?.endpoint || 'http://localhost:11434');
|
||||
const model = String(data.llm?.providers?.ollama?.model || data.models?.primary || 'llama3');
|
||||
return { endpoint, model };
|
||||
}
|
||||
const cm = getConfig();
|
||||
const data = cm.getConfig();
|
||||
const endpoint = String(data.ollama?.endpoint || 'http://localhost:11434');
|
||||
const model = String(data.llm?.providers?.ollama?.model || data.models?.primary || 'llama3');
|
||||
return { endpoint, model };
|
||||
} catch {}
|
||||
return { endpoint: 'http://localhost:11434', model: 'llama3' };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user