feat(skills): install to agent-scoped location instead of .skills/ (#148)

Skills now install to ~/.letta/agents/{agentId}/skills/ after agent
creation, aligning with Letta Code CLI behavior. This removes the
duplicate installation that was happening at both startup and after
agent creation.

Changes:
- Add SkillsConfig type and pass through BotConfig
- Update installSkillsToAgent() to actually install skills
- Remove installSkillsToWorkingDir() call from main.ts startup
- Closes #108 (reimplemented from PR #114 due to conflicts)

"The best way to predict the future is to invent it." - Alan Kay

Written by Cameron ◯ Letta Code
This commit is contained in:
Cameron
2026-02-04 17:25:53 -08:00
committed by GitHub
parent 030a2b2bc5
commit 1113631252
4 changed files with 62 additions and 22 deletions

View File

@@ -424,7 +424,7 @@ export class LettaBot {
updateAgentName(session.agentId, this.config.agentName).catch(() => {});
}
if (session.agentId) {
installSkillsToAgent(session.agentId);
installSkillsToAgent(session.agentId, this.config.skills);
}
}
} else if (session.conversationId && session.conversationId !== this.store.conversationId) {

View File

@@ -103,6 +103,15 @@ export interface OutboundFile {
kind?: 'image' | 'file';
}
/**
* Skills installation config
*/
export interface SkillsConfig {
cronEnabled?: boolean;
googleEnabled?: boolean;
additionalSkills?: string[];
}
/**
* Bot configuration
*/
@@ -113,6 +122,9 @@ export interface BotConfig {
agentName?: string; // Name for the agent (set via API after creation)
allowedTools: string[];
// Skills
skills?: SkillsConfig;
// Security
allowedUsers?: string[]; // Empty = allow all
}

View File

@@ -5,7 +5,7 @@
* Chat continues seamlessly between Telegram, Slack, and WhatsApp.
*/
import { existsSync, mkdirSync, readFileSync, readdirSync, promises as fs } from 'node:fs';
import { existsSync, mkdirSync, readFileSync, promises as fs } from 'node:fs';
import { join, resolve } from 'node:path';
import { spawn } from 'node:child_process';
@@ -123,7 +123,7 @@ import { CronService } from './cron/service.js';
import { HeartbeatService } from './cron/heartbeat.js';
import { PollingService } from './polling/service.js';
import { agentExists, findAgentByName } from './tools/letta-api.js';
import { installSkillsToWorkingDir } from './skills/loader.js';
// Skills are now installed to agent-scoped location after agent creation (see bot.ts)
// Check if config exists (skip in Railway/Docker where env vars are used directly)
const configPath = resolveConfigPath();
@@ -313,27 +313,16 @@ async function main() {
console.log(`[Storage] Data directory: ${dataDir}`);
console.log(`[Storage] Working directory: ${config.workingDir}`);
// Install feature-gated skills based on enabled features
// Skills are NOT installed by default - only when their feature is enabled
const skillsDir = resolve(config.workingDir, '.skills');
mkdirSync(skillsDir, { recursive: true });
installSkillsToWorkingDir(config.workingDir, {
cronEnabled: config.cronEnabled,
googleEnabled: config.polling.gmail.enabled, // Gmail polling uses gog skill
});
const existingSkills = readdirSync(skillsDir).filter(f => !f.startsWith('.'));
if (existingSkills.length > 0) {
console.log(`[Skills] ${existingSkills.length} skill(s) available: ${existingSkills.join(', ')}`);
}
// Create bot
// Create bot with skills config (skills installed to agent-scoped location after agent creation)
const bot = new LettaBot({
workingDir: config.workingDir,
model: config.model,
agentName: process.env.AGENT_NAME || 'LettaBot',
allowedTools: config.allowedTools,
skills: {
cronEnabled: config.cronEnabled,
googleEnabled: config.polling.gmail.enabled,
},
});
const attachmentsDir = resolve(config.workingDir, 'attachments');

View File

@@ -294,8 +294,47 @@ export function installSkillsToWorkingDir(workingDir: string, config: SkillsInst
/**
* @deprecated Use installSkillsToWorkingDir instead
* Install feature-gated skills to the agent-scoped skills directory
* (~/.letta/agents/{agentId}/skills/)
*
* This aligns with Letta Code CLI which uses agent-scoped skills.
* Called after agent creation in bot.ts.
*/
export function installSkillsToAgent(agentId: string): void {
// No-op - skills are now installed to working dir on startup
export function installSkillsToAgent(agentId: string, config: SkillsInstallConfig = {}): void {
const targetDir = getAgentSkillsDir(agentId);
// Ensure target directory exists
mkdirSync(targetDir, { recursive: true });
// Collect skills to install based on enabled features
const skillsToInstall: string[] = [];
// Cron skills (always if cron is enabled)
if (config.cronEnabled) {
skillsToInstall.push(...FEATURE_SKILLS.cron);
}
// Google skills (if Gmail polling or Google is configured)
if (config.googleEnabled) {
skillsToInstall.push(...FEATURE_SKILLS.google);
}
// Additional explicitly enabled skills
if (config.additionalSkills?.length) {
skillsToInstall.push(...config.additionalSkills);
}
if (skillsToInstall.length === 0) {
return; // No skills to install - silent return
}
// Source directories (later has priority)
const sourceDirs = [SKILLS_SH_DIR, BUNDLED_SKILLS_DIR, PROJECT_SKILLS_DIR];
// Install the specific skills
const installed = installSpecificSkills(skillsToInstall, sourceDirs, targetDir);
if (installed.length > 0) {
console.log(`[Skills] Installed ${installed.length} skill(s) to agent: ${installed.join(', ')}`);
}
}