toolsProvider.js
"use strict";
/**
* Ideas & Pain Points Plugin — toolsProvider
*
* Tools:
* Ideas · capture_idea, list_ideas, get_idea, update_idea, delete_idea
* Pain Points · capture_pain_point, list_pain_points, get_pain_point, update_pain_point
* Analysis · generate_problem_statement, evaluate_idea, link_pain_to_idea
* Generation · generate_solution_brief, brainstorm_solutions
* Research · search_similar_problems, search_market_size, search_competitors
* Validation · map_assumptions, design_experiment, log_experiment_result,
* generate_validation_questions, build_validation_scorecard
* Testing · define_mvp, generate_landing_page_copy
* Dashboard · validation_dashboard
* Export · export_report
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.toolsProvider = void 0;
const sdk_1 = require("@lmstudio/sdk");
const promises_1 = require("fs/promises");
const path_1 = require("path");
const os_1 = require("os");
const search_1 = require("./search");
const zod_1 = require("zod");
const config_1 = require("./config");
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function json(obj) {
return JSON.stringify(obj, null, 2);
}
function safe_impl(name, fn) {
return async (params) => {
try {
return await fn(params);
}
catch (err) {
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 stripHtml(html) {
return html
.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, " ")
.replace(/<[^>]+>/g, " ")
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
.replace(/ /g, " ").replace(/"/g, '"').replace(/'/g, "'")
.replace(/[ \t]+/g, " ")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
function getDataDir(configPath) {
return configPath.trim() || (0, path_1.join)((0, os_1.homedir)(), "ideas-data");
}
function dbPath(dataDir) {
return (0, path_1.join)(dataDir, "ideas.json");
}
async function loadDB(dataDir) {
try {
const raw = await (0, promises_1.readFile)(dbPath(dataDir), "utf8");
const db = JSON.parse(raw);
if (!db.ideas)
db.ideas = [];
if (!db.painPoints)
db.painPoints = [];
if (!db.experiments)
db.experiments = [];
// Hydrate fields added after initial release so old records don't crash
for (const idea of db.ideas) {
idea.assumptions ??= [];
idea.mvpDefinition ??= "";
idea.validationStatus ??= "not_started";
idea.linkedPainPointIds ??= [];
idea.tags ??= [];
}
for (const pp of db.painPoints) {
pp.linkedIdeaIds ??= [];
pp.tags ??= [];
}
return db;
}
catch {
return { ideas: [], painPoints: [], experiments: [] };
}
}
async function saveDB(dataDir, db) {
await (0, promises_1.mkdir)(dataDir, { recursive: true });
await (0, promises_1.writeFile)(dbPath(dataDir), JSON.stringify(db, null, 2), "utf8");
}
function makeId() {
return crypto.randomUUID();
}
// ---------------------------------------------------------------------------
// Tools Provider
// ---------------------------------------------------------------------------
const toolsProvider = async (ctl) => {
const cfg = ctl.getPluginConfig(config_1.pluginConfigSchematics);
const dataDir = () => getDataDir(cfg.get("dataPath"));
const maxResults = () => cfg.get("maxSearchResults");
const searxng = () => cfg.get("searxngUrl").trim() || undefined;
const searchWindow = () => {
const v = cfg.get("searchRecencyWindow").trim().toLowerCase();
return (["day", "week", "month", "year"].includes(v) ? v : undefined);
};
const webSearch = (query, max, timeRange) => (0, search_1.webSearch)(query, max, 10_000, searxng(), timeRange ?? searchWindow());
const tools = [
// =========================================================================
// IDEAS
// =========================================================================
(0, sdk_1.tool)({
name: "capture_idea",
description: (0, sdk_1.text) `
Save a new idea to the idea database.
Score the idea on impact (value if it works), feasibility (how easy to build),
novelty (how unique), and effort (1 = trivial, 10 = massive).
Returns the saved idea with its generated ID.
`,
parameters: {
title: zod_1.z.string().min(3).describe("Short, memorable title for the idea"),
description: zod_1.z.string().describe("Full description of the idea"),
category: zod_1.z.string().default("general")
.describe("Category: 'product', 'feature', 'process', 'research', 'business', etc."),
tags: zod_1.z.array(zod_1.z.string()).default([]).describe("Tags for filtering and search"),
impactScore: zod_1.z.coerce.number().int().min(1).max(10).default(5)
.describe("Impact if successful (1=low, 10=transformative)"),
feasibilityScore: zod_1.z.coerce.number().int().min(1).max(10).default(5)
.describe("How feasible to build/execute (1=nearly impossible, 10=trivial)"),
noveltyScore: zod_1.z.coerce.number().int().min(1).max(10).default(5)
.describe("How unique/novel this idea is (1=common, 10=very original)"),
effortScore: zod_1.z.coerce.number().int().min(1).max(10).default(5)
.describe("Effort required (1=hours, 10=years of work)"),
notes: zod_1.z.string().default("").describe("Additional notes or context"),
},
implementation: safe_impl("capture_idea", async (params) => {
const db = await loadDB(dataDir());
const idea = {
id: makeId(),
title: params.title,
description: params.description,
category: params.category,
tags: params.tags,
status: "active",
impactScore: params.impactScore,
feasibilityScore: params.feasibilityScore,
noveltyScore: params.noveltyScore,
effortScore: params.effortScore,
linkedPainPointIds: [],
assumptions: [],
mvpDefinition: "",
validationStatus: "not_started",
notes: params.notes,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
db.ideas.push(idea);
await saveDB(dataDir(), db);
// Compute a simple priority score: (impact + feasibility + novelty - effort) / 3
const priorityScore = Math.round(((idea.impactScore + idea.feasibilityScore + idea.noveltyScore - idea.effortScore) / 3) * 10) / 10;
return json({ success: true, idea, priorityScore });
}),
}),
(0, sdk_1.tool)({
name: "list_ideas",
description: (0, sdk_1.text) `
List all captured ideas, optionally filtered by status, category, or tag.
Returns ideas sorted by priority score (impact + feasibility + novelty - effort).
`,
parameters: {
status: zod_1.z.enum(["active", "archived", "validated", "rejected", "in_progress", "all"])
.default("active").describe("Filter by status"),
category: zod_1.z.string().default("").describe("Filter by category"),
tag: zod_1.z.string().default("").describe("Filter by tag"),
search: zod_1.z.string().default("").describe("Search keyword in title/description"),
sortBy: zod_1.z.enum(["priority", "impact", "feasibility", "novelty", "effort", "createdAt"])
.default("priority").describe("Sort order"),
},
implementation: safe_impl("list_ideas", async ({ status, category, tag, search, sortBy }) => {
const db = await loadDB(dataDir());
let ideas = db.ideas;
if (status !== "all")
ideas = ideas.filter((i) => i.status === status);
if (category)
ideas = ideas.filter((i) => i.category.toLowerCase().includes(category.toLowerCase()));
if (tag)
ideas = ideas.filter((i) => i.tags.some((t) => t.toLowerCase().includes(tag.toLowerCase())));
if (search) {
const kw = search.toLowerCase();
ideas = ideas.filter((i) => i.title.toLowerCase().includes(kw) || i.description.toLowerCase().includes(kw));
}
const withScore = ideas.map((i) => ({
...i,
priorityScore: Math.round(((i.impactScore + i.feasibilityScore + i.noveltyScore - i.effortScore) / 3) * 10) / 10,
}));
withScore.sort((a, b) => {
switch (sortBy) {
case "priority": return b.priorityScore - a.priorityScore;
case "impact": return b.impactScore - a.impactScore;
case "feasibility": return b.feasibilityScore - a.feasibilityScore;
case "novelty": return b.noveltyScore - a.noveltyScore;
case "effort": return a.effortScore - b.effortScore; // low effort first
case "createdAt": return b.createdAt.localeCompare(a.createdAt);
}
});
const summary = withScore.map((i) => ({
id: i.id,
title: i.title,
category: i.category,
tags: i.tags,
status: i.status,
impact: i.impactScore,
feasibility: i.feasibilityScore,
novelty: i.noveltyScore,
effort: i.effortScore,
priorityScore: i.priorityScore,
linkedPainPoints: i.linkedPainPointIds.length,
createdAt: i.createdAt.slice(0, 10),
}));
return json({ total: summary.length, sortBy, ideas: summary });
}),
}),
(0, sdk_1.tool)({
name: "get_idea",
description: "Get full details of a single idea by ID.",
parameters: {
id: zod_1.z.string().describe("Idea ID"),
},
implementation: safe_impl("get_idea", async ({ id }) => {
const db = await loadDB(dataDir());
const idea = db.ideas.find((i) => i.id === id);
if (!idea)
throw new Error(`Idea ID '${id}' not found.`);
const linkedPainPoints = db.painPoints.filter((p) => idea.linkedPainPointIds.includes(p.id));
return json({ ...idea, linkedPainPoints });
}),
}),
(0, sdk_1.tool)({
name: "update_idea",
description: "Update fields on an existing idea by ID. Only supply fields to change.",
parameters: {
id: zod_1.z.string().describe("Idea ID"),
title: zod_1.z.string().optional(),
description: zod_1.z.string().optional(),
category: zod_1.z.string().optional(),
tags: zod_1.z.array(zod_1.z.string()).optional(),
status: zod_1.z.enum(["active", "archived", "validated", "rejected", "in_progress"]).optional(),
impactScore: zod_1.z.coerce.number().int().min(1).max(10).optional(),
feasibilityScore: zod_1.z.coerce.number().int().min(1).max(10).optional(),
noveltyScore: zod_1.z.coerce.number().int().min(1).max(10).optional(),
effortScore: zod_1.z.coerce.number().int().min(1).max(10).optional(),
validationStatus: zod_1.z.enum(["not_started", "in_progress", "validated", "invalidated"]).optional(),
mvpDefinition: zod_1.z.string().optional(),
notes: zod_1.z.string().optional(),
},
implementation: safe_impl("update_idea", async ({ id, ...updates }) => {
const db = await loadDB(dataDir());
const idx = db.ideas.findIndex((i) => i.id === id);
if (idx === -1)
throw new Error(`Idea ID '${id}' not found.`);
// Filter out undefined optional fields before merging
const patch = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
db.ideas[idx] = { ...db.ideas[idx], ...patch, updatedAt: new Date().toISOString() };
await saveDB(dataDir(), db);
return json({ success: true, idea: db.ideas[idx] });
}),
}),
(0, sdk_1.tool)({
name: "delete_idea",
description: "Permanently delete an idea by ID.",
parameters: {
id: zod_1.z.string().describe("Idea ID"),
},
implementation: safe_impl("delete_idea", async ({ id }) => {
const db = await loadDB(dataDir());
const before = db.ideas.length;
db.ideas = db.ideas.filter((i) => i.id !== id);
if (db.ideas.length === before)
throw new Error(`Idea ID '${id}' not found.`);
await saveDB(dataDir(), db);
return json({ success: true, deleted: id });
}),
}),
// =========================================================================
// PAIN POINTS
// =========================================================================
(0, sdk_1.tool)({
name: "capture_pain_point",
description: (0, sdk_1.text) `
Log a pain point, frustration, or problem you observe.
Include who is affected, how often it occurs, and how severe it is.
Returns the saved pain point with its generated ID.
`,
parameters: {
title: zod_1.z.string().min(3).describe("Short title summarizing the pain"),
description: zod_1.z.string().describe("Detailed description of the pain point"),
context: zod_1.z.string().default("").describe("Where/when this was observed"),
affectedUsers: zod_1.z.string().default("").describe("Who experiences this pain"),
frequency: zod_1.z.enum(["rare", "occasional", "frequent", "constant"]).default("occasional"),
impact: zod_1.z.enum(["low", "medium", "high", "critical"]).default("medium")
.describe("How severely this impacts affected users"),
category: zod_1.z.string().default("general")
.describe("Category: 'ux', 'workflow', 'technical', 'business', 'social', etc."),
tags: zod_1.z.array(zod_1.z.string()).default([]),
notes: zod_1.z.string().default(""),
},
implementation: safe_impl("capture_pain_point", async (params) => {
const db = await loadDB(dataDir());
const pp = {
id: makeId(),
title: params.title,
description: params.description,
context: params.context,
affectedUsers: params.affectedUsers,
frequency: params.frequency,
impact: params.impact,
category: params.category,
tags: params.tags,
status: "active",
problemStatement: "",
linkedIdeaIds: [],
notes: params.notes,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
db.painPoints.push(pp);
await saveDB(dataDir(), db);
return json({ success: true, painPoint: pp });
}),
}),
(0, sdk_1.tool)({
name: "list_pain_points",
description: (0, sdk_1.text) `
List all captured pain points, optionally filtered by status, category, impact, or tag.
Sorted by impact severity (critical first) by default.
`,
parameters: {
status: zod_1.z.enum(["active", "archived", "validated", "rejected", "in_progress", "all"])
.default("active"),
category: zod_1.z.string().default(""),
impact: zod_1.z.enum(["low", "medium", "high", "critical", "all"]).default("all"),
tag: zod_1.z.string().default(""),
search: zod_1.z.string().default(""),
},
implementation: safe_impl("list_pain_points", async ({ status, category, impact, tag, search }) => {
const db = await loadDB(dataDir());
let pps = db.painPoints;
if (status !== "all")
pps = pps.filter((p) => p.status === status);
if (category)
pps = pps.filter((p) => p.category.toLowerCase().includes(category.toLowerCase()));
if (impact !== "all")
pps = pps.filter((p) => p.impact === impact);
if (tag)
pps = pps.filter((p) => p.tags.some((t) => t.toLowerCase().includes(tag.toLowerCase())));
if (search) {
const kw = search.toLowerCase();
pps = pps.filter((p) => p.title.toLowerCase().includes(kw) || p.description.toLowerCase().includes(kw));
}
const impactOrder = { critical: 4, high: 3, medium: 2, low: 1 };
pps = pps.slice().sort((a, b) => (impactOrder[b.impact] ?? 0) - (impactOrder[a.impact] ?? 0));
const summary = pps.map((p) => ({
id: p.id,
title: p.title,
category: p.category,
impact: p.impact,
frequency: p.frequency,
affectedUsers: p.affectedUsers,
tags: p.tags,
status: p.status,
hasProblemStatement: !!p.problemStatement,
linkedIdeas: p.linkedIdeaIds.length,
createdAt: p.createdAt.slice(0, 10),
}));
return json({ total: summary.length, painPoints: summary });
}),
}),
(0, sdk_1.tool)({
name: "get_pain_point",
description: "Get full details of a single pain point by ID.",
parameters: {
id: zod_1.z.string().describe("Pain point ID"),
},
implementation: safe_impl("get_pain_point", async ({ id }) => {
const db = await loadDB(dataDir());
const pp = db.painPoints.find((p) => p.id === id);
if (!pp)
throw new Error(`Pain point ID '${id}' not found.`);
const linkedIdeas = db.ideas.filter((i) => pp.linkedIdeaIds.includes(i.id));
return json({ ...pp, linkedIdeas });
}),
}),
(0, sdk_1.tool)({
name: "update_pain_point",
description: "Update fields on an existing pain point by ID. Only supply fields to change.",
parameters: {
id: zod_1.z.string().describe("Pain point ID"),
title: zod_1.z.string().optional(),
description: zod_1.z.string().optional(),
context: zod_1.z.string().optional(),
affectedUsers: zod_1.z.string().optional(),
frequency: zod_1.z.enum(["rare", "occasional", "frequent", "constant"]).optional(),
impact: zod_1.z.enum(["low", "medium", "high", "critical"]).optional(),
category: zod_1.z.string().optional(),
tags: zod_1.z.array(zod_1.z.string()).optional(),
status: zod_1.z.enum(["active", "archived", "validated", "rejected", "in_progress"]).optional(),
problemStatement: zod_1.z.string().optional()
.describe("Store a generated problem statement here"),
notes: zod_1.z.string().optional(),
},
implementation: safe_impl("update_pain_point", async ({ id, ...updates }) => {
const db = await loadDB(dataDir());
const idx = db.painPoints.findIndex((p) => p.id === id);
if (idx === -1)
throw new Error(`Pain point ID '${id}' not found.`);
const patch = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
db.painPoints[idx] = { ...db.painPoints[idx], ...patch, updatedAt: new Date().toISOString() };
await saveDB(dataDir(), db);
return json({ success: true, painPoint: db.painPoints[idx] });
}),
}),
// =========================================================================
// ANALYSIS & GENERATION
// =========================================================================
(0, sdk_1.tool)({
name: "generate_problem_statement",
description: (0, sdk_1.text) `
Generate a structured, formal problem statement from a pain point description.
Uses the "Who / What / Why / Impact" framework and the Jobs-to-be-Done lens.
After generating, call update_pain_point with the problemStatement field to save it.
`,
parameters: {
painPointId: zod_1.z.string().default("")
.describe("Pain point ID to load from database (leave blank to use inline text)"),
title: zod_1.z.string().default("").describe("Pain point title (if not using ID)"),
description: zod_1.z.string().default("").describe("Pain point description (if not using ID)"),
affectedUsers: zod_1.z.string().default("").describe("Who is affected"),
context: zod_1.z.string().default("").describe("Context where this pain occurs"),
impact: zod_1.z.string().default("").describe("Impact severity or business cost"),
},
implementation: safe_impl("generate_problem_statement", async (params) => {
let title = params.title;
let description = params.description;
let affectedUsers = params.affectedUsers;
let context = params.context;
let impact = params.impact;
if (params.painPointId) {
const db = await loadDB(dataDir());
const pp = db.painPoints.find((p) => p.id === params.painPointId);
if (!pp)
throw new Error(`Pain point ID '${params.painPointId}' not found.`);
title = title || pp.title;
description = description || pp.description;
affectedUsers = affectedUsers || pp.affectedUsers;
context = context || pp.context;
impact = impact || pp.impact;
}
const payload = {
title,
description,
affectedUsers,
context,
impact,
painPointId: params.painPointId || null,
instructions: "Generate a crisp, structured problem statement using this framework:\n" +
"**Who**: [specific user segment]\n" +
"**What**: [the core problem they face — not the symptom, the root cause]\n" +
"**When/Where**: [the context/trigger for the problem]\n" +
"**Why it matters**: [quantified or qualified impact]\n" +
"**Current workarounds**: [how they cope today and why it's insufficient]\n" +
"**Success criterion**: [what 'solved' looks like]\n\n" +
"Then write a single-sentence problem statement suitable for a product brief. " +
"Keep the whole output under 200 words.",
};
return json(payload);
}),
}),
(0, sdk_1.tool)({
name: "evaluate_idea",
description: (0, sdk_1.text) `
Deeply evaluate an idea across multiple dimensions:
market potential, technical feasibility, differentiation, risks, and time-to-value.
Produces a scorecard and go/no-go recommendation.
`,
parameters: {
ideaId: zod_1.z.string().default("")
.describe("Idea ID to load from database (leave blank to use inline text)"),
title: zod_1.z.string().default("").describe("Idea title (if not using ID)"),
description: zod_1.z.string().default("").describe("Idea description (if not using ID)"),
targetMarket: zod_1.z.string().default("").describe("Who would use/buy this"),
competitorContext: zod_1.z.string().default("")
.describe("Known competitors or existing solutions"),
},
implementation: safe_impl("evaluate_idea", async (params) => {
let title = params.title;
let description = params.description;
if (params.ideaId) {
const db = await loadDB(dataDir());
const idea = db.ideas.find((i) => i.id === params.ideaId);
if (!idea)
throw new Error(`Idea ID '${params.ideaId}' not found.`);
title = title || idea.title;
description = description || idea.description;
}
return json({
title,
description,
targetMarket: params.targetMarket,
competitorContext: params.competitorContext,
instructions: "Evaluate this idea across these dimensions (score each 1–10 with explicit rationale — do not assign scores without reasoning):\n" +
"1. **Market Size**: What evidence supports the market size claim? Note if unknown.\n" +
"2. **Problem Severity**: How painful is the problem — and for whom specifically? Do not generalize.\n" +
"3. **Differentiation**: What actually exists today that competes? Be specific — do not assume the space is open.\n" +
"4. **Technical Feasibility**: What are the real technical risks, not just the optimistic case?\n" +
"5. **Time to Value**: How quickly can users see value — and what must go right for that to happen?\n" +
"6. **Moat/Defensibility**: What would stop a well-funded competitor from copying this in 6 months?\n\n" +
"Then list:\n" +
"- Top 3 risks — include risks that could kill the idea entirely, not just manageable ones\n" +
"- Top 3 assumptions that must be validated before spending significant time or money\n" +
"- Recommended next step — base this on the weakest dimension above, not the strongest\n" +
"- Assessment: present arguments FOR and AGAINST proceeding; let the user form their own conclusion. " +
" Do not issue a single go/no-go verdict as if it were objective — it is not.",
});
}),
}),
(0, sdk_1.tool)({
name: "link_pain_to_idea",
description: (0, sdk_1.text) `
Create a bidirectional link between a pain point and an idea.
Useful for mapping which ideas address which pain points.
`,
parameters: {
painPointId: zod_1.z.string().describe("Pain point ID"),
ideaId: zod_1.z.string().describe("Idea ID"),
},
implementation: safe_impl("link_pain_to_idea", async ({ painPointId, ideaId }) => {
const db = await loadDB(dataDir());
const ppIdx = db.painPoints.findIndex((p) => p.id === painPointId);
const ideaIdx = db.ideas.findIndex((i) => i.id === ideaId);
if (ppIdx === -1)
throw new Error(`Pain point ID '${painPointId}' not found.`);
if (ideaIdx === -1)
throw new Error(`Idea ID '${ideaId}' not found.`);
const pp = db.painPoints[ppIdx];
const idea = db.ideas[ideaIdx];
if (!pp.linkedIdeaIds.includes(ideaId)) {
pp.linkedIdeaIds.push(ideaId);
pp.updatedAt = new Date().toISOString();
}
if (!idea.linkedPainPointIds.includes(painPointId)) {
idea.linkedPainPointIds.push(painPointId);
idea.updatedAt = new Date().toISOString();
}
await saveDB(dataDir(), db);
return json({
success: true,
painPoint: { id: pp.id, title: pp.title, linkedIdeas: pp.linkedIdeaIds },
idea: { id: idea.id, title: idea.title, linkedPainPoints: idea.linkedPainPointIds },
});
}),
}),
(0, sdk_1.tool)({
name: "generate_solution_brief",
description: (0, sdk_1.text) `
Generate a concise solution brief (mini product spec) from a pain point + idea pair.
Covers the problem, proposed solution, target users, key features, success metrics,
and a recommended MVP scope.
`,
parameters: {
painPointId: zod_1.z.string().default("").describe("Pain point ID (optional)"),
ideaId: zod_1.z.string().default("").describe("Idea ID (optional)"),
problemDescription: zod_1.z.string().default("").describe("Problem description (if not using IDs)"),
solutionDescription: zod_1.z.string().default("").describe("Solution description (if not using IDs)"),
targetUsers: zod_1.z.string().default("").describe("Target users or customer segment"),
},
implementation: safe_impl("generate_solution_brief", async (params) => {
const db = await loadDB(dataDir());
let problem = params.problemDescription;
let solution = params.solutionDescription;
let target = params.targetUsers;
if (params.painPointId) {
const pp = db.painPoints.find((p) => p.id === params.painPointId);
if (pp) {
problem = problem || `${pp.title}: ${pp.description}`;
target = target || pp.affectedUsers;
}
}
if (params.ideaId) {
const idea = db.ideas.find((i) => i.id === params.ideaId);
if (idea) {
solution = solution || `${idea.title}: ${idea.description}`;
}
}
return json({
problem,
solution,
targetUsers: target,
instructions: "Write a solution brief with these sections:\n" +
"## Problem\n[1-2 sentence problem statement]\n\n" +
"## Proposed Solution\n[Clear description of what you're building]\n\n" +
"## Target Users\n[Specific user persona]\n\n" +
"## Core Features (MVP)\n[3–5 bullet points — must-haves only]\n\n" +
"## Success Metrics\n[2–3 measurable KPIs]\n\n" +
"## What We're NOT Building\n[Explicit scope exclusions]\n\n" +
"## Open Questions\n[Top 3 unknowns to resolve]\n\n" +
"Keep it under 400 words. Be specific and opinionated.",
});
}),
}),
(0, sdk_1.tool)({
name: "brainstorm_solutions",
description: (0, sdk_1.text) `
Generate a diverse set of solution ideas for a given pain point or problem.
Produces ideas across different approaches: technology, process, business model,
community, and unconventional angles.
`,
parameters: {
painPointId: zod_1.z.string().default("").describe("Pain point ID to load context from"),
problem: zod_1.z.string().default("").describe("Problem description (if not using ID)"),
constraints: zod_1.z.string().default("")
.describe("Any constraints: budget, tech stack, timeline, team size"),
approachCount: zod_1.z.coerce.number().int().min(3).max(15).default(6)
.describe("Number of solution ideas to generate"),
},
implementation: safe_impl("brainstorm_solutions", async (params) => {
let problem = params.problem;
if (params.painPointId) {
const db = await loadDB(dataDir());
const pp = db.painPoints.find((p) => p.id === params.painPointId);
if (pp)
problem = problem || `${pp.title}: ${pp.description}`;
}
if (!problem)
throw new Error("Provide either a painPointId or problem description.");
return json({
problem,
constraints: params.constraints,
approachCount: params.approachCount,
instructions: `Generate ${params.approachCount} distinct solution ideas for the problem above. ` +
"For each idea:\n" +
"- **Idea**: One-line description\n" +
"- **Approach type**: (SaaS / automation / platform / community / hardware / process / etc.)\n" +
"- **Core insight**: The key insight that makes this work\n" +
"- **Pros**: 2-3 bullet points\n" +
"- **Cons**: 1-2 bullet points\n" +
"- **Effort estimate**: (hours / days / weeks / months)\n\n" +
"Include at least one unconventional or contrarian idea. Be specific, not generic.",
});
}),
}),
// =========================================================================
// RESEARCH
// =========================================================================
(0, sdk_1.tool)({
name: "search_similar_problems",
description: (0, sdk_1.text) `
Search the web for similar problems, existing solutions, competitors, and market research
related to a pain point or idea. Returns relevant links and snippets.
`,
parameters: {
query: zod_1.z.string().describe("Problem or idea to research"),
focus: zod_1.z.enum(["competitors", "solutions", "research", "discussions", "general"])
.default("general").describe("What to look for"),
max: zod_1.z.coerce.number().int().min(3).max(20).optional(),
},
implementation: safe_impl("search_similar_problems", async ({ query, focus, max }) => {
const limit = max ?? maxResults();
const suffixMap = {
competitors: "competitor product solution",
solutions: "how to solve solution tool",
research: "research study statistics",
discussions: "reddit hackernews discussion forum",
general: "",
};
const fullQuery = `${query} ${suffixMap[focus] ?? ""}`.trim();
const hits = await webSearch(fullQuery, limit, searchWindow());
return json({ query: fullQuery, focus, results: hits });
}),
}),
// =========================================================================
// EXPORT
// =========================================================================
(0, sdk_1.tool)({
name: "export_report",
description: (0, sdk_1.text) `
Export a formatted Markdown report of all ideas and pain points.
Includes summaries, scores, links, and statistics.
Returns the file path where the report was saved.
`,
parameters: {
outputPath: zod_1.z.string().default("")
.describe("File path to save the report. Defaults to <dataPath>/report-<date>.md"),
includeArchived: zod_1.z.coerce.boolean().default(false)
.describe("Include archived/rejected items"),
},
implementation: safe_impl("export_report", async ({ outputPath, includeArchived }) => {
const db = await loadDB(dataDir());
const date = new Date().toISOString().slice(0, 10);
const outPath = outputPath.trim() || (0, path_1.join)(dataDir(), `ideas-report-${date}.md`);
const filterStatus = (status) => includeArchived || (status !== "archived" && status !== "rejected");
const ideas = db.ideas.filter((i) => filterStatus(i.status));
const pps = db.painPoints.filter((p) => filterStatus(p.status));
let md = `# Ideas & Pain Points Report — ${date}\n\n`;
// Stats
md += `## Summary\n\n`;
md += `| Metric | Count |\n|--------|-------|\n`;
md += `| Total Ideas | ${db.ideas.length} |\n`;
md += `| Total Pain Points | ${db.painPoints.length} |\n`;
md += `| Active Ideas | ${db.ideas.filter((i) => i.status === "active").length} |\n`;
md += `| Validated Ideas | ${db.ideas.filter((i) => i.status === "validated").length} |\n`;
md += `| Critical Pain Points | ${db.painPoints.filter((p) => p.impact === "critical").length} |\n`;
md += `\n---\n\n`;
// Top ideas by priority
const topIdeas = ideas
.map((i) => ({ ...i, priority: (i.impactScore + i.feasibilityScore + i.noveltyScore - i.effortScore) / 3 }))
.sort((a, b) => b.priority - a.priority)
.slice(0, 10);
md += `## Top Ideas by Priority\n\n`;
for (const i of topIdeas) {
md += `### ${i.title} *(${i.status})*\n`;
md += `**Category**: ${i.category} | **Priority Score**: ${i.priority.toFixed(1)} \n`;
md += `**Scores**: Impact ${i.impactScore}/10 · Feasibility ${i.feasibilityScore}/10 · Novelty ${i.noveltyScore}/10 · Effort ${i.effortScore}/10 \n`;
if (i.tags.length > 0)
md += `**Tags**: ${i.tags.join(", ")} \n`;
md += `\n${i.description}\n\n`;
if (i.notes)
md += `> **Notes**: ${i.notes}\n\n`;
if (i.linkedPainPointIds.length > 0) {
const linked = db.painPoints.filter((p) => i.linkedPainPointIds.includes(p.id));
md += `**Addresses pain points**: ${linked.map((p) => p.title).join(", ")} \n`;
}
md += `\n`;
}
// Pain points by impact
const sortedPPs = pps.slice().sort((a, b) => {
const order = { critical: 4, high: 3, medium: 2, low: 1 };
return (order[b.impact] ?? 0) - (order[a.impact] ?? 0);
});
md += `---\n\n## Pain Points\n\n`;
for (const p of sortedPPs) {
md += `### ${p.title} *(${p.impact} impact · ${p.frequency})*\n`;
md += `**Category**: ${p.category} | **Status**: ${p.status} \n`;
if (p.affectedUsers)
md += `**Affected users**: ${p.affectedUsers} \n`;
if (p.context)
md += `**Context**: ${p.context} \n`;
if (p.tags.length > 0)
md += `**Tags**: ${p.tags.join(", ")} \n`;
md += `\n${p.description}\n\n`;
if (p.problemStatement)
md += `**Problem Statement**: ${p.problemStatement}\n\n`;
if (p.linkedIdeaIds.length > 0) {
const linked = db.ideas.filter((i) => p.linkedIdeaIds.includes(i.id));
md += `**Potential solutions**: ${linked.map((i) => i.title).join(", ")} \n`;
}
md += `\n`;
}
await (0, promises_1.mkdir)(dataDir(), { recursive: true });
await (0, promises_1.writeFile)(outPath, md, "utf8");
return json({
success: true,
path: outPath,
ideasIncluded: topIdeas.length,
painPointsIncluded: sortedPPs.length,
});
}),
}),
// =========================================================================
// RESEARCH — MARKET SIZE
// =========================================================================
(0, sdk_1.tool)({
name: "search_market_size",
description: (0, sdk_1.text) `
Search for market size data (TAM/SAM/SOM), growth rate, and industry reports
for an idea or problem space. Returns relevant statistics, reports, and sources.
Also estimates rough TAM from first-principles if search results are thin.
`,
parameters: {
ideaOrMarket: zod_1.z.string().describe("The idea, product category, or market to research"),
region: zod_1.z.string().default("global").describe("Geographic region (global, US, Europe, India, etc.)"),
},
implementation: safe_impl("search_market_size", async ({ ideaOrMarket, region }) => {
const yr = new Date().getFullYear();
const queries = [
`${ideaOrMarket} market size TAM ${region} ${yr} billion`,
`${ideaOrMarket} industry report growth rate CAGR ${yr} ${yr + 1}`,
`site:statista.com OR site:grandviewresearch.com OR site:mordorintelligence.com ${ideaOrMarket} market ${yr}`,
];
const allResults = [];
for (const q of queries) {
try {
const hits = await webSearch(q, 4, searchWindow());
allResults.push(...hits);
}
catch { /* continue */ }
}
// Deduplicate by URL
const seen = new Set();
const deduped = allResults.filter((r) => {
if (seen.has(r.url))
return false;
seen.add(r.url);
return true;
});
return json({
market: ideaOrMarket,
region,
searchResults: deduped.slice(0, maxResults()),
tamFramework: {
topDown: "Find a published industry report (Statista, Grand View Research, IBISWorld) and filter to your specific segment.",
bottomUp: "Count your target customers × annual spend per customer = TAM. E.g., '10M small businesses × $500/yr = $5B TAM'.",
valueTheory: "What % of value you create could you reasonably capture as revenue? Work backwards from that.",
},
instructions: "Using the search results above, present market size data as found — do not fabricate figures not in the results. " +
"Include: (1) TAM — cite the specific report or source for each figure; note publication date, " +
"(2) SAM — explain the segmentation logic and what assumptions it requires, " +
"(3) SOM — present the range of realistic estimates, not a single optimistic number, " +
"(4) Market dynamics — list both tailwinds AND headwinds from the data; do not omit negative signals, " +
"(5) Data quality assessment — flag if market size figures are contested, old, or from a single source. " +
"Do not conclude whether the user 'should' build — present the data and let them decide.",
});
}),
}),
(0, sdk_1.tool)({
name: "search_competitors",
description: (0, sdk_1.text) `
Search for existing products, startups, and solutions that already address the same problem as your idea.
Distinct from search_similar_problems (finds the pain) and search_market_size (finds TAM).
This finds WHO is already solving it and HOW — essential before investing time building.
Returns a list of competitors with positioning, pricing signals, and gaps you could exploit.
`,
parameters: {
idea: zod_1.z.string().min(10).describe("Your idea or the problem it solves (1–3 sentences)"),
category: zod_1.z.string().default("").describe("Optional category or industry (e.g. 'B2B SaaS', 'consumer app', 'dev tool')"),
max: zod_1.z.coerce.number().int().min(3).max(20).default(8)
.describe("Max results per search angle"),
},
implementation: safe_impl("search_competitors", async ({ idea, category, max }) => {
const cat = category ? ` ${category}` : "";
const searches = [
{ angle: "direct_competitors", query: `${idea}${cat} alternatives competitors` },
{ angle: "existing_products", query: `${idea}${cat} best tools software app` },
{ angle: "startup_landscape", query: `${idea}${cat} startup crunchbase wellfound product hunt` },
{ angle: "open_source", query: `${idea}${cat} open source github solution` },
];
const results = {};
for (const s of searches) {
results[s.angle] = await webSearch(s.query, max, searchWindow());
}
return json({
idea,
category: category || "general",
competitorSearch: results,
instructions: "Using the search results above, produce a neutral competitive landscape analysis — do not assume gaps exist before looking for them. " +
"(1) List all significant competitors found: name, what they do, pricing model, and both strengths AND weaknesses (not just weaknesses). " +
"(2) List any open-source alternatives found in the results. " +
"(3) Gaps — only report gaps that are genuinely unaddressed based on the evidence; do not invent a gap to make the idea look viable. " +
" If no clear gap exists, say so explicitly. " +
"(4) Differentiation — only if a credible angle exists based on the data; do not generate differentiation angles not supported by the research. " +
"(5) Market saturation assessment: cite the evidence (funding, product count, review volume) for your assessment. " +
"If 10 well-funded competitors exist, say so clearly. Do not soften this to encourage the user.",
});
}),
}),
// =========================================================================
// VALIDATION
// =========================================================================
(0, sdk_1.tool)({
name: "map_assumptions",
description: (0, sdk_1.text) `
Identify and prioritize all the critical assumptions that must be true for
an idea to succeed. Organizes them into four risk categories:
- Desirability: Do people want this?
- Feasibility: Can we build it?
- Viability: Can we make money?
- Usability: Can people use it effectively?
Returns a prioritized assumption map with suggested validation experiments for each.
Use design_experiment to create experiments that test individual assumptions.
`,
parameters: {
ideaId: zod_1.z.string().default("").describe("Idea ID to load and update"),
title: zod_1.z.string().default("").describe("Idea title (if not using ID)"),
description: zod_1.z.string().default("").describe("Idea description (if not using ID)"),
targetUsers: zod_1.z.string().default("").describe("Who the idea is for"),
revenueModel: zod_1.z.string().default("").describe("How the idea makes money"),
},
implementation: safe_impl("map_assumptions", async (params) => {
let title = params.title;
let description = params.description;
if (params.ideaId) {
const db = await loadDB(dataDir());
const idea = db.ideas.find((i) => i.id === params.ideaId);
if (!idea)
throw new Error(`Idea ID '${params.ideaId}' not found.`);
title = title || idea.title;
description = description || idea.description;
}
return json({
title,
description,
targetUsers: params.targetUsers,
revenueModel: params.revenueModel,
assumptionCategories: {
desirability: [
"Users have the problem we think they have",
"The problem is painful enough that they actively seek solutions",
"Users prefer our approach over existing workarounds",
"There are enough users with this problem to build a business",
],
feasibility: [
"The core technical challenge is solvable with our current skills",
"We can build a usable version within the available time/budget",
"Key dependencies (APIs, data, infrastructure) are accessible",
"We can hire the talent needed to build and maintain this",
],
viability: [
"Users will pay the price we need to charge for the business to work",
"Customer acquisition cost (CAC) will be lower than lifetime value (LTV)",
"We can reach customers through affordable channels",
"The business can survive until it reaches profitability",
],
usability: [
"Users can understand what the product does within the first 60 seconds",
"The core workflow requires no support or documentation",
"Users can achieve their goal successfully on their first attempt",
],
},
instructions: `For the idea "${title}", generate a prioritized list of 8–12 critical assumptions. ` +
"For each assumption:\n" +
"- **Statement**: 'We believe that [specific assumption]'\n" +
"- **Category**: desirability / feasibility / viability / usability\n" +
"- **Risk**: critical / high / medium / low (how bad if wrong)\n" +
"- **Current confidence**: 0–100% (honest gut estimate)\n" +
"- **Cheapest test**: what is the fastest, cheapest way to test this assumption?\n\n" +
"Sort by: (risk × inverse_confidence) — test the highest-risk, lowest-confidence assumptions first.\n" +
"Be brutally honest about what could kill this idea.",
});
}),
}),
(0, sdk_1.tool)({
name: "design_experiment",
description: (0, sdk_1.text) `
Design a lean validation experiment to test a specific assumption.
Selects the right experiment type for the assumption and produces a ready-to-run plan.
Experiment types available:
- customer_interview: 1:1 calls to validate problem/desirability
- landing_page: Build a page and measure sign-up rate before building
- fake_door: Show a feature/button to measure click-through before building
- smoke_test: Email campaign to gauge interest
- concierge_mvp: Manually deliver the service to first users
- wizard_of_oz: Appear automated but manually do the work behind the scenes
- survey: Quantitative validation across many users
- prototype: Low-fidelity clickable mockup for usability testing
- a_b_test: Compare two versions of a message/offer/feature
- cold_outreach: Direct outreach to measure response rate
Saves the experiment to the database linked to the idea.
`,
parameters: {
ideaId: zod_1.z.string().describe("Idea ID this experiment belongs to"),
assumption: zod_1.z.string().describe("The specific assumption being tested"),
experimentType: zod_1.z.enum([
"customer_interview", "landing_page", "fake_door", "smoke_test",
"concierge_mvp", "wizard_of_oz", "survey", "prototype",
"a_b_test", "cold_outreach", "other",
]).describe("Type of experiment to design"),
targetUsers: zod_1.z.string().default("").describe("Who you'll run this experiment with"),
budget: zod_1.z.string().default("$0").describe("Available budget for this experiment"),
timeAvailable: zod_1.z.enum(["hours", "days", "weeks"]).default("days")
.describe("How much time you can spend on this experiment"),
},
implementation: safe_impl("design_experiment", async (params) => {
const db = await loadDB(dataDir());
const idea = db.ideas.find((i) => i.id === params.ideaId);
if (!idea)
throw new Error(`Idea ID '${params.ideaId}' not found.`);
const experimentTemplates = {
customer_interview: {
method: "Recruit 5–10 people who match your target user profile via LinkedIn, Reddit, or your network. Conduct 20–30 min structured interviews. Do NOT pitch — only listen and ask open-ended questions about their current behavior.",
successCriteria: "≥7/10 interviewees confirm the problem is real and painful (rate it ≥7/10 severity). At least 3 describe a current workaround they actively use.",
},
landing_page: {
method: "Build a single-page site in Carrd, Webflow, or Framer (1–2 hours). Describe the product clearly. Add an email capture CTA. Drive 100–200 visitors via Reddit, Twitter, or small paid spend. Measure sign-up rate.",
successCriteria: "≥5% of visitors submit their email. At least 20 total sign-ups. Optional: ≥3 people reply when you send a follow-up email.",
},
fake_door: {
method: "Add a button/link for the feature in an existing product or page. When clicked, show 'Coming soon — sign up for early access'. Track click-through rate vs. page views.",
successCriteria: "≥3–5% click-through rate on the fake door button indicates genuine interest.",
},
smoke_test: {
method: "Write a cold email or social post clearly describing the product and its value prop. Send to 50–100 targeted people or post in 3–5 relevant communities. Track reply/click rate.",
successCriteria: "≥10% positive reply rate. At least 5 people ask 'how do I get access?'",
},
concierge_mvp: {
method: "Manually deliver the core value to 3–5 early users using existing tools (spreadsheets, email, calls). Don't build anything yet. Charge them if possible — even a nominal fee validates willingness to pay.",
successCriteria: "Users complete at least one full cycle of the service. At least 2/3 would pay for it. You identify the 1–2 most important features to automate first.",
},
wizard_of_oz: {
method: "Build a front-end UI that looks automated but you manually do the work behind the scenes. Users believe the system is working. Run 5–10 users through the full flow.",
successCriteria: "Users complete their goal successfully without asking for help. Satisfaction score ≥8/10. You discover which steps are most valuable to actually automate.",
},
survey: {
method: "Write a 5–10 question survey using Typeform or Google Forms. Distribute via your network, Reddit, or Twitter. Ask about the problem, current behavior, and willingness to pay. Aim for 50+ responses.",
successCriteria: "≥60% of respondents rate the problem as high/critical severity. ≥30% express intent to use the solution. Willingness to pay > your target price point.",
},
prototype: {
method: "Build a clickable prototype in Figma or Framer (no code). Recruit 5 target users for 30-min usability sessions. Observe them completing the core task — do not guide them.",
successCriteria: "≥4/5 users complete the core task without getting stuck. No critical usability blockers. Users can articulate what the product does unprompted.",
},
a_b_test: {
method: "Create two versions of a message/offer/CTA. Split your audience 50/50 and track conversion to the desired action (sign-up, reply, purchase). Run until statistical significance.",
successCriteria: "One variant shows ≥20% higher conversion rate with ≥95% statistical confidence.",
},
cold_outreach: {
method: "Send 50–100 personalized cold emails/DMs to people who match your target user profile. Offer them a free early access or 15-min call. Track open rate, reply rate, and meeting booked rate.",
successCriteria: "≥30% open rate, ≥10% reply rate, ≥5% meeting booked. At least 3 people say 'yes I have this problem'.",
},
other: {
method: "Design a custom experiment based on the assumption being tested.",
successCriteria: "Define a measurable metric that would indicate the assumption is valid.",
},
};
const template = experimentTemplates[params.experimentType] ?? experimentTemplates.other;
const exp = {
id: makeId(),
ideaId: params.ideaId,
title: `${params.experimentType.replace(/_/g, " ")} — ${params.assumption.slice(0, 50)}`,
type: params.experimentType,
hypothesis: `We believe that ${params.assumption}. We will know this is true when [success criteria is met].`,
method: template.method,
successCriteria: template.successCriteria,
effort: params.timeAvailable,
cost: params.budget,
result: "pending",
evidence: "",
learnings: "",
testedAssumptionIds: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
if (!db.experiments)
db.experiments = [];
db.experiments.push(exp);
// Update idea validation status
const ideaIdx = db.ideas.findIndex((i) => i.id === params.ideaId);
if (ideaIdx !== -1 && db.ideas[ideaIdx].validationStatus === "not_started") {
db.ideas[ideaIdx].validationStatus = "in_progress";
db.ideas[ideaIdx].updatedAt = new Date().toISOString();
}
await saveDB(dataDir(), db);
return json({
success: true,
experiment: exp,
ideaTitle: idea.title,
targetUsers: params.targetUsers,
instructions: `Design a complete, ready-to-run ${params.experimentType.replace(/_/g, " ")} experiment to test: "${params.assumption}". ` +
`Target users: ${params.targetUsers || "your target audience"}. ` +
`Budget: ${params.budget}. Time: ${params.timeAvailable}. ` +
"Produce: " +
"(1) Exact hypothesis in 'We believe X, we'll know it's true when Y' format, " +
"(2) Step-by-step execution plan (who to recruit, what tools, exact script/questions), " +
"(3) Specific success/failure criteria with numbers, " +
"(4) What to do if the experiment succeeds vs. fails, " +
"(5) Biggest risk that could make this experiment misleading.",
});
}),
}),
(0, sdk_1.tool)({
name: "log_experiment_result",
description: (0, sdk_1.text) `
Record the outcome of a validation experiment after running it.
Updates the experiment with evidence, learnings, and result verdict.
Also updates the parent idea's validation status based on all experiment results.
`,
parameters: {
experimentId: zod_1.z.string().describe("Experiment ID to update"),
result: zod_1.z.enum(["validated", "invalidated", "inconclusive"])
.describe("Did the experiment support the hypothesis?"),
evidence: zod_1.z.string().describe("What you actually observed — raw data, quotes, numbers"),
learnings: zod_1.z.string().default("").describe("What you learned and what to do next"),
confidenceChange: zod_1.z.coerce.number().int().min(-100).max(100).default(0)
.describe("How much did this change your confidence in the idea? (-100 to +100)"),
},
implementation: safe_impl("log_experiment_result", async ({ experimentId, result, evidence, learnings, confidenceChange }) => {
const db = await loadDB(dataDir());
if (!db.experiments)
db.experiments = [];
const expIdx = db.experiments.findIndex((e) => e.id === experimentId);
if (expIdx === -1)
throw new Error(`Experiment ID '${experimentId}' not found.`);
db.experiments[expIdx].result = result;
db.experiments[expIdx].evidence = evidence;
db.experiments[expIdx].learnings = learnings;
db.experiments[expIdx].updatedAt = new Date().toISOString();
// Recalculate idea validation status from all experiments
const ideaId = db.experiments[expIdx].ideaId;
const ideaExperiments = db.experiments.filter((e) => e.ideaId === ideaId);
const validated = ideaExperiments.filter((e) => e.result === "validated").length;
const invalidated = ideaExperiments.filter((e) => e.result === "invalidated").length;
const total = ideaExperiments.filter((e) => e.result !== "pending").length;
const ideaIdx = db.ideas.findIndex((i) => i.id === ideaId);
if (ideaIdx !== -1) {
if (total > 0) {
// Invalidated if failures outnumber or tie successes (ties = not proven)
if (invalidated > 0 && invalidated >= validated)
db.ideas[ideaIdx].validationStatus = "invalidated";
// Validated requires at least 2 passing experiments and majority passing
else if (validated >= 2 && validated > invalidated)
db.ideas[ideaIdx].validationStatus = "validated";
else
db.ideas[ideaIdx].validationStatus = "in_progress";
}
db.ideas[ideaIdx].updatedAt = new Date().toISOString();
}
await saveDB(dataDir(), db);
const ideaTitle = ideaIdx !== -1 ? db.ideas[ideaIdx].title : "Unknown";
const summary = {
totalExperiments: ideaExperiments.length,
validated,
invalidated,
pending: ideaExperiments.filter((e) => e.result === "pending").length,
ideaValidationStatus: ideaIdx !== -1 ? db.ideas[ideaIdx].validationStatus : "unknown",
};
return json({
success: true,
experiment: db.experiments[expIdx],
ideaTitle,
confidenceChange,
validationSummary: summary,
nextStepSuggestion: result === "validated"
? "Assumption confirmed. Move to the next highest-risk assumption or start building an MVP." :
result === "invalidated"
? "Assumption failed. Decide: pivot the idea, drop this assumption (reframe the problem), or abandon." :
"Inconclusive. Redesign the experiment with clearer success criteria or a larger sample.",
});
}),
}),
(0, sdk_1.tool)({
name: "generate_validation_questions",
description: (0, sdk_1.text) `
Generate a structured set of customer discovery questions for validating
an idea through interviews or surveys.
Follows the Mom Test principles: ask about past behavior, not future intent.
Returns questions organized by goal: problem validation, solution validation,
willingness to pay, and switching behavior.
`,
parameters: {
ideaId: zod_1.z.string().default("").describe("Idea ID to load context from"),
ideaDescription: zod_1.z.string().default("").describe("Idea description (if not using ID)"),
targetUser: zod_1.z.string().default("").describe("Who you're interviewing"),
validationGoal: zod_1.z.enum(["problem", "solution", "willingness_to_pay", "full_discovery"])
.default("full_discovery").describe("What aspect to focus on"),
format: zod_1.z.enum(["interview", "survey"]).default("interview")
.describe("Will these be asked in a call or a written survey?"),
},
implementation: safe_impl("generate_validation_questions", async (params) => {
let description = params.ideaDescription;
if (params.ideaId) {
const db = await loadDB(dataDir());
const idea = db.ideas.find((i) => i.id === params.ideaId);
if (idea)
description = description || idea.description;
}
return json({
ideaDescription: description,
targetUser: params.targetUser || "target user",
validationGoal: params.validationGoal,
format: params.format,
momTestPrinciples: [
"Ask about the PAST, not the future ('Have you ever...' not 'Would you...')",
"Ask about SPECIFICS, not generalities ('Tell me about the last time...')",
"Never pitch or lead the witness ('Do you think X would help?' is a bad question)",
"Dig into workarounds — what do they do TODAY instead?",
"Silence is data — let them fill the gap",
],
instructions: `Generate 12–15 ${params.format} questions to validate an idea: "${description}". ` +
`Target user: ${params.targetUser || "your target user"}. ` +
`Goal: ${params.validationGoal.replace(/_/g, " ")}. ` +
`Format: ${params.format}. ` +
"Organize questions into sections:\n" +
"**Warm-up** (2–3 questions): understand their role and context\n" +
"**Problem exploration** (4–5 questions): is the problem real? how bad? how often?\n" +
"**Current behavior** (3–4 questions): what do they do today? what's broken about it?\n" +
(params.validationGoal !== "problem"
? "**Solution reaction** (2–3 questions): show concept, ask for honest reaction (NOT 'do you like it?')\n"
: "") +
(params.validationGoal === "willingness_to_pay" || params.validationGoal === "full_discovery"
? "**Economics** (1–2 questions): what do they spend on this problem today?\n"
: "") +
"For each question, note: what you're trying to learn and what a 'good answer' looks like. " +
"Include 3 follow-up probes for the most important questions.",
});
}),
}),
(0, sdk_1.tool)({
name: "build_validation_scorecard",
description: (0, sdk_1.text) `
Build a validation scorecard for an idea based on all experiments run so far.
Aggregates evidence across experiments, scores confidence in each assumption category,
and produces a go/pivot/kill recommendation with clear reasoning.
`,
parameters: {
ideaId: zod_1.z.string().describe("Idea ID to build scorecard for"),
},
implementation: safe_impl("build_validation_scorecard", async ({ ideaId }) => {
const db = await loadDB(dataDir());
const idea = db.ideas.find((i) => i.id === ideaId);
if (!idea)
throw new Error(`Idea ID '${ideaId}' not found.`);
const experiments = (db.experiments ?? []).filter((e) => e.ideaId === ideaId);
const completed = experiments.filter((e) => e.result !== "pending");
const validated = completed.filter((e) => e.result === "validated");
const invalidated = completed.filter((e) => e.result === "invalidated");
const inconclusive = completed.filter((e) => e.result === "inconclusive");
const validationRate = completed.length > 0
? Math.round((validated.length / completed.length) * 100)
: 0;
const linkedPainPoints = db.painPoints.filter((p) => idea.linkedPainPointIds.includes(p.id));
const avgImpact = linkedPainPoints.length > 0
? linkedPainPoints.filter((p) => p.impact === "critical" || p.impact === "high").length / linkedPainPoints.length
: 0;
const priorityScore = Math.round(((idea.impactScore + idea.feasibilityScore + idea.noveltyScore - idea.effortScore) / 3) * 10) / 10;
let recommendation;
if (completed.length === 0) {
recommendation = "NO DATA — Run at least 2–3 experiments before deciding.";
}
else if (invalidated.length > validated.length && completed.length >= 2) {
recommendation = "KILL OR PIVOT — More assumptions failed than passed. Re-examine the core problem or target user.";
}
else if (validated.length >= 2 && validationRate >= 60) {
recommendation = "GO — Evidence supports proceeding. Define MVP and start building.";
}
else if (completed.length >= 2 && validationRate >= 40) {
recommendation = "CONTINUE VALIDATING — Promising signals but not enough confidence yet. Run 2–3 more targeted experiments.";
}
else {
recommendation = "UNCERTAIN — Mixed results. Focus next experiments on the assumptions that matter most.";
}
return json({
ideaTitle: idea.title,
ideaStatus: idea.validationStatus,
priorityScore,
scorecard: {
experimentsRun: experiments.length,
completed: completed.length,
validated: validated.length,
invalidated: invalidated.length,
inconclusive: inconclusive.length,
validationRate: `${validationRate}%`,
linkedPainPoints: linkedPainPoints.length,
highImpactPainPointCoverage: `${Math.round(avgImpact * 100)}%`,
},
ideaScores: {
impact: idea.impactScore,
feasibility: idea.feasibilityScore,
novelty: idea.noveltyScore,
effort: idea.effortScore,
priorityScore,
},
experiments: completed.map((e) => ({
type: e.type,
result: e.result,
evidence: e.evidence.slice(0, 200),
learnings: e.learnings.slice(0, 200),
})),
recommendation,
instructions: `Based on all the validation data above for "${idea.title}", produce: ` +
"(1) Confidence score 0–100 for each category: desirability, feasibility, viability, usability — " +
" base each score on the actual experiment results and evidence above, not intuition; " +
"(2) 3 strongest pieces of evidence FOR the idea — cite specific experiments or data points, " +
"(3) 3 strongest pieces of evidence AGAINST the idea — do not minimize negative results; " +
"(4) Top 2 remaining unknowns that the evidence does not yet answer, " +
"(5) Balanced assessment: summarize what the data shows. Present the case for continuing AND the case for stopping. " +
" Do not issue a single GO/PIVOT/KILL verdict as if it were objective — the user makes that call. " +
" If the evidence is genuinely ambiguous, say so explicitly.",
});
}),
}),
// =========================================================================
// MVP DEFINITION & TESTING
// =========================================================================
(0, sdk_1.tool)({
name: "define_mvp",
description: (0, sdk_1.text) `
Define the Minimum Viable Product (MVP) for an idea.
An MVP is NOT a minimal product — it is the SMALLEST experiment that tests the core value hypothesis.
Returns a focused MVP scope with must-have features, explicit cut list,
build/measure/learn loop, and launch checklist.
Saves the MVP definition to the idea record.
`,
parameters: {
ideaId: zod_1.z.string().describe("Idea ID to define MVP for"),
targetUser: zod_1.z.string().default("").describe("Primary user persona for the MVP"),
coreProblem: zod_1.z.string().default("").describe("The single problem the MVP solves"),
constraints: zod_1.z.string().default("").describe("Constraints: team size, budget, timeline, tech stack"),
mvpType: zod_1.z.enum([
"concierge", // Manual service delivery
"wizard_of_oz", // Fake automation
"landing_page", // Pre-launch page
"single_feature", // One core feature only
"prototype", // Clickable non-functional
"full_build", // Real lightweight product
]).default("single_feature").describe("Type of MVP to define"),
},
implementation: safe_impl("define_mvp", async (params) => {
const db = await loadDB(dataDir());
const ideaIdx = db.ideas.findIndex((i) => i.id === params.ideaId);
if (ideaIdx === -1)
throw new Error(`Idea ID '${params.ideaId}' not found.`);
const idea = db.ideas[ideaIdx];
const mvpTypeDescriptions = {
concierge: "Manually deliver the service to 3–5 paying users. No code. Validate willingness to pay and core value before building anything.",
wizard_of_oz: "Build the front-end UI. Do the back-end work manually. Users think it's automated — you're the wizard behind the curtain.",
landing_page: "A single page that communicates the value prop and captures emails. Measures demand before building.",
single_feature: "Build only the one feature that delivers the core value. Everything else is cut. No onboarding, no settings, no edge cases.",
prototype: "Clickable Figma/Framer prototype. Zero code. Tests UX and value prop with real users before writing a line.",
full_build: "A simple but real product. Focus on the happy path only — one user type, one core workflow, no edge cases.",
};
const payload = {
ideaTitle: idea.title,
ideaDescription: idea.description,
mvpType: params.mvpType,
mvpTypeApproach: mvpTypeDescriptions[params.mvpType],
targetUser: params.targetUser,
coreProblem: params.coreProblem,
constraints: params.constraints,
experiments: (db.experiments ?? [])
.filter((e) => e.ideaId === params.ideaId && e.result === "validated")
.map((e) => ({ type: e.type, evidence: e.evidence.slice(0, 150) })),
instructions: `Define the MVP for "${idea.title}" as a ${params.mvpType.replace(/_/g, " ")} MVP. ` +
`Target user: ${params.targetUser || "primary user"}. ` +
`Core problem: ${params.coreProblem || "the main pain point"}. ` +
`Constraints: ${params.constraints || "none specified"}.\n\n` +
"Produce:\n" +
"## Core Value Hypothesis\n[Single sentence: 'We believe [user] will [do action] because [value]']\n\n" +
"## MVP Scope — MUST HAVE (3–5 items max)\n[Only features that directly test the hypothesis]\n\n" +
"## Explicitly NOT in MVP\n[List 5+ things you're cutting and why]\n\n" +
"## Build Plan\n[Key steps, tools to use, estimated time]\n\n" +
"## Success Metrics\n[2–3 specific measurable outcomes that mean 'it worked']\n\n" +
"## Failure Criteria\n[What would tell you to stop and pivot]\n\n" +
"## Launch Checklist\n[5–10 items to check before showing to first users]\n\n" +
"Be ruthlessly minimal. Every feature you add is a hypothesis that needs its own validation.",
};
// Save MVP definition to idea
db.ideas[ideaIdx].mvpDefinition = `${params.mvpType} MVP for: ${params.coreProblem || idea.description.slice(0, 100)}`;
db.ideas[ideaIdx].updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
return json(payload);
}),
}),
(0, sdk_1.tool)({
name: "generate_landing_page_copy",
description: (0, sdk_1.text) `
Generate ready-to-use landing page copy for testing an idea before building it.
A landing page with a clear CTA and email capture is the cheapest way to measure demand.
Returns headline options, subheadline, feature bullets, social proof placeholders,
CTA copy, and an FAQ. All copy follows conversion best practices.
`,
parameters: {
ideaId: zod_1.z.string().default("").describe("Idea ID to load context from"),
productName: zod_1.z.string().default("").describe("Product/idea name"),
tagline: zod_1.z.string().default("").describe("One-line value prop (or leave blank to generate)"),
targetUser: zod_1.z.string().describe("Primary user persona (e.g. 'freelance designers')"),
coreBenefit: zod_1.z.string().describe("The #1 benefit the product delivers"),
topPainPoint: zod_1.z.string().default("").describe("The biggest pain this solves"),
ctaGoal: zod_1.z.enum(["email_signup", "waitlist", "book_demo", "early_access", "buy_now"])
.default("waitlist").describe("What action you want visitors to take"),
tone: zod_1.z.enum(["professional", "conversational", "bold", "empathetic"]).default("conversational"),
},
implementation: safe_impl("generate_landing_page_copy", async (params) => {
let description = "";
let title = params.productName;
if (params.ideaId) {
const db = await loadDB(dataDir());
const idea = db.ideas.find((i) => i.id === params.ideaId);
if (idea) {
description = idea.description;
title = title || idea.title;
}
}
const ctaCopy = {
email_signup: { primary: "Get Early Access", secondary: "Notify me when it launches" },
waitlist: { primary: "Join the Waitlist", secondary: "Be first to know" },
book_demo: { primary: "Book a Demo", secondary: "See it in action" },
early_access: { primary: "Request Early Access", secondary: "Join 100+ early users" },
buy_now: { primary: "Get Started", secondary: "Start your free trial" },
};
return json({
productName: title || "Your Product",
targetUser: params.targetUser,
coreBenefit: params.coreBenefit,
topPainPoint: params.topPainPoint,
ideaDescription: description,
ctaGoal: params.ctaGoal,
ctaSuggestions: ctaCopy[params.ctaGoal],
tone: params.tone,
instructions: `Write complete landing page copy for "${title || "this product"}" targeting ${params.targetUser}. ` +
`Core benefit: ${params.coreBenefit}. ` +
`Top pain: ${params.topPainPoint || "the main problem"}. ` +
`Tone: ${params.tone}. CTA: ${params.ctaGoal.replace(/_/g, " ")}.\n\n` +
"Produce exactly:\n" +
"**HEADLINE** (3 options, A/B/C — 8 words max each, benefit-first)\n" +
"**SUBHEADLINE** (1–2 sentences expanding on the headline promise)\n" +
"**HERO SECTION** (2–3 sentence description of what it does and for whom)\n" +
"**3 FEATURE BULLETS** (benefit-first, not feature-first, 10 words max each)\n" +
"**SOCIAL PROOF PLACEHOLDER** (what testimonials/logos to collect first)\n" +
"**CTA COPY** (button text + supporting micro-copy below the button)\n" +
"**FAQ** (3 most common objections and how to answer them)\n" +
"**META DESCRIPTION** (155 chars for SEO/social share)\n\n" +
"Rules: No jargon. No 'revolutionary' or 'innovative'. Start with the user's pain, not your product features.",
});
}),
}),
(0, sdk_1.tool)({
name: "validation_dashboard",
description: (0, sdk_1.text) `
Cross-idea validation overview — your "where am I?" briefing.
Shows active ideas by validation status, pending experiments,
highest-risk untested assumptions, and overall validation progress.
Use this when you want a quick pulse on your entire idea portfolio.
`,
parameters: {
includeArchived: zod_1.z.boolean().default(false).describe("Include archived/rejected ideas in the dashboard"),
},
implementation: safe_impl("validation_dashboard", async (params) => {
const db = await loadDB(dataDir());
const ideas = params.includeArchived
? db.ideas
: db.ideas.filter((i) => i.status !== "archived" && i.status !== "rejected");
const experiments = db.experiments ?? [];
// --- Validation status breakdown ---
const byValidation = {
not_started: [], in_progress: [], validated: [], invalidated: [],
};
for (const idea of ideas) {
const vs = idea.validationStatus ?? "not_started";
(byValidation[vs] ??= []).push(idea);
}
// --- Experiment stats ---
const activeIdeaIds = new Set(ideas.map((i) => i.id));
const relevantExperiments = experiments.filter((e) => activeIdeaIds.has(e.ideaId));
const pending = relevantExperiments.filter((e) => e.result === "pending");
const validated = relevantExperiments.filter((e) => e.result === "validated");
const invalidated = relevantExperiments.filter((e) => e.result === "invalidated");
const inconclusive = relevantExperiments.filter((e) => e.result === "inconclusive");
// --- Highest-risk untested assumptions ---
const untestedAssumptions = [];
for (const idea of ideas) {
for (const a of idea.assumptions ?? []) {
if (a.validatedBy.length === 0) {
untestedAssumptions.push({ ideaId: idea.id, ideaTitle: idea.title, assumption: a });
}
}
}
// Sort: critical/high risk first, then lowest confidence
const riskOrder = { critical: 0, high: 1, medium: 2, low: 3 };
untestedAssumptions.sort((a, b) => {
const rd = (riskOrder[a.assumption.riskLevel] ?? 3) - (riskOrder[b.assumption.riskLevel] ?? 3);
if (rd !== 0)
return rd;
return a.assumption.confidence - b.assumption.confidence;
});
// --- Per-idea progress ---
const ideaProgress = ideas.map((idea) => {
const ideaExps = relevantExperiments.filter((e) => e.ideaId === idea.id);
const totalAssumptions = (idea.assumptions ?? []).length;
const testedAssumptions = (idea.assumptions ?? []).filter((a) => a.validatedBy.length > 0).length;
return {
id: idea.id,
title: idea.title,
status: idea.status,
validationStatus: idea.validationStatus ?? "not_started",
scores: { impact: idea.impactScore, feasibility: idea.feasibilityScore, novelty: idea.noveltyScore },
assumptions: { total: totalAssumptions, tested: testedAssumptions },
experiments: {
total: ideaExps.length,
pending: ideaExps.filter((e) => e.result === "pending").length,
validated: ideaExps.filter((e) => e.result === "validated").length,
invalidated: ideaExps.filter((e) => e.result === "invalidated").length,
},
};
});
return json({
summary: {
totalActiveIdeas: ideas.length,
byValidationStatus: {
not_started: byValidation.not_started.length,
in_progress: byValidation.in_progress.length,
validated: byValidation.validated.length,
invalidated: byValidation.invalidated.length,
},
experiments: {
total: relevantExperiments.length,
pending: pending.length,
validated: validated.length,
invalidated: invalidated.length,
inconclusive: inconclusive.length,
},
untestedAssumptions: untestedAssumptions.length,
},
needsAttention: {
pendingExperiments: pending.slice(0, 10).map((e) => ({
id: e.id, ideaId: e.ideaId, title: e.title, type: e.type,
hypothesis: e.hypothesis, effort: e.effort,
})),
highRiskUntested: untestedAssumptions.slice(0, 10).map((a) => ({
ideaId: a.ideaId, ideaTitle: a.ideaTitle,
assumptionId: a.assumption.id,
statement: a.assumption.statement,
type: a.assumption.type,
riskLevel: a.assumption.riskLevel,
confidence: a.assumption.confidence,
})),
ideasWithNoExperiments: ideas
.filter((i) => i.validationStatus !== "validated" && i.validationStatus !== "invalidated")
.filter((i) => !relevantExperiments.some((e) => e.ideaId === i.id))
.map((i) => ({ id: i.id, title: i.title, status: i.validationStatus ?? "not_started" })),
},
ideaProgress,
});
}),
}),
];
return tools;
};
exports.toolsProvider = toolsProvider;