- session_id auto-generated on first run, saved to agent.json - Survives agent renames (slug changes don't break session history) - Directory mode: agent works inside its folder (agentHome = workspace)
192 lines
5.9 KiB
TypeScript
192 lines
5.9 KiB
TypeScript
import path from 'path';
|
|
import os from 'os';
|
|
import fs from 'fs';
|
|
import crypto from 'crypto';
|
|
|
|
export interface Config {
|
|
port: number;
|
|
defaultWorkDir: string;
|
|
sessionDir: string;
|
|
model: string;
|
|
logLevel: string;
|
|
apiKey: string;
|
|
provider: string;
|
|
}
|
|
|
|
export interface AgentConfig {
|
|
name: string;
|
|
slug: string;
|
|
prompt: string;
|
|
/** REST API base URL (e.g. https://dev.team.uix.su/agent-api) */
|
|
trackerUrl: string;
|
|
/** WebSocket URL (e.g. wss://dev.team.uix.su/agent-ws). Falls back to trackerUrl with http→ws conversion. */
|
|
wsUrl: string;
|
|
token: string;
|
|
transport: 'http' | 'ws';
|
|
listenPort: number;
|
|
workDir: string;
|
|
agentHome: string;
|
|
capabilities: string[];
|
|
maxConcurrentTasks: number;
|
|
model: string;
|
|
provider: string;
|
|
apiKey: string;
|
|
heartbeatIntervalSec: number;
|
|
/** Restrict file access to these directories. Empty = unrestricted. */
|
|
allowedPaths: string[];
|
|
/** Persistent session UUID — survives renames. Stored in agent.json. */
|
|
sessionId: string;
|
|
}
|
|
|
|
const homeDir = os.homedir();
|
|
|
|
export function loadConfig(): Config {
|
|
return {
|
|
port: parseInt(process.env.PICOGENT_PORT || '3100', 10),
|
|
defaultWorkDir: process.env.PICOGENT_WORK_DIR || process.cwd(),
|
|
sessionDir: process.env.PICOGENT_SESSION_DIR || path.join(homeDir, '.picogent', 'sessions'),
|
|
model: process.env.PICOGENT_MODEL || 'sonnet',
|
|
logLevel: process.env.LOG_LEVEL || 'info',
|
|
apiKey: process.env.PICOGENT_API_KEY || process.env.ANTHROPIC_API_KEY || '',
|
|
provider: process.env.PICOGENT_PROVIDER || 'anthropic',
|
|
};
|
|
}
|
|
|
|
interface ResolvedConfig {
|
|
configPath: string | null;
|
|
agentHome: string | null;
|
|
}
|
|
|
|
/**
|
|
* Resolve config: CLI arg can be a .json file OR a directory.
|
|
*
|
|
* Directory mode: picogent ./agents/coder/
|
|
* → configPath = ./agents/coder/agent.json
|
|
* → agentHome = ./agents/coder/
|
|
*
|
|
* File mode: picogent ./agent.json
|
|
* → configPath = ./agent.json
|
|
* → agentHome = null
|
|
*
|
|
* Env/default fallback: same as before
|
|
*/
|
|
export function resolveConfig(): ResolvedConfig {
|
|
const arg = process.argv[2];
|
|
|
|
if (arg) {
|
|
const resolved = path.resolve(arg);
|
|
|
|
// Directory mode: picogent ./agents/coder/
|
|
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
const configInDir = path.join(resolved, 'agent.json');
|
|
return {
|
|
configPath: fs.existsSync(configInDir) ? configInDir : null,
|
|
agentHome: resolved,
|
|
};
|
|
}
|
|
|
|
// File mode: picogent ./agent.json
|
|
if (arg.endsWith('.json')) {
|
|
return { configPath: resolved, agentHome: null };
|
|
}
|
|
}
|
|
|
|
// Env fallback
|
|
if (process.env.PICOGENT_CONFIG) {
|
|
return { configPath: path.resolve(process.env.PICOGENT_CONFIG), agentHome: null };
|
|
}
|
|
|
|
// Default: ./agent.json if exists
|
|
const defaultPath = path.resolve('./agent.json');
|
|
if (fs.existsSync(defaultPath)) {
|
|
return { configPath: defaultPath, agentHome: null };
|
|
}
|
|
|
|
return { configPath: null, agentHome: null };
|
|
}
|
|
|
|
/**
|
|
* Agent mode if: config found, directory passed, or TRACKER_URL env set.
|
|
*/
|
|
export function hasAgentConfig(): boolean {
|
|
const { configPath, agentHome } = resolveConfig();
|
|
return configPath !== null || agentHome !== null || !!process.env.TRACKER_URL;
|
|
}
|
|
|
|
export function loadAgentConfig(): AgentConfig {
|
|
const { configPath, agentHome } = resolveConfig();
|
|
|
|
// Load file if available, otherwise empty object
|
|
let file: Record<string, unknown> = {};
|
|
if (configPath) {
|
|
if (!fs.existsSync(configPath)) {
|
|
throw new Error(`Agent config not found: ${configPath}`);
|
|
}
|
|
file = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
}
|
|
|
|
const trackerUrl = process.env.TRACKER_URL || (file.tracker_url as string) || '';
|
|
const token = process.env.AGENT_TOKEN || (file.token as string) || '';
|
|
|
|
if (!trackerUrl) {
|
|
throw new Error('Tracker URL required. Set TRACKER_URL env or tracker_url in config.');
|
|
}
|
|
if (!token) {
|
|
throw new Error('Agent token required. Set AGENT_TOKEN env or token in config.');
|
|
}
|
|
|
|
// In directory mode, agentHome IS the work dir (unless overridden)
|
|
const resolvedHome = agentHome || '';
|
|
const workDir = process.env.PICOGENT_WORK_DIR
|
|
|| (file.work_dir as string)
|
|
|| resolvedHome
|
|
|| process.cwd();
|
|
|
|
const wsUrl = process.env.AGENT_WS_URL || (file.ws_url as string) || '';
|
|
|
|
return {
|
|
name: (file.name as string) || process.env.AGENT_NAME || 'Agent',
|
|
slug: (file.slug as string) || process.env.AGENT_SLUG || 'agent',
|
|
prompt: (file.prompt as string) || process.env.AGENT_PROMPT || '',
|
|
trackerUrl,
|
|
wsUrl,
|
|
token,
|
|
transport: (process.env.AGENT_TRANSPORT || (file.transport as string) || 'http') as 'http' | 'ws',
|
|
listenPort: parseInt(process.env.AGENT_PORT || String(file.listen_port || '3200'), 10),
|
|
workDir,
|
|
agentHome: resolvedHome || workDir,
|
|
capabilities: (file.capabilities as string[]) || ['coding'],
|
|
maxConcurrentTasks: (file.max_concurrent_tasks as number) || 2,
|
|
model: process.env.PICOGENT_MODEL || (file.model as string) || 'sonnet',
|
|
provider: process.env.PICOGENT_PROVIDER || (file.provider as string) || 'anthropic',
|
|
apiKey: process.env.PICOGENT_API_KEY || process.env.ANTHROPIC_API_KEY || (file.api_key as string) || '',
|
|
heartbeatIntervalSec: (file.heartbeat_interval_sec as number) || 30,
|
|
allowedPaths: (file.allowed_paths as string[]) || [],
|
|
sessionId: ensureSessionId(file, configPath),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Ensure agent.json has a persistent session_id.
|
|
* Generates UUID on first run and writes it back to the config file.
|
|
*/
|
|
function ensureSessionId(file: Record<string, unknown>, configPath: string | null): string {
|
|
if (file.session_id && typeof file.session_id === 'string') {
|
|
return file.session_id;
|
|
}
|
|
|
|
const id = crypto.randomUUID();
|
|
|
|
// Persist to agent.json if we have a path
|
|
if (configPath) {
|
|
try {
|
|
file.session_id = id;
|
|
fs.writeFileSync(configPath, JSON.stringify(file, null, 2) + '\n');
|
|
} catch {
|
|
// Non-fatal — session works in memory
|
|
}
|
|
}
|
|
|
|
return id;
|
|
}
|