src / toolsProvider.ts
/**
* Developer Growth Plugin — toolsProvider
*
* Tools:
* Skills · rate_skill, get_skill_map, assess_skill_gaps
* Learning · log_session, get_stats, list_sessions
* Goals · set_goal, update_goal, list_goals
* Resources · add_resource, list_resources, update_resource, mark_resource_complete, next_resource, search_resources
* Planning · generate_learning_path, replan_learning_path, generate_study_plan, weekly_review
* Content · explain_concept, generate_project_idea, search_ai_trends, compare_ai_tools
* Export · export_growth_report
*/
import { text, tool, type Tool, type ToolsProvider } from "@lmstudio/sdk";
import { readFile, writeFile, mkdir } from "fs/promises";
import { join } from "path";
import { homedir } from "os";
import { webSearch as _webSearch, type TimeRange } from "./search";
import { z } from "zod";
import { pluginConfigSchematics } from "./config";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function json(obj: unknown): string {
return JSON.stringify(obj, null, 2);
}
function safe_impl<T extends Record<string, unknown>>(
name: string,
fn: (params: T) => Promise<string>
): (params: T) => Promise<string> {
return async (params: T) => {
try {
return await fn(params);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return JSON.stringify({
tool_error: true,
tool: name,
error: msg,
hint: "Read the error above, fix the parameter causing the issue, and retry the tool call.",
}, null, 2);
}
};
}
function sleep(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}
// searxngUrl set from config inside toolsProvider export
let _searxngUrl: string | undefined;
// Practitioner blogs — best signal for applied AI/LLM content
const EXPERT_BLOGS = "site:eugeneyan.com OR site:lilianweng.github.io OR site:huyenchip.com OR site:simonwillison.net OR site:sebastianraschka.com";
// ---------------------------------------------------------------------------
// Skill categories
// ---------------------------------------------------------------------------
const SKILL_CATEGORIES = [
"prompt_engineering",
"rag_and_retrieval",
"agents_and_orchestration",
"llm_evaluation",
"ai_assisted_coding",
"llmops_production",
"system_design_ai",
"fine_tuning_and_peft",
"open_source_llms",
"ai_security_and_safety",
"vector_databases",
"multimodal_ai",
"cs_fundamentals",
"software_architecture",
"data_engineering",
"devops_and_infra",
"product_and_communication",
"other",
] as const;
type SkillCategory = (typeof SKILL_CATEGORIES)[number];
// Human-readable labels for categories
const CATEGORY_LABELS: Record<SkillCategory, string> = {
prompt_engineering: "Prompt Engineering & LLM Interaction",
rag_and_retrieval: "RAG & Retrieval Systems",
agents_and_orchestration: "Agents, Tool Use & Orchestration",
llm_evaluation: "LLM Evaluation & Testing",
ai_assisted_coding: "AI-Assisted Coding",
llmops_production: "LLMOps & Production AI",
system_design_ai: "System Design for AI",
fine_tuning_and_peft: "Fine-tuning & PEFT",
open_source_llms: "Open-source LLMs & Local Models",
ai_security_and_safety: "AI Security & Safety",
vector_databases: "Vector Databases & Embeddings",
multimodal_ai: "Multimodal AI",
cs_fundamentals: "CS Fundamentals",
software_architecture: "Software Architecture",
data_engineering: "Data Engineering",
devops_and_infra: "DevOps & Infrastructure",
product_and_communication:"Product Thinking & Communication",
other: "Other",
};
// ---------------------------------------------------------------------------
// Data model
// ---------------------------------------------------------------------------
interface SkillRatingEntry {
rating: number;
notes: string;
ratedAt: string;
}
interface SkillRating {
skill: string;
category: SkillCategory;
rating: number; // current rating (1–10)
notes: string; // current notes
ratedAt: string; // when last rated
history: SkillRatingEntry[]; // all previous ratings, oldest first
}
interface LearningSession {
id: string;
date: string; // YYYY-MM-DD
topic: string;
category: SkillCategory;
durationMinutes: number;
notes: string;
insights: string; // Key takeaways
resourcesUsed: string[];
mood: "frustrated" | "neutral" | "productive" | "excited";
createdAt: string;
}
interface Resource {
id: string;
title: string;
url: string;
type: "paper" | "course" | "book" | "article" | "video" | "repo" | "podcast" | "other";
category: SkillCategory;
tags: string[];
priority: "low" | "medium" | "high";
status: "unread" | "in_progress" | "completed" | "abandoned";
notes: string;
rating: number; // 1–5, 0 = not rated
addedAt: string;
updatedAt: string;
}
interface LearningGoal {
id: string;
title: string;
description: string;
category: SkillCategory;
targetDate: string; // YYYY-MM-DD
status: "active" | "completed" | "abandoned";
progressPercent: number;
milestones: string[];
notes: string;
createdAt: string;
updatedAt: string;
}
interface GrowthDB {
skills: SkillRating[];
sessions: LearningSession[];
resources: Resource[];
goals: LearningGoal[];
}
// ---------------------------------------------------------------------------
// DB helpers
// ---------------------------------------------------------------------------
function getDataDir(configPath: string): string {
return configPath.trim() || join(homedir(), "developer-growth");
}
function dbPath(dataDir: string): string {
return join(dataDir, "growth.json");
}
async function loadDB(dataDir: string): Promise<GrowthDB> {
try {
const raw = await readFile(dbPath(dataDir), "utf8");
const db = JSON.parse(raw) as GrowthDB;
db.skills ??= [];
db.sessions ??= [];
db.resources ??= [];
db.goals ??= [];
for (const s of db.skills) { s.history ??= []; }
for (const r of db.resources) {
r.rating ??= 0;
r.tags ??= [];
}
for (const g of db.goals) {
g.milestones ??= [];
}
return db;
} catch {
return { skills: [], sessions: [], resources: [], goals: [] };
}
}
async function saveDB(dataDir: string, db: GrowthDB): Promise<void> {
await mkdir(dataDir, { recursive: true });
await writeFile(dbPath(dataDir), JSON.stringify(db, null, 2), "utf8");
}
function makeId(): string {
return crypto.randomUUID();
}
// ---------------------------------------------------------------------------
// Tools Provider
// ---------------------------------------------------------------------------
export const toolsProvider: ToolsProvider = async (ctl) => {
const cfg = ctl.getPluginConfig(pluginConfigSchematics);
const dataDir = () => getDataDir(cfg.get("dataPath"));
const devName = () => cfg.get("developerName").trim() || "Developer";
const currentRole = () => cfg.get("currentRole").trim() || "Developer";
const targetRole = () => cfg.get("targetRole").trim();
const expYears = () => cfg.get("experienceYears");
const weeklyGoal = () => cfg.get("weeklyGoalMinutes");
const maxResults = () => cfg.get("maxSearchResults");
_searxngUrl = cfg.get("searxngUrl").trim() || undefined;
const searchWindow = (): TimeRange | undefined => {
const v = cfg.get("searchRecencyWindow").trim().toLowerCase();
return (["day", "week", "month", "year"].includes(v) ? v : undefined) as TimeRange | undefined;
};
function webSearch(query: string, max?: number, timeRange?: TimeRange) {
return _webSearch(query, max, 10_000, _searxngUrl, timeRange ?? searchWindow());
}
const tools: Tool[] = [
// =========================================================================
// SKILLS
// =========================================================================
tool({
name: "rate_skill",
description: text`
Self-rate your proficiency in a specific skill (1–10).
Saves to your skill map. Run get_skill_map to see your full picture.
Be honest — overrating hurts your learning plan.
Rating guide:
1–2 Never used it / heard the term only
3–4 Basic awareness, read about it
5–6 Used it in a project, understand core concepts
7–8 Comfortable, can use without much help
9–10 Deep expertise, could teach or build production systems
`,
parameters: {
skill: z.string().describe("Specific skill to rate (e.g. 'RAG pipelines', 'Prompt chaining', 'LangChain')"),
category: z.enum(SKILL_CATEGORIES).describe("Which LLM-era category this skill belongs to"),
rating: z.coerce.number().int().min(1).max(10).describe("Your honest proficiency (1–10)"),
notes: z.string().default("").describe("Context — what have you built, what do you still struggle with?"),
},
implementation: safe_impl("rate_skill", async ({ skill, category, rating, notes }) => {
const db = await loadDB(dataDir());
const existing = db.skills.findIndex(
(s) => s.skill.toLowerCase() === skill.toLowerCase()
);
const now = new Date().toISOString();
if (existing !== -1) {
const prev = db.skills[existing];
// Preserve old rating in history before overwriting
const historyEntry: SkillRatingEntry = { rating: prev.rating, notes: prev.notes, ratedAt: prev.ratedAt };
const history = [...(prev.history ?? []), historyEntry];
db.skills[existing] = { skill, category, rating, notes, ratedAt: now, history };
await saveDB(dataDir(), db);
const change = rating - prev.rating;
const trend = change > 0 ? `+${change} â–²` : change < 0 ? `${change} â–¼` : "no change";
return json({
success: true,
skill,
category: CATEGORY_LABELS[category],
rating,
previousRating: prev.rating,
change,
trend,
totalRatings: history.length + 1,
notes,
});
}
const entry: SkillRating = { skill, category, rating, notes, ratedAt: now, history: [] };
db.skills.push(entry);
await saveDB(dataDir(), db);
return json({ success: true, skill, category: CATEGORY_LABELS[category], rating, notes });
}),
}),
tool({
name: "get_skill_map",
description: text`
View your full skill map — all rated skills grouped by LLM-era category
with averages, coverage, and a radar chart-style summary.
Highlights strongest areas, weakest areas, and unrated priority categories
based on your current and target role.
`,
parameters: {
category: z.enum([...SKILL_CATEGORIES, "all"] as const).default("all")
.describe("Filter to a specific category or 'all'"),
},
implementation: safe_impl("get_skill_map", async ({ category }) => {
const db = await loadDB(dataDir());
const role = currentRole();
const target = targetRole();
const year = new Date().getFullYear();
const roleContext = target ? `${role} transitioning to ${target}` : role;
let skills = db.skills;
if (category !== "all") skills = skills.filter((s) => s.category === category);
// Group by category
const grouped: Record<string, { skills: SkillRating[]; avg: number }> = {};
for (const s of skills) {
if (!grouped[s.category]) grouped[s.category] = { skills: [], avg: 0 };
grouped[s.category].skills.push(s);
}
for (const cat of Object.keys(grouped)) {
const ratings = grouped[cat].skills.map((s) => s.rating);
grouped[cat].avg = Math.round((ratings.reduce((a, b) => a + b, 0) / ratings.length) * 10) / 10;
}
const strongest = db.skills.slice().sort((a, b) => b.rating - a.rating).slice(0, 3);
const weakest = db.skills.filter((s) => s.rating <= 4).sort((a, b) => a.rating - b.rating).slice(0, 3);
const overallAvg = db.skills.length > 0
? Math.round((db.skills.reduce((a, s) => a + s.rating, 0) / db.skills.length) * 10) / 10
: 0;
// Growth trajectory: skills that have been re-rated at least once
const growthTrajectory = db.skills
.filter((s) => s.history && s.history.length > 0)
.map((s) => {
const first = s.history[0];
const change = s.rating - first.rating;
return {
skill: s.skill,
firstRating: first.rating,
firstRatedOn: first.ratedAt.slice(0, 10),
currentRating: s.rating,
change,
trend: change > 0 ? `+${change} â–²` : change < 0 ? `${change} â–¼` : "no change",
ratingHistory: [...s.history, { rating: s.rating, ratedAt: s.ratedAt }]
.map((h) => `${h.ratedAt.slice(0, 10)}: ${h.rating}`),
};
})
.sort((a, b) => b.change - a.change);
// Live search: what skills actually matter for this role right now
const roleSkillsSearch = await webSearch(
`"${target || role}" skills most important ${year} what to know`,
maxResults(),
"year"
);
return json({
developer: devName(),
currentRole: role,
targetRole: target || "not set",
overallAverage: overallAvg,
totalSkillsRated: db.skills.length,
growthTrajectory: growthTrajectory.length > 0 ? growthTrajectory : "no re-ratings yet — rate a skill again to track growth",
categoryBreakdown: Object.fromEntries(
Object.entries(grouped).map(([cat, data]) => [
CATEGORY_LABELS[cat as SkillCategory] ?? cat,
{ average: data.avg, skills: data.skills.map((s) => ({ skill: s.skill, rating: s.rating })) },
])
),
strongest: strongest.map((s) => ({ skill: s.skill, rating: s.rating, category: CATEGORY_LABELS[s.category] })),
weakest: weakest.map((s) => ({ skill: s.skill, rating: s.rating, category: CATEGORY_LABELS[s.category] })),
role_skills_research: {
query: `"${target || role}" skills most important ${year}`,
results: roleSkillsSearch,
},
instructions:
`For ${devName()} (${roleContext}): ` +
"Using the skill map AND the live role_skills_research above, present what the data shows: " +
"(1) Skills that appear frequently in job postings and market signals for this role — list all, not just 2; " +
"(2) Which of the developer's current skills overlap with market demand — cite the specific signals; " +
"(3) Skills with low or no ratings relative to what the market signals — note uncertainty where search data is thin; " +
"(4) Any skills that appear in only a minority of signals — present these as optional rather than mandatory. " +
"Ground every observation in the search results. Do not tell the user to ignore a skill category unless the market data clearly supports de-prioritizing it.",
});
}),
}),
tool({
name: "assess_skill_gaps",
description: text`
Analyse your skill map against your current/target role and generate a
prioritised gap analysis. Identifies critical gaps (blocking your goal),
important gaps (high leverage), and nice-to-have gaps.
Also distinguishes between skills AI is replacing vs amplifying.
`,
parameters: {
focusArea: z.string().default("")
.describe("Optional focus — e.g. 'getting to Staff Engineer', 'building an AI SaaS', 'switching to AI roles'"),
},
implementation: safe_impl("assess_skill_gaps", async ({ focusArea }) => {
const db = await loadDB(dataDir());
const role = currentRole();
const target = targetRole();
const years = expYears();
const year = new Date().getFullYear();
const targetRoleStr = (target || focusArea || "AI engineer").trim();
const transitionStr = target ? `${role} to ${target}` : focusArea || "developer to AI engineer";
// Live web searches — what does this role actually require RIGHT NOW
const searchQueries = [
{ angle: "role_requirements", query: `"${targetRoleStr}" skills required ${year} job description must-have` },
{ angle: "transition_path", query: `"${transitionStr}" transition how to ${year} roadmap` },
{ angle: "what_to_learn", query: `how to become "${targetRoleStr}" ${year} what to learn` },
{ angle: "hiring_bar", query: `"${targetRoleStr}" interview topics skills ${year} hiring` },
];
const webResearch: Array<{ angle: string; query: string; results: Array<{ title: string; url: string; snippet: string }> }> = [];
for (const s of searchQueries) {
const results = await webSearch(s.query, maxResults(), "year");
webResearch.push({ angle: s.angle, query: s.query, results });
await sleep(350);
}
// Current self-rated skills from DB (context for the gap analysis)
const currentSkills = db.skills.map((s) => ({
skill: s.skill,
category: CATEGORY_LABELS[s.category],
rating: s.rating,
notes: s.notes,
ratedAt: s.ratedAt,
}));
return json({
developer: devName(),
currentRole: role,
targetRole: targetRoleStr,
experienceYears: years,
focusArea: focusArea || null,
totalSkillsRated: db.skills.length,
current_skills: currentSkills,
web_research: webResearch,
next_step:
"IMPORTANT: Based on the web research above (not hardcoded lists), identify the 3 most critical " +
"skills this developer is missing for the target role. Then call generate_learning_path once per skill " +
"(3 calls total). Use the category enum value, set currentRating from current_skills (0 if not rated), " +
"targetRating=8, timelineWeeks=12, weeklyHours=5. After all 3 calls, write a complete transition roadmap.",
instructions:
`Analyse the live web research above to understand what "${targetRoleStr}" actually requires in ${year}. ` +
`Compare against ${devName()}'s current_skills (${years} yrs experience as ${role}). ` +
"Present what the research shows — do not fill gaps with your training data: " +
"(1) Skills that appear consistently across multiple search results for this role — list them with citation, " +
"(2) Skills the developer already has that match — be specific about which results confirm this overlap, " +
"(3) Skills with low or no coverage in the current skill map relative to what the results show, " +
"(4) Any conflicting signals across sources — if one community says X is critical and another says it's declining, show both. " +
"Do not assert that any specific skill is 'being replaced by AI' unless the search results explicitly support that claim. " +
"Ground every point in the search results — cite specific sources.",
});
}),
}),
// =========================================================================
// LEARNING TRACKER
// =========================================================================
tool({
name: "log_session",
description: text`
Log a learning session — what you studied, for how long, and what you learned.
Builds your streak, tracks weekly progress toward your goal, and surfaces patterns.
Minimum 10 minutes to count as a session.
`,
parameters: {
topic: z.string().describe("What you studied (specific — e.g. 'LangGraph agent loops', not just 'AI')"),
category: z.enum(SKILL_CATEGORIES).describe("Which skill category this belongs to"),
durationMinutes: z.coerce.number().int().min(10).max(720)
.describe("How long you studied in minutes"),
insights: z.string().default("").describe("Key takeaways — what clicked, what confused you, what you want to explore next"),
notes: z.string().default("").describe("Free-form notes"),
resourcesUsed: z.array(z.string()).default([])
.describe("Resources you used — URLs, book names, course names"),
mood: z.enum(["frustrated", "neutral", "productive", "excited"]).default("productive")
.describe("How did the session feel?"),
date: z.string().default("").describe("Date (YYYY-MM-DD), leave blank for today"),
},
implementation: safe_impl("log_session", async (params) => {
const db = await loadDB(dataDir());
const today = new Date().toISOString().slice(0, 10);
const date = params.date.trim() || today;
const session: LearningSession = {
id: makeId(),
date,
topic: params.topic,
category: params.category,
durationMinutes: params.durationMinutes,
notes: params.notes,
insights: params.insights,
resourcesUsed: params.resourcesUsed,
mood: params.mood,
createdAt: new Date().toISOString(),
};
db.sessions.push(session);
await saveDB(dataDir(), db);
// Compute week stats (Mon–Sun)
const weekStart = new Date();
weekStart.setDate(weekStart.getDate() - weekStart.getDay() + 1);
const weekStartStr = weekStart.toISOString().slice(0, 10);
const weekSessions = db.sessions.filter((s) => s.date >= weekStartStr);
const weekMinutes = weekSessions.reduce((a, s) => a + s.durationMinutes, 0);
const weekGoal = weeklyGoal();
// Compute streak (consecutive days with at least one session)
const sessionDates = new Set(db.sessions.map((s) => s.date));
let streak = 0;
const d = new Date();
while (sessionDates.has(d.toISOString().slice(0, 10))) {
streak++;
d.setDate(d.getDate() - 1);
}
return json({
success: true,
session: { id: session.id, topic: session.topic, durationMinutes: session.durationMinutes, date },
weekProgress: {
minutesThisWeek: weekMinutes,
weeklyGoalMinutes: weekGoal,
percentComplete: Math.round((weekMinutes / weekGoal) * 100),
remainingMinutes: Math.max(0, weekGoal - weekMinutes),
sessionsThisWeek: weekSessions.length,
},
streak: { days: streak, message: streak >= 7 ? "Week streak! Keep going." : streak >= 3 ? `${streak}-day streak` : streak === 1 ? "Day 1 — start of a streak" : "No current streak" },
totalSessionsEver: db.sessions.length,
totalHoursEver: Math.round(db.sessions.reduce((a, s) => a + s.durationMinutes, 0) / 60 * 10) / 10,
});
}),
}),
tool({
name: "get_stats",
description: text`
View your learning statistics: total hours, weekly progress, streak,
most-studied categories, mood patterns, and session history summary.
`,
parameters: {
period: z.enum(["week", "month", "quarter", "all"]).default("month")
.describe("Time period to summarise"),
},
implementation: safe_impl("get_stats", async ({ period }) => {
const db = await loadDB(dataDir());
const today = new Date();
const todayStr = today.toISOString().slice(0, 10);
const cutoffDate = new Date(today);
if (period === "week") cutoffDate.setDate(today.getDate() - 7);
else if (period === "month") cutoffDate.setDate(today.getDate() - 30);
else if (period === "quarter") cutoffDate.setDate(today.getDate() - 90);
const cutoff = period === "all" ? "0000-00-00" : cutoffDate.toISOString().slice(0, 10);
const sessions = db.sessions.filter((s) => s.date >= cutoff);
const totalMins = sessions.reduce((a, s) => a + s.durationMinutes, 0);
// Category breakdown
const catMins: Record<string, number> = {};
for (const s of sessions) {
catMins[s.category] = (catMins[s.category] ?? 0) + s.durationMinutes;
}
const topCategories = Object.entries(catMins)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([cat, mins]) => ({ category: CATEGORY_LABELS[cat as SkillCategory] ?? cat, hours: Math.round(mins / 60 * 10) / 10 }));
// Mood breakdown
const moodCount: Record<string, number> = {};
for (const s of sessions) moodCount[s.mood] = (moodCount[s.mood] ?? 0) + 1;
// Streak
const sessionDates = new Set(db.sessions.map((s) => s.date));
let streak = 0;
const d = new Date();
while (sessionDates.has(d.toISOString().slice(0, 10))) {
streak++;
d.setDate(d.getDate() - 1);
}
// Weekly goal progress (current week)
const weekStart = new Date();
weekStart.setDate(weekStart.getDate() - weekStart.getDay() + 1);
const weekSessions = db.sessions.filter((s) => s.date >= weekStart.toISOString().slice(0, 10));
const weekMins = weekSessions.reduce((a, s) => a + s.durationMinutes, 0);
const weekGoal = weeklyGoal();
// Active goals
const activeGoals = db.goals.filter((g) => g.status === "active").length;
const overdueGoals = db.goals.filter(
(g) => g.status === "active" && g.targetDate < todayStr
).length;
return json({
developer: devName(),
period,
sessions: sessions.length,
totalHours: Math.round(totalMins / 60 * 10) / 10,
avgSessionMinutes: sessions.length > 0 ? Math.round(totalMins / sessions.length) : 0,
currentStreak: { days: streak },
weeklyGoal: { current: weekMins, target: weekGoal, percent: Math.round((weekMins / weekGoal) * 100) },
topCategories,
moodBreakdown: moodCount,
allTime: {
totalSessions: db.sessions.length,
totalHours: Math.round(db.sessions.reduce((a, s) => a + s.durationMinutes, 0) / 60 * 10) / 10,
skillsRated: db.skills.length,
resourcesAdded: db.resources.length,
resourcesCompleted: db.resources.filter((r) => r.status === "completed").length,
},
goals: { active: activeGoals, overdue: overdueGoals },
});
}),
}),
tool({
name: "list_sessions",
description: text`
List recent learning sessions with insights. Useful for weekly review
or finding patterns in what you've been studying.
`,
parameters: {
limit: z.coerce.number().int().min(1).max(50).default(10).describe("Number of sessions to return"),
category: z.enum([...SKILL_CATEGORIES, "all"] as const).default("all"),
},
implementation: safe_impl("list_sessions", async ({ limit, category }) => {
const db = await loadDB(dataDir());
let sessions = db.sessions.slice().sort((a, b) => b.date.localeCompare(a.date));
if (category !== "all") sessions = sessions.filter((s) => s.category === category);
sessions = sessions.slice(0, limit);
return json({
total: db.sessions.length,
shown: sessions.length,
sessions: sessions.map((s) => ({
id: s.id,
date: s.date,
topic: s.topic,
category: CATEGORY_LABELS[s.category],
durationMinutes: s.durationMinutes,
mood: s.mood,
insights: s.insights.slice(0, 150),
})),
});
}),
}),
// =========================================================================
// GOALS
// =========================================================================
tool({
name: "set_goal",
description: text`
Set a learning goal with a target date, category, and milestones.
Good goals are specific and time-bound: "Build a RAG pipeline from scratch by May 30"
not "Learn AI".
`,
parameters: {
title: z.string().describe("Specific goal (e.g. 'Build a production RAG pipeline', 'Get comfortable with LangGraph')"),
description: z.string().default("").describe("Why this goal matters to you and what done looks like"),
category: z.enum(SKILL_CATEGORIES),
targetDate: z.string().describe("Target completion date (YYYY-MM-DD)"),
milestones: z.array(z.string()).default([])
.describe("3–5 concrete milestones that mark progress toward this goal"),
},
implementation: safe_impl("set_goal", async (params) => {
const db = await loadDB(dataDir());
const goal: LearningGoal = {
id: makeId(),
title: params.title,
description: params.description,
category: params.category,
targetDate: params.targetDate,
status: "active",
progressPercent: 0,
milestones: params.milestones,
notes: "",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
db.goals.push(goal);
await saveDB(dataDir(), db);
const today = new Date().toISOString().slice(0, 10);
const daysLeft = Math.floor(
(new Date(params.targetDate).getTime() - new Date(today).getTime()) / (1000 * 60 * 60 * 24)
);
return json({
success: true,
goal,
daysLeft,
weeklyHoursNeeded: daysLeft > 0 ? Math.round(((daysLeft / 7) * 2 * 10) / 10) : "past due",
});
}),
}),
tool({
name: "update_goal",
description: text`
Update a goal's progress, status, or notes. Call this regularly to keep
your goals current. Completing milestones also helps track momentum.
`,
parameters: {
id: z.string().describe("Goal ID"),
progressPercent: z.coerce.number().int().min(0).max(100).optional(),
status: z.enum(["active", "completed", "abandoned"]).optional(),
notes: z.string().optional(),
milestones: z.array(z.string()).optional().describe("Replace milestone list"),
},
implementation: safe_impl("update_goal", async ({ id, ...updates }) => {
const db = await loadDB(dataDir());
const idx = db.goals.findIndex((g) => g.id === id);
if (idx === -1) throw new Error(`Goal ID '${id}' not found.`);
const patch = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
db.goals[idx] = { ...db.goals[idx], ...patch, updatedAt: new Date().toISOString() } as LearningGoal;
await saveDB(dataDir(), db);
return json({ success: true, goal: db.goals[idx] });
}),
}),
tool({
name: "list_goals",
description: "List all learning goals with progress and days remaining.",
parameters: {
status: z.enum(["active", "completed", "abandoned", "all"]).default("active"),
},
implementation: safe_impl("list_goals", async ({ status }) => {
const db = await loadDB(dataDir());
const today = new Date().toISOString().slice(0, 10);
let goals = db.goals;
if (status !== "all") goals = goals.filter((g) => g.status === status);
goals = goals.slice().sort((a, b) => a.targetDate.localeCompare(b.targetDate));
return json({
total: goals.length,
goals: goals.map((g) => {
const daysLeft = Math.floor(
(new Date(g.targetDate).getTime() - new Date(today).getTime()) / (1000 * 60 * 60 * 24)
);
return {
id: g.id,
title: g.title,
category: CATEGORY_LABELS[g.category],
status: g.status,
progressPercent: g.progressPercent,
targetDate: g.targetDate,
daysLeft,
overdue: daysLeft < 0 && g.status === "active",
milestones: g.milestones,
};
}),
});
}),
}),
// =========================================================================
// RESOURCES
// =========================================================================
tool({
name: "add_resource",
description: text`
Save a learning resource (paper, course, book, article, video, repo, podcast).
Build a personal reading/watching list tied to your skill goals.
`,
parameters: {
title: z.string().describe("Resource title"),
url: z.string().default("").describe("URL or reference"),
type: z.enum(["paper", "course", "book", "article", "video", "repo", "podcast", "other"]),
category: z.enum(SKILL_CATEGORIES),
tags: z.array(z.string()).default([]),
priority: z.enum(["low", "medium", "high"]).default("medium"),
notes: z.string().default("").describe("Why you're saving this, what you want to get from it"),
},
implementation: safe_impl("add_resource", async (params) => {
const db = await loadDB(dataDir());
const resource: Resource = {
id: makeId(),
title: params.title,
url: params.url,
type: params.type,
category: params.category,
tags: params.tags,
priority: params.priority,
status: "unread",
notes: params.notes,
rating: 0,
addedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
db.resources.push(resource);
await saveDB(dataDir(), db);
return json({ success: true, resource });
}),
}),
tool({
name: "list_resources",
description: "List saved resources filtered by status, category, type, or priority.",
parameters: {
status: z.enum(["unread", "in_progress", "completed", "abandoned", "all"]).default("unread"),
category: z.enum([...SKILL_CATEGORIES, "all"] as const).default("all"),
type: z.enum(["paper", "course", "book", "article", "video", "repo", "podcast", "other", "all"]).default("all"),
priority: z.enum(["low", "medium", "high", "all"]).default("all"),
},
implementation: safe_impl("list_resources", async ({ status, category, type, priority }) => {
const db = await loadDB(dataDir());
let resources = db.resources;
if (status !== "all") resources = resources.filter((r) => r.status === status);
if (category !== "all") resources = resources.filter((r) => r.category === category);
if (type !== "all") resources = resources.filter((r) => r.type === type);
if (priority !== "all") resources = resources.filter((r) => r.priority === priority);
const priorityOrder = { high: 3, medium: 2, low: 1 };
resources = resources.slice().sort((a, b) =>
(priorityOrder[b.priority] ?? 0) - (priorityOrder[a.priority] ?? 0)
);
const stats = {
total: db.resources.length,
unread: db.resources.filter((r) => r.status === "unread").length,
in_progress: db.resources.filter((r) => r.status === "in_progress").length,
completed: db.resources.filter((r) => r.status === "completed").length,
};
return json({
stats,
filter: { status, category, type, priority },
resources: resources.map((r) => ({
id: r.id,
title: r.title,
type: r.type,
category: CATEGORY_LABELS[r.category],
priority: r.priority,
status: r.status,
rating: r.rating || "not rated",
url: r.url,
notes: r.notes.slice(0, 100),
})),
});
}),
}),
tool({
name: "update_resource",
description: "Update a resource's status, rating, or notes after reading/watching it.",
parameters: {
id: z.string().describe("Resource ID"),
status: z.enum(["unread", "in_progress", "completed", "abandoned"]).optional(),
rating: z.coerce.number().int().min(1).max(5).optional().describe("Rating 1–5 after completing"),
notes: z.string().optional().describe("Notes or key takeaways after reading"),
priority: z.enum(["low", "medium", "high"]).optional(),
},
implementation: safe_impl("update_resource", async ({ id, ...updates }) => {
const db = await loadDB(dataDir());
const idx = db.resources.findIndex((r) => r.id === id);
if (idx === -1) throw new Error(`Resource ID '${id}' not found.`);
const patch = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
db.resources[idx] = { ...db.resources[idx], ...patch, updatedAt: new Date().toISOString() } as Resource;
await saveDB(dataDir(), db);
return json({ success: true, resource: db.resources[idx] });
}),
}),
tool({
name: "mark_resource_complete",
description: text`
Mark a resource as completed, capturing your rating and key takeaways in one call.
Use this after finishing a course, paper, book, or video — it's faster than update_resource
and prompts for reflection notes that make the list useful long-term.
`,
parameters: {
id: z.string().describe("Resource ID (from list_resources)"),
rating: z.coerce.number().int().min(1).max(5).describe("Rating 1–5 (5 = must-read, 1 = waste of time)"),
takeaways: z.string().default("").describe("Key things you learned or will apply — helps retention"),
},
implementation: safe_impl("mark_resource_complete", async ({ id, rating, takeaways }) => {
const db = await loadDB(dataDir());
const idx = db.resources.findIndex((r) => r.id === id);
if (idx === -1) throw new Error(`Resource ID '${id}' not found.`);
const resource = db.resources[idx];
db.resources[idx] = {
...resource,
status: "completed",
rating,
notes: takeaways
? `${resource.notes ? resource.notes + "\n\n" : ""}[Completed ${new Date().toISOString().slice(0, 10)}] ${takeaways}`
: resource.notes,
updatedAt: new Date().toISOString(),
};
await saveDB(dataDir(), db);
const completed = db.resources.filter((r) => r.status === "completed").length;
const remaining = db.resources.filter((r) => r.status === "unread" || r.status === "in_progress").length;
return json({
success: true,
resource: db.resources[idx],
stats: { totalCompleted: completed, remaining },
message: `Marked "${resource.title}" as complete (${rating}/5). ${remaining} resource${remaining === 1 ? "" : "s"} left in your list.`,
});
}),
}),
tool({
name: "next_resource",
description: text`
Suggest what to learn next based on your highest-priority unstarted resources,
active learning goals, and current skill gaps.
Filters out completed and abandoned resources, sorts by priority and relevance to goals.
`,
parameters: {
category: z.enum([...SKILL_CATEGORIES, "any"] as const).default("any")
.describe("Filter to a specific skill category, or 'any' for all"),
limit: z.coerce.number().int().min(1).max(10).default(3),
},
implementation: safe_impl("next_resource", async ({ category, limit }) => {
const db = await loadDB(dataDir());
// Active goals categories for relevance scoring
const activeGoalCategories = new Set(
db.goals.filter((g) => g.status === "active").map((g) => g.category)
);
let candidates = db.resources.filter(
(r) => r.status === "unread" || r.status === "in_progress"
);
if (category !== "any") candidates = candidates.filter((r) => r.category === category);
const priorityScore = { high: 30, medium: 20, low: 10 };
const statusScore = { in_progress: 5, unread: 0, completed: -100, abandoned: -100 };
candidates = candidates
.map((r) => ({
resource: r,
score:
(priorityScore[r.priority] ?? 0) +
(statusScore[r.status] ?? 0) +
(activeGoalCategories.has(r.category) ? 15 : 0),
}))
.sort((a, b) => b.score - a.score)
.slice(0, limit)
.map((x) => x.resource);
return json({
suggestions: candidates.map((r) => ({
id: r.id,
title: r.title,
type: r.type,
category: CATEGORY_LABELS[r.category],
priority: r.priority,
status: r.status,
url: r.url,
notes: r.notes.slice(0, 150),
alignsWithActiveGoal: activeGoalCategories.has(r.category),
})),
queueStats: {
unread: db.resources.filter((r) => r.status === "unread").length,
in_progress: db.resources.filter((r) => r.status === "in_progress").length,
completed: db.resources.filter((r) => r.status === "completed").length,
},
tip: candidates.length === 0
? "No resources in your list. Use search_resources to find something to learn."
: `${candidates[0].status === "in_progress" ? "Resume" : "Start"} with: "${candidates[0].title}"`,
});
}),
}),
tool({
name: "search_resources",
description: text`
Search the web for learning resources on a specific LLM-era skill or topic.
Targets high-signal sources: Hugging Face, Arxiv, GitHub, official docs,
Andrej Karpathy, Simon Willison, Eugene Yan, Chip Huyen, and other trusted voices.
`,
parameters: {
topic: z.string().describe("Topic to search for (e.g. 'RAG evaluation', 'LangGraph tutorial', 'LoRA fine-tuning')"),
resourceType: z.enum(["paper", "course", "tutorial", "repo", "blog", "any"]).default("any"),
level: z.enum(["beginner", "intermediate", "advanced", "any"]).default("any"),
},
implementation: safe_impl("search_resources", async ({ topic, resourceType, level }) => {
const year = new Date().getFullYear();
const levelTag = level !== "any" ? ` ${level}` : "";
const typeMap: Record<string, string> = {
paper: `site:arxiv.org OR site:huggingface.co/papers`,
course: `course OR tutorial`,
tutorial: `tutorial OR guide OR "step by step"`,
repo: `site:github.com`,
blog: `${EXPERT_BLOGS} OR blog`,
any: "",
};
const siteFilter = typeMap[resourceType] ?? "";
const q = `${topic}${levelTag} LLM ${year} ${siteFilter}`.trim();
const results = await webSearch(q, maxResults(), "year");
return json({
topic,
resourceType,
level,
results,
trustedSources: [
"arxiv.org — preprints and papers",
"huggingface.co — models, datasets, courses",
"github.com — repos and implementations",
"fast.ai — practical deep learning",
`practitioner blogs searched: ${EXPERT_BLOGS}`,
],
});
}),
}),
// =========================================================================
// PLANNING
// =========================================================================
tool({
name: "generate_learning_path",
description: text`
Generate a personalised learning roadmap for ONE specific skill.
Call this once per skill. For a full career transition, call it 3 times —
once for each of the top critical gaps returned by assess_skill_gaps.
Returns a phased plan: Foundation → Core Skills → Build Projects → Advanced,
with specific resources, time estimates, and milestone checkpoints.
Calibrated to the developer's current rating, target rating, and weekly hours.
`,
parameters: {
skill: z.string().describe("Skill or area to build a path for (e.g. 'RAG systems', 'LLM Evaluation', 'Agents')"),
category: z.enum(SKILL_CATEGORIES),
currentRating: z.coerce.number().int().min(0).max(10).default(0)
.describe("Your current level in this skill (0 = never touched it)"),
targetRating: z.coerce.number().int().min(1).max(10).default(8)
.describe("Where you want to get to (8 = comfortable building production systems)"),
timelineWeeks: z.coerce.number().int().min(2).max(52).default(12)
.describe("Weeks you want to reach target rating in"),
weeklyHours: z.coerce.number().min(1).max(40).default(5)
.describe("Hours per week you can commit"),
},
implementation: safe_impl("generate_learning_path", async (params) => {
const role = currentRole();
const target = targetRole();
const years = expYears();
const year = new Date().getFullYear();
const db = await loadDB(dataDir());
const completedInCategory = db.resources
.filter((r) => r.category === params.category && r.status === "completed")
.map((r) => r.title);
const totalHours = params.timelineWeeks * params.weeklyHours;
const ratingGap = params.targetRating - params.currentRating;
const depthLabel = params.currentRating <= 3 ? "beginner" : params.currentRating <= 6 ? "intermediate" : "advanced";
// Live searches for current, real resources — no hardcoded lists
const searchQueries = [
{ angle: "roadmap", query: `"${params.skill}" learning roadmap path ${year}` },
{ angle: "tutorials", query: `"${params.skill}" tutorial guide ${depthLabel} ${year} site:github.com OR site:huggingface.co OR site:arxiv.org` },
{ angle: "courses", query: `"${params.skill}" course ${year} best free` },
{ angle: "projects", query: `"${params.skill}" project ideas build practice hands-on ${year}` },
{ angle: "expert_blog", query: `"${params.skill}" ${year} ${EXPERT_BLOGS}` },
];
const webResources: Array<{ angle: string; query: string; results: Array<{ title: string; url: string; snippet: string }> }> = [];
for (const s of searchQueries) {
const results = await webSearch(s.query, maxResults(), "year");
webResources.push({ angle: s.angle, query: s.query, results });
await sleep(350);
}
return json({
skill: params.skill,
category: CATEGORY_LABELS[params.category],
developer: { name: devName(), role, targetRole: target, years },
currentRating: params.currentRating,
targetRating: params.targetRating,
ratingGap,
currentLevel: depthLabel,
timelineWeeks: params.timelineWeeks,
weeklyHours: params.weeklyHours,
totalHours,
alreadyCompleted: completedInCategory.length > 0 ? completedInCategory : null,
web_resources: webResources,
instructions:
`Using ONLY the live web_resources found above (not your training data), build a ${params.timelineWeeks}-week ` +
`learning path for ${devName()} to go from ${params.currentRating}/10 to ${params.targetRating}/10 in "${params.skill}". ` +
`Background: ${role}, ${years} yrs experience. Available: ${params.weeklyHours} hrs/week = ${totalHours} total hours. ` +
(completedInCategory.length > 0 ? `Already completed: ${completedInCategory.join(", ")} — skip these. ` : "") +
"Structure:\n" +
"**Phase 1 — Foundation** (weeks 1–2): Core concepts. Pick the single best beginner resource from web_resources.\n" +
"**Phase 2 — Core Skills** (weeks 3–6): Hands-on. Pick 1–2 tutorials/repos from web_resources.\n" +
"**Phase 3 — Build** (weeks 7–10): One concrete project idea. Find a real repo or example from web_resources to reference.\n" +
"**Phase 4 — Advanced** (weeks 11+): Papers, production patterns. Use the expert_blog results.\n" +
"For each resource: use the real URL from search results. Include estimated hours and a milestone. " +
"Be opinionated — pick the best, not all of them.",
});
}),
}),
tool({
name: "replan_learning_path",
description: text`
Adapt your learning plan based on actual progress.
Use this when: you're ahead/behind schedule, your goals changed, or a generated plan feels stale.
Reads your real session logs, skill ratings, and completed resources for a given skill,
then generates a revised plan that starts from where you actually are — not where you planned to be.
`,
parameters: {
skill: z.string().describe("Skill to replan for — should match a previous generate_learning_path call"),
category: z.enum(SKILL_CATEGORIES),
targetRating: z.coerce.number().int().min(1).max(10).default(8)
.describe("Target rating (can update this if goals changed)"),
remainingWeeks: z.coerce.number().int().min(1).max(52).default(8)
.describe("How many weeks you have left"),
weeklyHours: z.coerce.number().min(1).max(40).default(5),
whatWorked: z.string().default("")
.describe("Optional: what approaches or resources helped most so far"),
whatDidnt: z.string().default("")
.describe("Optional: what wasn't working or felt like wasted time"),
},
implementation: safe_impl("replan_learning_path", async (params) => {
const db = await loadDB(dataDir());
// Pull actual data for this skill
const skill = db.skills.find(
(s) => s.skill.toLowerCase().includes(params.skill.toLowerCase())
);
const currentRating = skill?.rating ?? 0;
const relevantSessions = db.sessions
.filter((s) => s.topic.toLowerCase().includes(params.skill.toLowerCase()))
.sort((a, b) => b.date.localeCompare(a.date))
.slice(0, 10);
const completedResources = db.resources
.filter((r) => r.category === params.category && r.status === "completed")
.map((r) => ({ title: r.title, rating: r.rating, notes: r.notes.slice(0, 100) }));
const inProgressResources = db.resources
.filter((r) => r.category === params.category && r.status === "in_progress")
.map((r) => ({ title: r.title, url: r.url }));
const ratingGap = params.targetRating - currentRating;
const totalHours = params.remainingWeeks * params.weeklyHours;
const actualHoursLogged = relevantSessions.reduce((sum, s) => sum + s.durationMinutes, 0) / 60;
// Search for fresh resources based on current level
const level = currentRating <= 3 ? "beginner" : currentRating <= 6 ? "intermediate" : "advanced";
const year = new Date().getFullYear();
const freshResources = await webSearch(
`"${params.skill}" ${level} to advanced ${year} tutorial project`, maxResults(), "year"
);
return json({
skill: params.skill,
replanContext: {
currentRating,
targetRating: params.targetRating,
ratingGap,
currentLevel: level,
remainingWeeks: params.remainingWeeks,
weeklyHours: params.weeklyHours,
totalRemainingHours: totalHours,
actualHoursLoggedSoFar: Math.round(actualHoursLogged * 10) / 10,
},
actualProgress: {
recentSessions: relevantSessions.map((s) => ({
date: s.date,
topic: s.topic,
minutes: s.durationMinutes,
mood: s.mood,
})),
completedResources,
inProgressResources,
},
userFeedback: {
whatWorked: params.whatWorked || null,
whatDidnt: params.whatDidnt || null,
},
freshWebResources: freshResources,
instructions:
`Replan the learning path for "${params.skill}" based on ACTUAL progress, not the original plan. ` +
`Current real rating: ${currentRating}/10. Target: ${params.targetRating}/10. ` +
`${params.remainingWeeks} weeks left, ${params.weeklyHours} hrs/week = ${totalHours} hours total. ` +
`The developer has already completed: ${completedResources.map((r) => r.title).join(", ") || "nothing yet"}. ` +
(params.whatWorked ? `What worked: ${params.whatWorked}. ` : "") +
(params.whatDidnt ? `What didn't work: ${params.whatDidnt} — AVOID recommending these approaches. ` : "") +
"Produce a revised week-by-week plan that: " +
"(1) Skips what's already done, " +
"(2) Continues any in-progress resources, " +
"(3) Doubles down on what's working, " +
"(4) Adjusts difficulty to the current level, " +
"(5) Is realistic given the remaining time. " +
"Be direct about whether the target is still achievable in the timeframe.",
});
}),
}),
tool({
name: "generate_study_plan",
description: text`
Plan a focused study session for today or the week.
Considers your active goals, unread high-priority resources, and recent session patterns.
Returns a concrete study agenda — not vague advice.
`,
parameters: {
sessionType: z.enum(["today", "week"]).default("today")
.describe("Plan for a single session today or the full week"),
availableMinutes: z.coerce.number().int().min(20).max(480).default(60)
.describe("How much time you have available (for 'today')"),
},
implementation: safe_impl("generate_study_plan", async ({ sessionType, availableMinutes }) => {
const db = await loadDB(dataDir());
const today = new Date().toISOString().slice(0, 10);
const activeGoals = db.goals.filter((g) => g.status === "active")
.sort((a, b) => a.targetDate.localeCompare(b.targetDate))
.slice(0, 3);
const highPriorityResources = db.resources
.filter((r) => r.status === "unread" && r.priority === "high")
.slice(0, 5)
.map((r) => ({ title: r.title, type: r.type, category: CATEGORY_LABELS[r.category] }));
const recentCategories = db.sessions
.filter((s) => s.date >= new Date(Date.now() - 7 * 864e5).toISOString().slice(0, 10))
.map((s) => s.category);
const recentCategorySet = [...new Set(recentCategories)];
// Week stats
const weekStart = new Date();
weekStart.setDate(weekStart.getDate() - weekStart.getDay() + 1);
const weekMins = db.sessions
.filter((s) => s.date >= weekStart.toISOString().slice(0, 10))
.reduce((a, s) => a + s.durationMinutes, 0);
const remainingWeekMins = Math.max(0, weeklyGoal() - weekMins);
return json({
developer: devName(),
sessionType,
today,
availableMinutes: sessionType === "today" ? availableMinutes : null,
weeklyGoalMinutes: weeklyGoal(),
weekMinutesSoFar: weekMins,
remainingWeekMinutes: remainingWeekMins,
activeGoals: activeGoals.map((g) => ({
title: g.title,
category: CATEGORY_LABELS[g.category],
progress: g.progressPercent,
targetDate: g.targetDate,
daysLeft: Math.floor((new Date(g.targetDate).getTime() - Date.now()) / 864e5),
})),
highPriorityResources,
recentlyStudiedCategories: recentCategorySet.map((c) => CATEGORY_LABELS[c as SkillCategory] ?? c),
instructions:
sessionType === "today"
? `Plan a focused ${availableMinutes}-minute study session for ${devName()} today (${today}). ` +
`Remaining weekly goal: ${remainingWeekMins} mins. Active goals and top resources above. ` +
"Return: (1) What to study and why (pick ONE focus), (2) Exact agenda with time blocks, " +
"(3) A specific deliverable for the session (e.g. 'implement a basic RAG loop'), " +
"(4) One question to answer by the end of the session."
: `Plan the week for ${devName()}. Weekly goal: ${weeklyGoal()} mins, done so far: ${weekMins} mins. ` +
"Distribute learning across 4–5 sessions. " +
"Return a day-by-day plan (Mon–Fri) with topic, duration, and goal for each session.",
});
}),
}),
tool({
name: "weekly_review",
description: text`
Generate a structured weekly review — what you learned, how it connects
to your goals, what to adjust, and what to focus on next week.
Best done every Friday or Sunday. Takes ~15 minutes.
`,
parameters: {
additionalNotes: z.string().default("")
.describe("Anything specific you want to reflect on this week"),
},
implementation: safe_impl("weekly_review", async ({ additionalNotes }) => {
const db = await loadDB(dataDir());
const today = new Date();
const todayStr = today.toISOString().slice(0, 10);
// Mon–Sun week — matches log_session, get_stats, generate_study_plan
const weekStart = new Date(today);
weekStart.setDate(weekStart.getDate() - weekStart.getDay() + 1);
const weekStartStr = weekStart.toISOString().slice(0, 10);
const weekSessions = db.sessions.filter((s) => s.date >= weekStartStr);
const weekMins = weekSessions.reduce((a, s) => a + s.durationMinutes, 0);
const weekGoal = weeklyGoal();
const goalMet = weekMins >= weekGoal;
const insights = weekSessions
.filter((s) => s.insights)
.map((s) => ({ topic: s.topic, insight: s.insights.slice(0, 200) }));
const completedResources = db.resources
.filter((r) => r.status === "completed" && r.updatedAt >= weekStartStr + "T00:00:00.000Z")
.map((r) => ({ title: r.title, type: r.type, rating: r.rating }));
const activeGoals = db.goals
.filter((g) => g.status === "active")
.map((g) => ({
title: g.title,
progress: g.progressPercent,
daysLeft: Math.floor((new Date(g.targetDate).getTime() - Date.now()) / 864e5),
overdue: g.targetDate < todayStr,
}));
// Streak
const sessionDates = new Set(db.sessions.map((s) => s.date));
let streak = 0;
const d = new Date();
while (sessionDates.has(d.toISOString().slice(0, 10))) { streak++; d.setDate(d.getDate() - 1); }
return json({
developer: devName(),
weekOf: weekStartStr,
weeklyGoalMet: goalMet,
minutesThisWeek: weekMins,
weeklyGoalMinutes: weekGoal,
percentOfGoal: Math.round((weekMins / weekGoal) * 100),
sessionCount: weekSessions.length,
currentStreak: streak,
topicsStudied: weekSessions.map((s) => s.topic),
keyInsights: insights,
completedResources,
activeGoals,
additionalNotes,
instructions:
`Run a weekly review for ${devName()} (${currentRole()}). ` +
`This week: ${weekSessions.length} sessions, ${Math.round(weekMins / 60 * 10) / 10} hours, ` +
`goal ${goalMet ? "MET" : "NOT MET"} (${weekMins}/${weekGoal} min). ` +
"Structure the review:\n" +
"**What I learned** — summarise the week's sessions and insights in 3 bullets\n" +
"**What clicked** — one concept that became clearer this week\n" +
"**What I'm stuck on** — one thing still confusing or hard\n" +
"**Goal progress** — honest assessment of each active goal\n" +
"**What to adjust** — one change to make next week (pace, focus, resources)\n" +
"**Next week focus** — one primary topic and one specific deliverable\n" +
(additionalNotes ? `Additional context: ${additionalNotes}` : ""),
});
}),
}),
// =========================================================================
// CONTENT & RESEARCH
// =========================================================================
tool({
name: "explain_concept",
description: text`
Get a clear explanation of any LLM-era concept at a specified depth.
Returns: what it is, why it matters, how it works, common misconceptions,
where it fits in the ecosystem, and what to build to understand it.
`,
parameters: {
concept: z.string().describe("Concept to explain (e.g. 'RLHF', 'KV cache', 'speculative decoding', 'RAG vs fine-tuning', 'PEFT vs LoRA')"),
depth: z.enum(["overview", "intermediate", "deep_dive"]).default("intermediate")
.describe("How deep to go"),
relatedTo: z.string().default("").describe("Optional: relate the explanation to a specific use case or problem"),
},
implementation: safe_impl("explain_concept", async ({ concept, depth, relatedTo }) => {
const years = expYears();
const role = currentRole();
const year = new Date().getFullYear();
const db = await loadDB(dataDir());
const relevantSkills = db.skills.slice(0, 5).map((s) => `${s.skill} (${s.rating}/10)`);
// Live searches: current understanding + best resource right now
const searches = [
{ angle: "current_state", query: `"${concept}" explained ${year} how it works` },
{ angle: "best_resource", query: `"${concept}" best tutorial blog paper ${year} site:arxiv.org OR ${EXPERT_BLOGS} OR site:github.com` },
...(relatedTo ? [{ angle: "applied", query: `"${concept}" "${relatedTo}" practical example ${year}` }] : []),
];
const webResults: Array<{ angle: string; results: Array<{ title: string; url: string; snippet: string }> }> = [];
for (const s of searches) {
webResults.push({ angle: s.angle, results: await webSearch(s.query, 6, searchWindow()) });
await sleep(300);
}
return json({
concept,
depth,
relatedTo: relatedTo || null,
developerContext: { role, years, relevantSkills },
web_results: webResults,
instructions:
`Explain "${concept}" at ${depth} depth for a ${years}-year ${role}. ` +
(relatedTo ? `Frame it in the context of: "${relatedTo}". ` : "") +
"Use the web_results above to ground your explanation in current understanding and cite a real resource. " +
"Structure:\n" +
"**What it is** (1–2 sentences, no jargon)\n" +
"**Why it matters** (practical impact — when does this change what you build?)\n" +
"**How it works** " + (depth === "overview" ? "(high-level mental model only)" : depth === "intermediate" ? "(key mechanism with a simple example)" : "(detailed mechanism, math if relevant, edge cases)") + "\n" +
"**Common misconceptions** (what people usually get wrong)\n" +
"**Where it fits** (what problem it solves, alternatives, when NOT to use it)\n" +
"**Build to understand** (one specific thing to build to make this concrete)\n" +
"**Best resource** — pick the single best link from web_results.best_resource with its real URL\n" +
"No padding. Be direct.",
});
}),
}),
tool({
name: "generate_project_idea",
description: text`
Generate a concrete, buildable project idea that develops a specific LLM-era skill.
Projects are the fastest way to go from understanding → competence.
Returns a scoped project with clear learning objectives, tech stack, and milestones.
Calibrated to your experience level and the skill you're building.
`,
parameters: {
skill: z.string().describe("Skill you want to build (e.g. 'RAG', 'agents', 'LLM evaluation', 'fine-tuning')"),
category: z.enum(SKILL_CATEGORIES),
difficulty: z.enum(["weekend", "week", "month"]).default("week")
.describe("Scope: weekend (4–8hrs), week (10–20hrs), month (40–80hrs)"),
domain: z.string().default("").describe("Optional: domain to apply the skill to (e.g. 'finance', 'code', 'healthcare', 'developer tools')"),
count: z.coerce.number().int().min(1).max(5).default(3)
.describe("Number of project ideas to generate"),
},
implementation: safe_impl("generate_project_idea", async (params) => {
const role = currentRole();
const years = expYears();
const year = new Date().getFullYear();
const db = await loadDB(dataDir());
const completedInCategory = db.resources
.filter((r) => r.category === params.category && r.status === "completed")
.map((r) => r.title);
const domainStr = params.domain ? ` ${params.domain}` : "";
// Live searches: what are people actually building with this skill right now
const searches = [
{ angle: "project_ideas", query: `"${params.skill}"${domainStr} project ideas build ${year} github` },
{ angle: "real_examples", query: `"${params.skill}"${domainStr} example implementation repo ${year} site:github.com` },
{ angle: "tech_stack", query: `"${params.skill}" tech stack tools libraries ${year} best` },
{ angle: "portfolio", query: `"${params.skill}" portfolio project showcase ${year} developer` },
];
const webResults: Array<{ angle: string; results: Array<{ title: string; url: string; snippet: string }> }> = [];
for (const s of searches) {
webResults.push({ angle: s.angle, results: await webSearch(s.query, 6, searchWindow()) });
await sleep(300);
}
return json({
skill: params.skill,
category: CATEGORY_LABELS[params.category],
difficulty: params.difficulty,
domain: params.domain || "any",
developer: { role, years },
alreadyCompleted: completedInCategory.length > 0 ? completedInCategory : null,
web_results: webResults,
instructions:
`Using the live web_results above, generate ${params.count} ${params.difficulty}-scope project idea(s) ` +
`that build "${params.skill}" skills for a ${years}-year ${role}. ` +
(params.domain ? `Domain focus: ${params.domain}. ` : "") +
"Base the tech stack on what real_examples and tech_stack searches show people using RIGHT NOW. " +
"Reference real repos from the web_results where relevant. " +
"For each project:\n" +
"**Project name** — specific, not generic\n" +
"**Problem it solves** — real problem, not toy\n" +
"**What you'll learn** — 3 concrete skills this builds\n" +
"**Tech stack** — exact current libraries/APIs from the search results\n" +
"**Reference repo** — a real GitHub link from web_results to use as inspiration (if found)\n" +
"**Milestones** — 3–5 steps from zero to done\n" +
"**Portfolio value** — how to present this\n" +
"Make the projects genuinely useful — something real, not a tutorial rehash.",
});
}),
}),
tool({
name: "search_ai_trends",
description: text`
Search for what's happening RIGHT NOW in the LLM and AI space.
Find recent papers, model releases, framework updates, and emerging patterns.
Filters for high-signal sources (Hugging Face, Arxiv, AI Twitter, research blogs).
Helps you stay current without drowning in noise.
`,
parameters: {
topic: z.string().default("").describe("Specific area to track (blank = general AI/LLM trends)"),
focus: z.enum(["papers", "releases", "tools", "opinions", "all"]).default("all")
.describe("What type of content to look for"),
},
implementation: safe_impl("search_ai_trends", async ({ topic, focus }) => {
const year = new Date().getFullYear();
const month = new Date().toLocaleString("default", { month: "long" });
const queries: Record<string, string> = {
papers: `${topic || "LLM"} research paper ${month} ${year} site:arxiv.org OR site:huggingface.co`,
releases: `${topic || "AI LLM"} new release model update ${month} ${year}`,
tools: `${topic || "AI developer"} tool framework library ${year} github`,
opinions: `${topic || "LLM AI"} ${year} ${month} ${EXPERT_BLOGS}`,
all: `${topic || "LLM AI"} ${month} ${year} latest`,
};
const q = queries[focus] ?? queries.all;
const results = await webSearch(q, maxResults(), "month");
return json({
topic: topic || "AI/LLM general",
focus,
period: `${month} ${year}`,
results,
signalSources: {
papers: "arxiv.org, huggingface.co/papers",
news: "the-decoder.com, techcrunch.com/ai",
practitioners: EXPERT_BLOGS,
community: "reddit.com/r/LocalLLaMA, news.ycombinator.com",
},
instructions:
"From these search results, present what is actually in the data — do not rank by your priors: " +
"(1) Developments appearing across multiple search results — list all significant ones, note which have the most coverage, " +
"(2) Where sources agree on importance AND where they disagree — show both sides of any debate, " +
"(3) Signals about future demand — cite which sources say what; flag if the signal comes from a single vendor or community vs. multiple independent sources. " +
"Do not assert something is 'hype' vs 'genuinely useful' without evidence from the search results supporting that distinction.",
});
}),
}),
tool({
name: "compare_ai_tools",
description: text`
Compare AI frameworks, libraries, or tools for a specific use case.
Covers: LangChain vs LlamaIndex vs raw API, vector DB options, orchestration frameworks,
embedding models, evaluation libraries, and more.
Returns trade-offs, when to use each, and a recommendation for your situation.
`,
parameters: {
useCase: z.string().describe("What you're trying to build (e.g. 'document Q&A', 'multi-step agent', 'semantic search')"),
tools: z.array(z.string()).default([])
.describe("Specific tools to compare (blank = let the model suggest the main options)"),
constraints: z.string().default("")
.describe("Your constraints: 'local-only', 'no API costs', 'production-ready', 'simplest possible', etc."),
},
implementation: safe_impl("compare_ai_tools", async ({ useCase, tools, constraints }) => {
const role = currentRole();
const years = expYears();
// Search for recent comparisons
const searchQuery = `${useCase} ${tools.join(" vs ")} ${new Date().getFullYear()} comparison best`;
const searchResults = await webSearch(searchQuery, 5, searchWindow());
return json({
useCase,
toolsToCompare: tools.length > 0 ? tools : "suggest the main options",
constraints: constraints || "none specified",
developer: { role, years },
searchResults,
instructions:
`Compare tools for: "${useCase}"${tools.length > 0 ? ` (${tools.join(" vs ")})` : ""}. ` +
`Constraints: ${constraints || "none"}. Developer: ${years}-year ${role}. ` +
"Base the comparison on the search results above — do not substitute your training data for live evidence. " +
"Return:\n" +
"**Options** — list all significant tools found in the search results for this use case\n" +
"**Comparison table** — tradeoffs across: ease of use, production-readiness, performance, cost, community. " +
"Show strengths AND weaknesses for each tool; do not omit known issues.\n" +
"**When to use each** — describe the conditions under which each tool is the better choice\n" +
"**For these specific constraints** — given the constraints above, explain which trade-offs matter most and why. " +
"If the evidence from the search results does not clearly favor one tool, say so — do not pick a winner when the data is genuinely ambiguous.\n" +
"**Watch out for** — documented pitfalls found in the search results, not general advice.",
});
}),
}),
// =========================================================================
// EXPORT
// =========================================================================
tool({
name: "export_growth_report",
description: text`
Export a full Markdown growth report to disk.
Includes: skill map, learning stats, goal progress, completed resources,
and a summary of recent insights. Good for quarterly reviews or sharing with a mentor.
`,
parameters: {
outputPath: z.string().default("").describe("File path to save the report. Defaults to <dataPath>/report-<date>.md"),
period: z.enum(["month", "quarter", "all"]).default("quarter"),
},
implementation: safe_impl("export_growth_report", async ({ outputPath, period }) => {
const db = await loadDB(dataDir());
const date = new Date().toISOString().slice(0, 10);
const outPath = outputPath.trim() || join(dataDir(), `growth-report-${date}.md`);
const cutoffDays = period === "month" ? 30 : period === "quarter" ? 90 : 36500;
const cutoff = new Date(Date.now() - cutoffDays * 864e5).toISOString().slice(0, 10);
const sessions = db.sessions.filter((s) => s.date >= cutoff);
const totalMins = sessions.reduce((a, s) => a + s.durationMinutes, 0);
let md = `# Developer Growth Report — ${devName()}\n`;
md += `**Generated**: ${date} | **Period**: ${period} | **Role**: ${currentRole()}\n\n`;
if (targetRole()) md += `**Target**: ${targetRole()}\n\n`;
md += `---\n\n`;
// Stats
md += `## Learning Stats (${period})\n\n`;
md += `| Metric | Value |\n|--------|-------|\n`;
md += `| Sessions | ${sessions.length} |\n`;
md += `| Total Hours | ${Math.round(totalMins / 60 * 10) / 10} |\n`;
md += `| Avg Session | ${sessions.length > 0 ? Math.round(totalMins / sessions.length) : 0} min |\n`;
md += `| Skills Rated | ${db.skills.length} |\n`;
md += `| Resources Completed | ${db.resources.filter((r) => r.status === "completed").length} |\n`;
md += `\n`;
// Skill map
if (db.skills.length > 0) {
md += `## Skill Map\n\n`;
const grouped: Record<string, SkillRating[]> = {};
for (const s of db.skills) {
if (!grouped[s.category]) grouped[s.category] = [];
grouped[s.category].push(s);
}
for (const [cat, skills] of Object.entries(grouped)) {
const avg = Math.round(skills.reduce((a, s) => a + s.rating, 0) / skills.length * 10) / 10;
md += `### ${CATEGORY_LABELS[cat as SkillCategory] ?? cat} (avg ${avg}/10)\n\n`;
for (const s of skills.sort((a, b) => b.rating - a.rating)) {
const bar = "â–ˆ".repeat(Math.round(s.rating)) + "â–‘".repeat(10 - Math.round(s.rating));
md += `- **${s.skill}**: ${bar} ${s.rating}/10\n`;
}
md += `\n`;
}
}
// Goals
if (db.goals.length > 0) {
md += `## Goals\n\n`;
for (const g of db.goals) {
const icon = g.status === "completed" ? "✓" : g.status === "abandoned" ? "✗" : "○";
md += `${icon} **${g.title}** — ${g.progressPercent}% — due ${g.targetDate}\n`;
if (g.milestones.length > 0) {
for (const m of g.milestones) md += ` - ${m}\n`;
}
md += `\n`;
}
}
// Recent insights
const insights = sessions.filter((s) => s.insights).slice(-10);
if (insights.length > 0) {
md += `## Key Insights (${period})\n\n`;
for (const s of insights) {
md += `**${s.date} — ${s.topic}**\n> ${s.insights}\n\n`;
}
}
// Completed resources
const completed = db.resources.filter((r) => r.status === "completed" && r.updatedAt >= cutoff + "T00:00:00.000Z");
if (completed.length > 0) {
md += `## Completed Resources\n\n`;
for (const r of completed) {
const stars = r.rating > 0 ? "★".repeat(r.rating) + "☆".repeat(5 - r.rating) : "not rated";
md += `- **${r.title}** (${r.type}) ${stars}\n`;
if (r.notes) md += ` > ${r.notes.slice(0, 150)}\n`;
}
md += `\n`;
}
await mkdir(dataDir(), { recursive: true });
await writeFile(outPath, md, "utf8");
return json({
success: true,
path: outPath,
sessionsIncluded: sessions.length,
skillsIncluded: db.skills.length,
goalsIncluded: db.goals.length,
resourcesIncluded: completed.length,
});
}),
}),
];
return tools;
};