toolsProvider.js
"use strict";
/**
* Founder Plugin — toolsProvider
*
* Tools:
* Startup · capture_startup, update_startup, list_startups, get_startup
* Discovery · log_customer_problem, list_customer_problems
* Competitors · scan_competitors, list_competitors
* Validation · create_validation_plan, record_experiment_result, list_experiments
* Decisions · log_decision, list_decisions
* Strategy · generate_mvp_scope, launch_asset_generator
* Review · weekly_founder_review
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.toolsProvider = void 0;
const sdk_1 = require("@lmstudio/sdk");
const zod_1 = require("zod");
const config_1 = require("./config");
const db_1 = require("./db");
const search_1 = require("./search");
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, fix the parameter, and retry.",
}, null, 2);
}
};
}
const toolsProvider = async (ctl) => {
const cfg = ctl.getPluginConfig(config_1.pluginConfigSchematics);
const dataDir = () => (0, db_1.getDataDir)(cfg.get("dataPath"));
const searxngUrl = () => cfg.get("searxngUrl").trim() || undefined;
const tools = [
// -----------------------------------------------------------------------
// STARTUP CRUD
// -----------------------------------------------------------------------
(0, sdk_1.tool)({
name: "capture_startup",
description: (0, sdk_1.text) `
Register a new startup in persistent storage. Returns the startup ID.
A startup is a hypothesis about a problem worth solving for a specific customer.
Fill in as much as you know — fields can be updated later with update_startup.
Call when the user describes a new business idea or startup they're working on.
Do NOT create duplicate startups — call list_startups first to check.
`,
parameters: {
name: zod_1.z.string().describe("Startup name or working title"),
oneLiner: zod_1.z.string().default("")
.describe("One sentence: '[Product] for [ICP] who [problem]'"),
hypothesis: zod_1.z.string().default("")
.describe("Core belief to validate: 'We believe [user] has [problem] and will [behavior] because [reason]'"),
icp: zod_1.z.string().default("")
.describe("Ideal customer profile: who specifically is the first customer?"),
problemStatement: zod_1.z.string().default("")
.describe("The specific pain being solved"),
revenueModel: zod_1.z.string().default("")
.describe("How it makes money: SaaS, marketplace, usage-based, etc."),
pricingHypothesis: zod_1.z.string().default("")
.describe("What you believe customers will pay and why"),
stage: zod_1.z.enum(["idea", "problem", "solution", "traction", "growth", "paused", "killed"])
.default("idea").describe("Current stage"),
},
implementation: safe_impl("capture_startup", async (params) => {
const db = await (0, db_1.loadDB)(dataDir());
const now = new Date().toISOString();
const startup = {
id: (0, db_1.makeId)(),
name: params.name,
stage: params.stage,
oneLiner: params.oneLiner,
hypothesis: params.hypothesis,
icp: params.icp,
problemStatement: params.problemStatement,
revenueModel: params.revenueModel,
pricingHypothesis: params.pricingHypothesis,
createdAt: now,
updatedAt: now,
};
db.startups.push(startup);
await (0, db_1.saveDB)(dataDir(), db);
return json({ saved: true, startupId: startup.id, name: startup.name, stage: startup.stage });
}),
}),
(0, sdk_1.tool)({
name: "update_startup",
description: (0, sdk_1.text) `
Update fields on an existing startup record. Only provided fields are changed.
Returns the updated startup.
Call when the startup pivots, advances stage, or new information is learned.
`,
parameters: {
startupId: zod_1.z.string().describe("Startup ID from capture_startup or list_startups"),
name: zod_1.z.string().optional(),
oneLiner: zod_1.z.string().optional(),
hypothesis: zod_1.z.string().optional(),
icp: zod_1.z.string().optional(),
problemStatement: zod_1.z.string().optional(),
revenueModel: zod_1.z.string().optional(),
pricingHypothesis: zod_1.z.string().optional(),
stage: zod_1.z.enum(["idea", "problem", "solution", "traction", "growth", "paused", "killed"]).optional(),
},
implementation: safe_impl("update_startup", async (params) => {
const db = await (0, db_1.loadDB)(dataDir());
const idx = db.startups.findIndex(s => s.id === params.startupId);
if (idx === -1)
throw new Error(`Startup '${params.startupId}' not found.`);
const s = db.startups[idx];
if (params.name !== undefined)
s.name = params.name;
if (params.oneLiner !== undefined)
s.oneLiner = params.oneLiner;
if (params.hypothesis !== undefined)
s.hypothesis = params.hypothesis;
if (params.icp !== undefined)
s.icp = params.icp;
if (params.problemStatement !== undefined)
s.problemStatement = params.problemStatement;
if (params.revenueModel !== undefined)
s.revenueModel = params.revenueModel;
if (params.pricingHypothesis !== undefined)
s.pricingHypothesis = params.pricingHypothesis;
if (params.stage !== undefined)
s.stage = params.stage;
s.updatedAt = new Date().toISOString();
await (0, db_1.saveDB)(dataDir(), db);
return json({ updated: true, startup: s });
}),
}),
(0, sdk_1.tool)({
name: "list_startups",
description: (0, sdk_1.text) `
List all startups in persistent storage with id, name, stage, one-liner,
and last-updated timestamp. Does NOT return full detail — use get_startup for that.
Call at the start of a session to see what's being tracked, or before capture_startup
to avoid duplicates.
`,
parameters: {
stage: zod_1.z.enum(["idea", "problem", "solution", "traction", "growth", "paused", "killed", "all"])
.default("all").describe("Filter by stage, or 'all' for everything"),
},
implementation: safe_impl("list_startups", async ({ stage }) => {
const db = await (0, db_1.loadDB)(dataDir());
const startups = stage === "all"
? db.startups
: db.startups.filter(s => s.stage === stage);
return json({
startups: startups.map(s => ({
id: s.id, name: s.name, stage: s.stage,
oneLiner: s.oneLiner, updatedAt: s.updatedAt,
})),
count: startups.length,
});
}),
}),
(0, sdk_1.tool)({
name: "get_startup",
description: (0, sdk_1.text) `
Return full detail for a single startup including hypothesis, ICP, problem statement,
revenue model, pricing hypothesis, and counts of problems/experiments/decisions/competitors.
Call when the user asks about a specific startup or before creating a validation plan.
`,
parameters: {
startupId: zod_1.z.string().describe("Startup ID"),
},
implementation: safe_impl("get_startup", async ({ startupId }) => {
const db = await (0, db_1.loadDB)(dataDir());
const s = db.startups.find(s => s.id === startupId);
if (!s)
throw new Error(`Startup '${startupId}' not found.`);
return json({
...s,
problemCount: db.customerProblems.filter(p => p.startupId === startupId).length,
experimentCount: db.experiments.filter(e => e.startupId === startupId).length,
decisionCount: db.decisions.filter(d => d.startupId === startupId).length,
competitorCount: db.competitors.filter(c => c.startupId === startupId).length,
});
}),
}),
// -----------------------------------------------------------------------
// CUSTOMER DISCOVERY
// -----------------------------------------------------------------------
(0, sdk_1.tool)({
name: "log_customer_problem",
description: (0, sdk_1.text) `
Record a customer pain point observed in an interview, survey, support ticket,
or user observation. Returns the problem ID.
Verbatim quotes are mandatory — they are the evidence. Paraphrasing loses signal.
Call immediately after any customer conversation, not days later.
Do NOT log hypothetical problems — only problems observed in the real world.
`,
parameters: {
startupId: zod_1.z.string().describe("Startup ID this problem belongs to"),
source: zod_1.z.string().describe("Where this was observed: 'interview', 'survey', 'support ticket', 'observation', 'reddit thread', etc."),
description: zod_1.z.string().describe("What the problem is in your own words"),
verbatim: zod_1.z.string().default("")
.describe("Exact quote from the customer. Required if from an interview."),
frequency: zod_1.z.enum(["rare", "occasional", "frequent", "constant"])
.describe("How often the customer faces this problem"),
severity: zod_1.z.enum(["minor", "moderate", "critical"])
.describe("How much pain this causes them"),
workaround: zod_1.z.string().default("")
.describe("What they do today to work around this problem"),
},
implementation: safe_impl("log_customer_problem", async (params) => {
const db = await (0, db_1.loadDB)(dataDir());
if (!db.startups.find(s => s.id === params.startupId)) {
throw new Error(`Startup '${params.startupId}' not found.`);
}
const problem = {
id: (0, db_1.makeId)(),
startupId: params.startupId,
source: params.source,
description: params.description,
verbatim: params.verbatim,
frequency: params.frequency,
severity: params.severity,
workaround: params.workaround,
createdAt: new Date().toISOString(),
};
db.customerProblems.push(problem);
await (0, db_1.saveDB)(dataDir(), db);
return json({ saved: true, problemId: problem.id });
}),
}),
(0, sdk_1.tool)({
name: "list_customer_problems",
description: (0, sdk_1.text) `
List all customer problems logged for a startup, sorted by severity then frequency.
Returns description, source, frequency, severity, verbatim quote, and workaround.
Call before create_validation_plan to see what evidence exists, or when the user
asks "what problems have we found?" or "what did customers say?".
`,
parameters: {
startupId: zod_1.z.string().describe("Startup ID"),
severity: zod_1.z.enum(["minor", "moderate", "critical", "all"]).default("all")
.describe("Filter by severity, or 'all'"),
},
implementation: safe_impl("list_customer_problems", async ({ startupId, severity }) => {
const db = await (0, db_1.loadDB)(dataDir());
const severityOrder = { critical: 0, moderate: 1, minor: 2 };
const freqOrder = { constant: 0, frequent: 1, occasional: 2, rare: 3 };
const problems = db.customerProblems
.filter(p => p.startupId === startupId && (severity === "all" || p.severity === severity))
.sort((a, b) => (severityOrder[a.severity] - severityOrder[b.severity]) ||
(freqOrder[a.frequency] - freqOrder[b.frequency]));
return json({ problems, count: problems.length });
}),
}),
// -----------------------------------------------------------------------
// COMPETITOR INTELLIGENCE
// -----------------------------------------------------------------------
(0, sdk_1.tool)({
name: "scan_competitors",
description: (0, sdk_1.text) `
Search the web for competitors to a startup, extract positioning and pricing signals,
and save results to persistent storage. Returns a list of competitor records.
Call when the user asks "who are our competitors?" or before generate_mvp_scope
to ensure the differentiation is grounded in real market data.
Do NOT call more than once per week per startup — data ages gracefully.
`,
parameters: {
startupId: zod_1.z.string().describe("Startup ID"),
marketDescription: zod_1.z.string()
.describe("Short description of the market, e.g. 'AI writing tools for marketing teams'"),
maxCompetitors: zod_1.z.coerce.number().int().min(1).max(10).default(5)
.describe("Max number of competitors to research"),
},
implementation: safe_impl("scan_competitors", async ({ startupId, marketDescription, maxCompetitors }) => {
const db = await (0, db_1.loadDB)(dataDir());
const startup = db.startups.find(s => s.id === startupId);
if (!startup)
throw new Error(`Startup '${startupId}' not found.`);
const queries = [
`${marketDescription} top tools software`,
`${marketDescription} alternatives competitors`,
`best ${marketDescription} 2024 2025`,
];
const seen = new Set();
const results = [];
for (const q of queries) {
const hits = await (0, search_1.webSearch)(q, 6, 10_000, searxngUrl());
for (const h of hits) {
if (!seen.has(h.url)) {
seen.add(h.url);
results.push(h);
}
}
if (results.length >= maxCompetitors * 3)
break;
}
const now = new Date().toISOString();
const competitors = results.slice(0, maxCompetitors).map(r => {
let domain = r.url;
try {
domain = new URL(r.url).hostname.replace(/^www\./, "");
}
catch { /* ok */ }
const existing = db.competitors.find(c => c.startupId === startupId && c.name === domain);
if (existing) {
existing.positioning = r.snippet.slice(0, 300);
existing.lastScanned = now;
return existing;
}
const newComp = {
id: (0, db_1.makeId)(),
startupId,
name: domain,
url: r.url,
positioning: r.snippet.slice(0, 300),
targetCustomer: "",
pricing: "",
strengths: "",
weaknesses: "",
differentiator: "",
lastScanned: now,
createdAt: now,
};
db.competitors.push(newComp);
return newComp;
});
await (0, db_1.saveDB)(dataDir(), db);
return json({
startupId,
marketDescription,
competitorsFound: competitors.length,
competitors: competitors.map(c => ({
id: c.id, name: c.name, url: c.url,
positioning: c.positioning, lastScanned: c.lastScanned,
})),
nextStep: "Use update_competitor to add pricing, strengths, weaknesses, and differentiator after reviewing each competitor's site.",
});
}),
}),
(0, sdk_1.tool)({
name: "update_competitor",
description: (0, sdk_1.text) `
Add or update pricing, strengths, weaknesses, target customer, and differentiator
for a competitor record. Returns the updated competitor.
Call after scan_competitors once you've reviewed each competitor's website manually.
`,
parameters: {
competitorId: zod_1.z.string().describe("Competitor ID from scan_competitors or list_competitors"),
pricing: zod_1.z.string().optional().describe("Pricing model and price points"),
targetCustomer: zod_1.z.string().optional().describe("Who they sell to"),
strengths: zod_1.z.string().optional().describe("What they do well"),
weaknesses: zod_1.z.string().optional().describe("Their gaps — what customers complain about"),
differentiator: zod_1.z.string().optional().describe("How you're different from them"),
},
implementation: safe_impl("update_competitor", async (params) => {
const db = await (0, db_1.loadDB)(dataDir());
const c = db.competitors.find(c => c.id === params.competitorId);
if (!c)
throw new Error(`Competitor '${params.competitorId}' not found.`);
if (params.pricing !== undefined)
c.pricing = params.pricing;
if (params.targetCustomer !== undefined)
c.targetCustomer = params.targetCustomer;
if (params.strengths !== undefined)
c.strengths = params.strengths;
if (params.weaknesses !== undefined)
c.weaknesses = params.weaknesses;
if (params.differentiator !== undefined)
c.differentiator = params.differentiator;
await (0, db_1.saveDB)(dataDir(), db);
return json({ updated: true, competitor: c });
}),
}),
(0, sdk_1.tool)({
name: "list_competitors",
description: (0, sdk_1.text) `
List all tracked competitors for a startup. Returns name, positioning, pricing,
weaknesses, and differentiator per competitor.
Call when the user asks "who are our competitors?" or before generate_mvp_scope.
`,
parameters: {
startupId: zod_1.z.string().describe("Startup ID"),
},
implementation: safe_impl("list_competitors", async ({ startupId }) => {
const db = await (0, db_1.loadDB)(dataDir());
const competitors = db.competitors.filter(c => c.startupId === startupId);
return json({ competitors, count: competitors.length });
}),
}),
// -----------------------------------------------------------------------
// VALIDATION
// -----------------------------------------------------------------------
(0, sdk_1.tool)({
name: "create_validation_plan",
description: (0, sdk_1.text) `
Map the startup's core assumptions and return a ranked experiment queue with
the highest-risk, lowest-confidence assumptions first. Returns a scaffold payload
— follow the instructions to produce the validation plan JSON.
Call after list_customer_problems to ground the assumptions in real evidence.
Do NOT skip this — building before validating assumptions burns time and money.
`,
parameters: {
startupId: zod_1.z.string().describe("Startup ID"),
focusArea: zod_1.z.string().optional()
.describe("Optional: focus on a specific assumption type — 'desirability', 'viability', 'feasibility'"),
},
implementation: safe_impl("create_validation_plan", async ({ startupId, focusArea }) => {
const db = await (0, db_1.loadDB)(dataDir());
const startup = db.startups.find(s => s.id === startupId);
if (!startup)
throw new Error(`Startup '${startupId}' not found.`);
const problems = db.customerProblems.filter(p => p.startupId === startupId);
const experiments = db.experiments.filter(e => e.startupId === startupId);
const competitors = db.competitors.filter(c => c.startupId === startupId);
return json({
action: "create_validation_plan",
startup: {
name: startup.name,
stage: startup.stage,
hypothesis: startup.hypothesis,
icp: startup.icp,
problemStatement: startup.problemStatement,
revenueModel: startup.revenueModel,
pricingHypothesis: startup.pricingHypothesis,
},
evidence: {
customerProblemsLogged: problems.length,
criticalProblems: problems.filter(p => p.severity === "critical").length,
experimentsRun: experiments.length,
validatedAssumptions: experiments.filter(e => e.result === "validated").length,
invalidatedAssumptions: experiments.filter(e => e.result === "invalidated").length,
competitorsTracked: competitors.length,
},
focusArea: focusArea ?? "all",
instructions: `You are a startup validator. Analyze the startup data above and produce a validation plan.
Identify the 5 most critical assumptions this startup is making. For each assumption:
1. State the assumption explicitly
2. Classify it: desirability (will people want this?) / viability (can they pay for it?) / feasibility (can we build it?)
3. Risk level: critical / high / medium / low
4. Current evidence: what the logged customer problems or experiments already tell us
5. Recommended experiment: the cheapest, fastest way to test it
Return ONLY valid JSON:
{
"assumptions": [
{
"rank": 1,
"assumption": "...",
"type": "desirability | viability | feasibility",
"riskLevel": "critical | high | medium | low",
"currentEvidence": "...",
"recommendedExperiment": {
"method": "...",
"successCriteria": "...",
"estimatedEffort": "hours | days | weeks",
"estimatedCost": "..."
}
}
],
"nextAction": "the single most important thing to do in the next 7 days"
}`,
});
}),
}),
(0, sdk_1.tool)({
name: "record_experiment_result",
description: (0, sdk_1.text) `
Log the result of a validation experiment — what was tested, what was observed,
and what was learned. Saves to persistent storage.
Results: validated (assumption confirmed), invalidated (assumption wrong),
inconclusive (insufficient signal), pending (not yet run).
Call immediately after running an experiment, not later.
Always fill in evidence with real data or quotes — not impressions.
`,
parameters: {
startupId: zod_1.z.string().describe("Startup ID"),
assumption: zod_1.z.string().describe("The assumption that was tested"),
method: zod_1.z.string().describe("How the experiment was run"),
successCriteria: zod_1.z.string().describe("What a pass looked like before running"),
result: zod_1.z.enum(["validated", "invalidated", "inconclusive", "pending"])
.describe("Outcome of the experiment"),
learnings: zod_1.z.string().default("").describe("What was discovered — pivot, persevere, or abandon?"),
evidence: zod_1.z.string().default("").describe("Data, metrics, or verbatim quotes supporting the result"),
},
implementation: safe_impl("record_experiment_result", async (params) => {
const db = await (0, db_1.loadDB)(dataDir());
if (!db.startups.find(s => s.id === params.startupId)) {
throw new Error(`Startup '${params.startupId}' not found.`);
}
const experiment = {
id: (0, db_1.makeId)(),
startupId: params.startupId,
assumption: params.assumption,
method: params.method,
successCriteria: params.successCriteria,
result: params.result,
learnings: params.learnings,
evidence: params.evidence,
testedAt: params.result !== "pending" ? (0, db_1.todayStr)() : null,
createdAt: new Date().toISOString(),
};
db.experiments.push(experiment);
// Auto-advance startup stage if first validation done
const startup = db.startups.find(s => s.id === params.startupId);
if (startup.stage === "idea" && params.result !== "pending") {
startup.stage = "problem";
startup.updatedAt = new Date().toISOString();
}
await (0, db_1.saveDB)(dataDir(), db);
return json({ saved: true, experimentId: experiment.id, result: experiment.result });
}),
}),
(0, sdk_1.tool)({
name: "list_experiments",
description: (0, sdk_1.text) `
List all validation experiments for a startup, sorted by most recent first.
Returns assumption, method, result, learnings, and evidence.
Call when the user asks "what have we tested?" or before weekly_founder_review.
`,
parameters: {
startupId: zod_1.z.string().describe("Startup ID"),
result: zod_1.z.enum(["validated", "invalidated", "inconclusive", "pending", "all"]).default("all")
.describe("Filter by result, or 'all'"),
},
implementation: safe_impl("list_experiments", async ({ startupId, result }) => {
const db = await (0, db_1.loadDB)(dataDir());
const experiments = db.experiments
.filter(e => e.startupId === startupId && (result === "all" || e.result === result))
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
return json({ experiments, count: experiments.length });
}),
}),
// -----------------------------------------------------------------------
// DECISIONS
// -----------------------------------------------------------------------
(0, sdk_1.tool)({
name: "log_decision",
description: (0, sdk_1.text) `
Record a strategic decision with the options considered, rationale, and expected outcome.
Decisions are append-only — they build a decision log for future reference.
Call when the user makes a significant strategic choice: pivot, pricing, target market,
build vs. buy, kill a feature, enter/exit a market.
Do NOT log tactical decisions — only choices that change direction.
`,
parameters: {
startupId: zod_1.z.string().describe("Startup ID"),
decision: zod_1.z.string().describe("What was decided"),
options: zod_1.z.string().default("").describe("What alternatives were considered"),
rationale: zod_1.z.string().describe("Why this choice over the alternatives"),
date: zod_1.z.string().default("").describe("ISO date, e.g. 2025-06-01. Defaults to today."),
},
implementation: safe_impl("log_decision", async (params) => {
const db = await (0, db_1.loadDB)(dataDir());
if (!db.startups.find(s => s.id === params.startupId)) {
throw new Error(`Startup '${params.startupId}' not found.`);
}
const decision = {
id: (0, db_1.makeId)(),
startupId: params.startupId,
decision: params.decision,
options: params.options,
rationale: params.rationale,
date: params.date || (0, db_1.todayStr)(),
outcome: "",
createdAt: new Date().toISOString(),
};
db.decisions.push(decision);
await (0, db_1.saveDB)(dataDir(), db);
return json({ saved: true, decisionId: decision.id });
}),
}),
(0, sdk_1.tool)({
name: "update_decision_outcome",
description: (0, sdk_1.text) `
Record the actual outcome of a past decision. Updates the outcome field on an
existing decision record.
Call when the user reports what happened as a result of a prior decision.
`,
parameters: {
decisionId: zod_1.z.string().describe("Decision ID from log_decision or list_decisions"),
outcome: zod_1.z.string().describe("What actually happened as a result of this decision"),
},
implementation: safe_impl("update_decision_outcome", async ({ decisionId, outcome }) => {
const db = await (0, db_1.loadDB)(dataDir());
const d = db.decisions.find(d => d.id === decisionId);
if (!d)
throw new Error(`Decision '${decisionId}' not found.`);
d.outcome = outcome;
await (0, db_1.saveDB)(dataDir(), db);
return json({ updated: true, decisionId, outcome });
}),
}),
(0, sdk_1.tool)({
name: "list_decisions",
description: (0, sdk_1.text) `
List all strategic decisions for a startup in reverse chronological order.
Returns decision, options considered, rationale, date, and outcome (if known).
Call when the user asks "why did we decide X?" or "what decisions have we made?".
`,
parameters: {
startupId: zod_1.z.string().describe("Startup ID"),
},
implementation: safe_impl("list_decisions", async ({ startupId }) => {
const db = await (0, db_1.loadDB)(dataDir());
const decisions = db.decisions
.filter(d => d.startupId === startupId)
.sort((a, b) => b.date.localeCompare(a.date));
return json({ decisions, count: decisions.length });
}),
}),
// -----------------------------------------------------------------------
// STRATEGY TOOLS
// -----------------------------------------------------------------------
(0, sdk_1.tool)({
name: "generate_mvp_scope",
description: (0, sdk_1.text) `
Define the smallest experiment that tests the startup's core value hypothesis.
Returns a scaffold payload — follow the instructions to produce the MVP scope JSON.
An MVP is NOT a minimal product — it is the cheapest way to test the core assumption.
Call after create_validation_plan and at least 3 customer problems are logged.
Do NOT call before customer evidence exists — MVPs built on gut die.
`,
parameters: {
startupId: zod_1.z.string().describe("Startup ID"),
mvpType: zod_1.z.enum([
"concierge", // Manual service, no code
"wizard_of_oz", // Fake automation, manual backend
"landing_page", // Demand test only
"single_feature", // One core feature, nothing else
"prototype", // Clickable, non-functional
]).default("single_feature").describe("Type of MVP to scope"),
constraints: zod_1.z.string().default("")
.describe("Constraints: team size, time budget, tech stack, etc."),
},
implementation: safe_impl("generate_mvp_scope", async ({ startupId, mvpType, constraints }) => {
const db = await (0, db_1.loadDB)(dataDir());
const startup = db.startups.find(s => s.id === startupId);
if (!startup)
throw new Error(`Startup '${startupId}' not found.`);
const problems = db.customerProblems.filter(p => p.startupId === startupId);
const experiments = db.experiments.filter(e => e.startupId === startupId && e.result === "validated");
const competitors = db.competitors.filter(c => c.startupId === startupId);
const typeGuide = {
concierge: "Manually deliver the service to 3–5 paying users. No code. Validate willingness to pay and core value first.",
wizard_of_oz: "Build the front-end. Do the back-end work manually. Users think it's automated — you're the wizard.",
landing_page: "One page with a value prop and email capture. Measures demand before building anything.",
single_feature: "Build only the one feature that delivers the core value. Cut everything else ruthlessly.",
prototype: "Clickable Figma/Framer prototype. Zero code. Tests UX and value prop with real users.",
};
return json({
action: "generate_mvp_scope",
startup: {
name: startup.name,
hypothesis: startup.hypothesis,
icp: startup.icp,
problemStatement: startup.problemStatement,
pricingHypothesis: startup.pricingHypothesis,
},
mvpType,
mvpTypeGuide: typeGuide[mvpType],
constraints: constraints || "none specified",
evidence: {
customerProblems: problems.slice(0, 5).map(p => ({
description: p.description,
severity: p.severity,
verbatim: p.verbatim,
})),
validatedAssumptions: experiments.map(e => e.assumption),
competitors: competitors.slice(0, 3).map(c => ({
name: c.name, weaknesses: c.weaknesses, differentiator: c.differentiator,
})),
},
instructions: `Define the ${mvpType.replace(/_/g, " ")} MVP for "${startup.name}".
Context:
- Hypothesis: ${startup.hypothesis || "(not set)"}
- ICP: ${startup.icp || "(not set)"}
- Core problem: ${startup.problemStatement || "(not set)"}
- Constraints: ${constraints || "none"}
- MVP approach: ${typeGuide[mvpType]}
Return ONLY valid JSON:
{
"coreValueHypothesis": "We believe [user] will [action] because [value]. We'll know it works when [measurable signal].",
"mustHave": ["feature 1", "feature 2", "feature 3"],
"explicitlyCut": ["thing 1 — why cut", "thing 2 — why cut"],
"buildPlan": ["step 1", "step 2", "step 3"],
"successMetrics": ["metric 1 with number", "metric 2 with number"],
"failureCriteria": "What would make you stop and pivot",
"launchChecklist": ["item 1", "item 2"]
}
Be ruthlessly minimal. Every item in mustHave is a bet. Minimize bets.`,
});
}),
}),
(0, sdk_1.tool)({
name: "launch_asset_generator",
description: (0, sdk_1.text) `
Generate ready-to-use launch assets: landing page copy, positioning statement,
cold outreach template, and elevator pitch. Returns a scaffold payload.
Call when the user says "help me write the landing page" / "write cold outreach"
/ "help me pitch this" or when preparing to launch.
Requires capture_startup with hypothesis and ICP filled in.
`,
parameters: {
startupId: zod_1.z.string().describe("Startup ID"),
assetType: zod_1.z.enum(["landing_page", "cold_outreach", "elevator_pitch", "positioning_statement", "all"])
.default("all").describe("Which asset to generate"),
ctaGoal: zod_1.z.enum(["waitlist", "demo_request", "free_trial", "purchase", "interview_request"])
.default("waitlist").describe("Desired call to action"),
tone: zod_1.z.enum(["professional", "conversational", "bold", "empathetic"])
.default("conversational"),
},
implementation: safe_impl("launch_asset_generator", async ({ startupId, assetType, ctaGoal, tone }) => {
const db = await (0, db_1.loadDB)(dataDir());
const startup = db.startups.find(s => s.id === startupId);
if (!startup)
throw new Error(`Startup '${startupId}' not found.`);
const problems = db.customerProblems
.filter(p => p.startupId === startupId && p.severity === "critical")
.slice(0, 3);
const competitors = db.competitors.filter(c => c.startupId === startupId).slice(0, 3);
const ctaCopy = {
waitlist: "Join the Waitlist",
demo_request: "Book a Demo",
free_trial: "Start Free Trial",
purchase: "Get Started",
interview_request: "Talk to Us",
};
return json({
action: "launch_asset_generator",
startup: {
name: startup.name,
oneLiner: startup.oneLiner,
hypothesis: startup.hypothesis,
icp: startup.icp,
problemStatement: startup.problemStatement,
pricingHypothesis: startup.pricingHypothesis,
},
assetType,
ctaGoal,
ctaCopy: ctaCopy[ctaGoal],
tone,
evidence: {
topProblems: problems.map(p => ({ description: p.description, verbatim: p.verbatim })),
competitors: competitors.map(c => ({ name: c.name, weaknesses: c.weaknesses })),
},
instructions: `Write launch assets for "${startup.name}" targeting: ${startup.icp || "primary customer"}.
Core problem: ${startup.problemStatement || startup.oneLiner}. Tone: ${tone}. CTA: ${ctaCopy[ctaGoal]}.
${assetType === "all" || assetType === "landing_page" ? `
## LANDING PAGE
- HEADLINE (3 options A/B/C, ≤8 words, benefit-first)
- SUBHEADLINE (1–2 sentences expanding the promise)
- 3 FEATURE BULLETS (benefit-first, ≤10 words each)
- SOCIAL PROOF PLACEHOLDER (what testimonials to collect first)
- CTA (button text + micro-copy below)
- FAQ (3 top objections and answers)` : ""}
${assetType === "all" || assetType === "cold_outreach" ? `
## COLD OUTREACH EMAIL
- Subject line (≤7 words)
- Body (≤100 words): pain → relevance → CTA
- P.S. line` : ""}
${assetType === "all" || assetType === "elevator_pitch" ? `
## ELEVATOR PITCH (30 seconds)
One paragraph, spoken-word style. Problem → solution → why us → ask.` : ""}
${assetType === "all" || assetType === "positioning_statement" ? `
## POSITIONING STATEMENT
For [ICP] who [problem], [product] is a [category] that [benefit]. Unlike [competitor], [differentiator].` : ""}
Rules: No jargon. No "revolutionary" or "game-changing". Start with the customer's pain, not your product.`,
});
}),
}),
// -----------------------------------------------------------------------
// WEEKLY REVIEW
// -----------------------------------------------------------------------
(0, sdk_1.tool)({
name: "weekly_founder_review",
description: (0, sdk_1.text) `
Generate a weekly review across all active startups: what assumptions are untested,
what experiments are pending, what decisions need to be made, and what the single
most important action is for each startup this week.
Call when the user says "weekly review" / "what should I focus on?" / "check in"
/ "what's most important this week?".
`,
parameters: {
startupId: zod_1.z.string().optional()
.describe("Focus on one startup. If blank, reviews all active startups."),
},
implementation: safe_impl("weekly_founder_review", async ({ startupId }) => {
const db = await (0, db_1.loadDB)(dataDir());
const activeStages = ["idea", "problem", "solution", "traction", "growth"];
const startups = startupId
? db.startups.filter(s => s.id === startupId)
: db.startups.filter(s => activeStages.includes(s.stage));
const reviews = startups.map(s => {
const problems = db.customerProblems.filter(p => p.startupId === s.id);
const experiments = db.experiments.filter(e => e.startupId === s.id);
const pending = experiments.filter(e => e.result === "pending");
const validated = experiments.filter(e => e.result === "validated");
const invalidated = experiments.filter(e => e.result === "invalidated");
const decisions = db.decisions.filter(d => d.startupId === s.id);
const openDecisions = decisions.filter(d => !d.outcome);
const competitors = db.competitors.filter(c => c.startupId === s.id);
// Determine what's missing or stale
const gaps = [];
if (problems.length === 0)
gaps.push("No customer problems logged — talk to potential customers first");
if (problems.length > 0 && experiments.length === 0)
gaps.push("Problems found but no experiments run — build a validation plan");
if (pending.length > 0)
gaps.push(`${pending.length} experiment(s) pending — run them this week`);
if (competitors.length === 0 && s.stage !== "idea")
gaps.push("No competitor research — run scan_competitors");
if (!s.hypothesis)
gaps.push("Core hypothesis not written — define it before building anything");
if (!s.icp)
gaps.push("ICP not defined — who specifically is the first customer?");
return {
startupId: s.id,
name: s.name,
stage: s.stage,
hypothesis: s.hypothesis,
icp: s.icp,
metrics: {
customerProblems: problems.length,
criticalProblems: problems.filter(p => p.severity === "critical").length,
experimentsRun: experiments.length,
validated: validated.length,
invalidated: invalidated.length,
pending: pending.length,
competitors: competitors.length,
openDecisions: openDecisions.length,
},
gaps,
mostRecentExperiment: experiments.sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0] ?? null,
openDecisions: openDecisions.map(d => ({ id: d.id, decision: d.decision, date: d.date })),
};
});
return json({
reviewDate: (0, db_1.todayStr)(),
activeStartups: reviews.length,
reviews,
topPriority: reviews
.sort((a, b) => b.metrics.criticalProblems - a.metrics.criticalProblems)
.slice(0, 1)
.map(r => ({ name: r.name, mostUrgentGap: r.gaps[0] ?? "All gaps addressed" }))[0] ?? null,
});
}),
}),
];
return tools;
};
exports.toolsProvider = toolsProvider;