fathom-cli 0.3.0 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +62 -56
  2. package/dist/data/guardrails/gdpr-basic.yaml +19 -0
  3. package/dist/data/guardrails/hipaa-basic.yaml +19 -0
  4. package/dist/data/guardrails/owasp-top-10.yaml +16 -0
  5. package/dist/data/guardrails/pci-basic.yaml +19 -0
  6. package/dist/data/guardrails/soc2-basic.yaml +19 -0
  7. package/dist/data/guardrails/wcag-aa.yaml +16 -0
  8. package/dist/index.js +993 -223
  9. package/dist/index.js.map +1 -1
  10. package/package.json +21 -9
  11. package/plugins/fathom/.claude-plugin/plugin.json +13 -0
  12. package/plugins/fathom/.mcp.json +8 -0
  13. package/plugins/fathom/skills/budget/.gitkeep +0 -0
  14. package/plugins/fathom/skills/budget/SKILL.md +40 -0
  15. package/plugins/fathom/skills/budget-warning/.gitkeep +0 -0
  16. package/plugins/fathom/skills/budget-warning/SKILL.md +52 -0
  17. package/plugins/fathom/skills/cost-preview/.gitkeep +0 -0
  18. package/plugins/fathom/skills/cost-preview/SKILL.md +70 -0
  19. package/plugins/fathom/skills/estimate/.gitkeep +0 -0
  20. package/plugins/fathom/skills/estimate/SKILL.md +58 -0
  21. package/plugins/fathom/skills/gsd/SKILL.md +35 -0
  22. package/plugins/fathom/skills/projections/.gitkeep +0 -0
  23. package/plugins/fathom/skills/projections/SKILL.md +36 -0
  24. package/plugins/fathom/skills/recommend-model/.gitkeep +0 -0
  25. package/plugins/fathom/skills/recommend-model/SKILL.md +53 -0
  26. package/plugins/fathom/skills/status/.gitkeep +0 -0
  27. package/plugins/fathom/skills/status/SKILL.md +42 -0
  28. package/plugins/fathom/tests/integrations.test.ts +66 -0
  29. package/plugins/fathom/tests/plugin.test.ts +49 -0
  30. package/plugins/fathom/tests/skills.test.ts +135 -0
package/dist/index.js CHANGED
@@ -6,7 +6,6 @@ import {
6
6
  import {
7
7
  __commonJS,
8
8
  __export,
9
- __require,
10
9
  __toESM
11
10
  } from "./chunk-SEGVTWSK.js";
12
11
 
@@ -103,7 +102,7 @@ var require_extend = __commonJS({
103
102
 
104
103
  // src/index.ts
105
104
  import "dotenv/config";
106
- import { Command as Command21 } from "commander";
105
+ import { Command as Command22 } from "commander";
107
106
 
108
107
  // ../../node_modules/.pnpm/bail@2.0.2/node_modules/bail/index.js
109
108
  function bail(error) {
@@ -480,10 +479,10 @@ var VFile = class {
480
479
  * @returns {undefined}
481
480
  * Nothing.
482
481
  */
483
- set basename(basename4) {
484
- assertNonEmpty(basename4, "basename");
485
- assertPart(basename4, "basename");
486
- this.path = default2.join(this.dirname || "", basename4);
482
+ set basename(basename5) {
483
+ assertNonEmpty(basename5, "basename");
484
+ assertPart(basename5, "basename");
485
+ this.path = default2.join(this.dirname || "", basename5);
487
486
  }
488
487
  /**
489
488
  * Get the parent path (example: `'~'`).
@@ -504,9 +503,9 @@ var VFile = class {
504
503
  * @returns {undefined}
505
504
  * Nothing.
506
505
  */
507
- set dirname(dirname8) {
506
+ set dirname(dirname10) {
508
507
  assertPath(this.basename, "dirname");
509
- this.path = default2.join(dirname8 || "", this.basename);
508
+ this.path = default2.join(dirname10 || "", this.basename);
510
509
  }
511
510
  /**
512
511
  * Get the extname (including dot) (example: `'.js'`).
@@ -1105,7 +1104,7 @@ var Processor = class _Processor extends CallableInstance {
1105
1104
  assertParser("process", this.parser || this.Parser);
1106
1105
  assertCompiler("process", this.compiler || this.Compiler);
1107
1106
  return done ? executor(void 0, done) : new Promise(executor);
1108
- function executor(resolve23, reject) {
1107
+ function executor(resolve25, reject) {
1109
1108
  const realFile = vfile(file);
1110
1109
  const parseTree = (
1111
1110
  /** @type {HeadTree extends undefined ? Node : HeadTree} */
@@ -1136,8 +1135,8 @@ var Processor = class _Processor extends CallableInstance {
1136
1135
  function realDone(error, file2) {
1137
1136
  if (error || !file2) {
1138
1137
  reject(error);
1139
- } else if (resolve23) {
1140
- resolve23(file2);
1138
+ } else if (resolve25) {
1139
+ resolve25(file2);
1141
1140
  } else {
1142
1141
  ok(done, "`done` is defined if `resolve` is not");
1143
1142
  done(void 0, file2);
@@ -1239,7 +1238,7 @@ var Processor = class _Processor extends CallableInstance {
1239
1238
  file = void 0;
1240
1239
  }
1241
1240
  return done ? executor(void 0, done) : new Promise(executor);
1242
- function executor(resolve23, reject) {
1241
+ function executor(resolve25, reject) {
1243
1242
  ok(
1244
1243
  typeof file !== "function",
1245
1244
  "`file` can\u2019t be a `done` anymore, we checked"
@@ -1253,8 +1252,8 @@ var Processor = class _Processor extends CallableInstance {
1253
1252
  );
1254
1253
  if (error) {
1255
1254
  reject(error);
1256
- } else if (resolve23) {
1257
- resolve23(resultingTree);
1255
+ } else if (resolve25) {
1256
+ resolve25(resultingTree);
1258
1257
  } else {
1259
1258
  ok(done, "`done` is defined if `resolve` is not");
1260
1259
  done(void 0, resultingTree, file2);
@@ -4082,10 +4081,10 @@ function resolveAll(constructs2, events, context) {
4082
4081
  const called = [];
4083
4082
  let index2 = -1;
4084
4083
  while (++index2 < constructs2.length) {
4085
- const resolve23 = constructs2[index2].resolveAll;
4086
- if (resolve23 && !called.includes(resolve23)) {
4087
- events = resolve23(events, context);
4088
- called.push(resolve23);
4084
+ const resolve25 = constructs2[index2].resolveAll;
4085
+ if (resolve25 && !called.includes(resolve25)) {
4086
+ events = resolve25(events, context);
4087
+ called.push(resolve25);
4089
4088
  }
4090
4089
  }
4091
4090
  return events;
@@ -9003,26 +9002,43 @@ var SYSTEM_PROMPT = `You are a technical complexity scorer for software features
9003
9002
  Respond with ONLY a JSON object. No markdown, no explanation.`;
9004
9003
  async function scoreComplexityAI(description, options = {}) {
9005
9004
  try {
9006
- const client = options.client ?? await getAnthropicClient(options.apiKey);
9007
- const model = options.model ?? "claude-haiku-4-5-20251001";
9008
- const response = await client.messages.create({
9009
- model,
9010
- max_tokens: 1024,
9011
- system: SYSTEM_PROMPT,
9012
- messages: [
9013
- {
9014
- role: "user",
9015
- content: `Analyze the complexity of this feature:
9005
+ const userPrompt = `Analyze the complexity of this feature:
9016
9006
 
9017
- ${description}`
9018
- }
9019
- ]
9020
- });
9021
- const textBlock = response.content.find((block) => block.type === "text");
9022
- if (!textBlock || textBlock.type !== "text") {
9023
- return null;
9007
+ ${description}`;
9008
+ let text3;
9009
+ if (options.generate) {
9010
+ text3 = await options.generate(SYSTEM_PROMPT, userPrompt);
9011
+ } else if (options.provider === "openai") {
9012
+ const openaiModule = await import("openai");
9013
+ const OpenAI = openaiModule.default ?? openaiModule;
9014
+ const client = new OpenAI({ apiKey: options.openaiApiKey ?? process.env.OPENAI_API_KEY });
9015
+ const response = await client.chat.completions.create({
9016
+ model: options.model ?? "gpt-4o-mini",
9017
+ messages: [
9018
+ { role: "system", content: SYSTEM_PROMPT },
9019
+ { role: "user", content: userPrompt }
9020
+ ],
9021
+ max_tokens: 1024
9022
+ });
9023
+ text3 = response.choices[0]?.message?.content ?? "";
9024
+ } else {
9025
+ const client = options.client ?? await getAnthropicClient(options.apiKey);
9026
+ const model = options.model ?? "claude-haiku-4-5-20251001";
9027
+ const response = await client.messages.create({
9028
+ model,
9029
+ max_tokens: 1024,
9030
+ system: SYSTEM_PROMPT,
9031
+ messages: [
9032
+ { role: "user", content: userPrompt }
9033
+ ]
9034
+ });
9035
+ const textBlock = response.content.find((block) => block.type === "text");
9036
+ if (!textBlock || textBlock.type !== "text") {
9037
+ return null;
9038
+ }
9039
+ text3 = textBlock.text;
9024
9040
  }
9025
- let jsonText = textBlock.text.trim();
9041
+ let jsonText = text3.trim();
9026
9042
  if (jsonText.startsWith("```")) {
9027
9043
  jsonText = jsonText.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
9028
9044
  }
@@ -9220,8 +9236,8 @@ var VERSION = "0.1.0";
9220
9236
 
9221
9237
  // src/commands/go.ts
9222
9238
  import { Command as Command3 } from "commander";
9223
- import { existsSync as existsSync6, readFileSync as readFileSync6, cpSync, mkdirSync as mkdirSync6 } from "fs";
9224
- import { resolve as resolve7, basename as basename2 } from "path";
9239
+ import { existsSync as existsSync6, readFileSync as readFileSync7, cpSync, mkdirSync as mkdirSync6 } from "fs";
9240
+ import { resolve as resolve7, basename as basename3 } from "path";
9225
9241
  import { spawn } from "child_process";
9226
9242
  import chalk4 from "chalk";
9227
9243
  import { select as select2, confirm as confirm2, input as input2, checkbox } from "@inquirer/prompts";
@@ -9302,11 +9318,21 @@ function updateWorkflowState(updates) {
9302
9318
 
9303
9319
  // src/commands/setup.ts
9304
9320
  import { Command } from "commander";
9305
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
9306
- import { resolve as resolve4 } from "path";
9321
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
9322
+ import { basename, resolve as resolve4 } from "path";
9307
9323
  import chalk from "chalk";
9324
+ function detectProjectName(dir) {
9325
+ try {
9326
+ const pkg = JSON.parse(readFileSync4(resolve4(dir, "package.json"), "utf-8"));
9327
+ if (pkg.name && typeof pkg.name === "string") {
9328
+ return pkg.name.replace(/^@[^/]+\//, "");
9329
+ }
9330
+ } catch {
9331
+ }
9332
+ return basename(dir);
9333
+ }
9308
9334
  function scaffoldProject(projectName, opts = {}) {
9309
- const baseDir = process.cwd();
9335
+ const baseDir = opts.projectDir ?? process.cwd();
9310
9336
  const log = opts.quiet ? () => {
9311
9337
  } : (msg) => console.log(msg);
9312
9338
  const fathomDir = resolve4(baseDir, ".fathom");
@@ -9390,9 +9416,25 @@ function scaffoldProject(projectName, opts = {}) {
9390
9416
  } else {
9391
9417
  log(chalk.dim(" \xB7 .claude/hooks/te-session-sync.sh (exists)"));
9392
9418
  }
9419
+ const mcpPath = resolve4(baseDir, ".mcp.json");
9420
+ let mcpConfig = { mcpServers: {} };
9421
+ if (existsSync3(mcpPath)) {
9422
+ try {
9423
+ mcpConfig = JSON.parse(readFileSync4(mcpPath, "utf-8"));
9424
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
9425
+ } catch {
9426
+ mcpConfig = { mcpServers: {} };
9427
+ }
9428
+ }
9429
+ mcpConfig.mcpServers.fathom = {
9430
+ command: "npx",
9431
+ args: ["-y", "fathom-mcp@latest", "--project-dir", "."]
9432
+ };
9433
+ writeFileSync3(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n");
9434
+ log(chalk.green(" \u2713 .mcp.json (MCP auto-discovery)"));
9393
9435
  }
9394
- var setupCommand = new Command("setup").description("Scaffold .fathom/ project directory + .claude/ skill + hooks").option("-p, --project <name>", "Project name", "unnamed").action((options) => {
9395
- const projectName = options.project;
9436
+ var setupCommand = new Command("setup").description("Scaffold .fathom/ project directory + .claude/ skill + hooks").option("-p, --project <name>", "Project name").action((options) => {
9437
+ const projectName = options.project ?? detectProjectName(process.cwd());
9396
9438
  console.log(chalk.bold(`
9397
9439
  Fathom \u2014 Setup: ${projectName}`));
9398
9440
  console.log(chalk.dim("\u2500".repeat(50)));
@@ -9450,8 +9492,8 @@ function generateHookScript(projectName) {
9450
9492
 
9451
9493
  // src/commands/intake.ts
9452
9494
  import { Command as Command2 } from "commander";
9453
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, statSync } from "fs";
9454
- import { resolve as resolve5, basename, dirname as dirname3 } from "path";
9495
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, statSync } from "fs";
9496
+ import { resolve as resolve5, basename as basename2, dirname as dirname3 } from "path";
9455
9497
  import chalk3 from "chalk";
9456
9498
  import Table from "cli-table3";
9457
9499
  import { input, confirm, select } from "@inquirer/prompts";
@@ -9975,16 +10017,24 @@ function findLocalMarkdownFiles() {
9975
10017
  async function runIntakeFlow(projectName) {
9976
10018
  const cwd = process.cwd();
9977
10019
  const { rawInput, source } = await resolveInput(cwd);
9978
- if (!process.env.ANTHROPIC_API_KEY) {
9979
- console.error(chalk3.red("ANTHROPIC_API_KEY environment variable is required."));
9980
- console.error(chalk3.dim(" Add it to your .env file or export it:"));
10020
+ const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
10021
+ const hasOpenAIKey = !!process.env.OPENAI_API_KEY;
10022
+ if (!hasAnthropicKey && !hasOpenAIKey) {
10023
+ console.error(chalk3.red("No LLM provider API key found."));
10024
+ console.error(chalk3.dim(" Set one of:"));
9981
10025
  console.error(chalk3.dim(" export ANTHROPIC_API_KEY=sk-ant-..."));
10026
+ console.error(chalk3.dim(" export OPENAI_API_KEY=sk-..."));
9982
10027
  process.exit(1);
9983
10028
  }
9984
- console.log(chalk3.dim("\n Extracting work items via Claude..."));
10029
+ const provider = hasAnthropicKey ? "anthropic" : "openai";
10030
+ console.log(chalk3.dim(`
10031
+ Extracting work items via ${provider === "anthropic" ? "Claude" : "OpenAI"}...`));
9985
10032
  let items;
9986
10033
  try {
9987
- items = await extractWorkItems(rawInput);
10034
+ items = await extractWorkItems(rawInput, {
10035
+ provider,
10036
+ ...provider === "openai" ? { openaiApiKey: process.env.OPENAI_API_KEY } : {}
10037
+ });
9988
10038
  } catch (err) {
9989
10039
  console.error(chalk3.red(" Extraction failed:"), err.message);
9990
10040
  process.exit(1);
@@ -10033,7 +10083,7 @@ async function resolveInput(cwd) {
10033
10083
  { name: "Type/paste text instead", value: "__text__" }
10034
10084
  );
10035
10085
  const chosen = await select({
10036
- message: `Input source (${localFiles.length} file${localFiles.length === 1 ? "" : "s"} found in ${basename(cwd)}):`,
10086
+ message: `Input source (${localFiles.length} file${localFiles.length === 1 ? "" : "s"} found in ${basename2(cwd)}):`,
10037
10087
  choices: fileChoices
10038
10088
  });
10039
10089
  if (chosen === "__text__") {
@@ -10048,9 +10098,9 @@ async function resolveInput(cwd) {
10048
10098
  console.error(chalk3.red(`File not found: ${resolved}`));
10049
10099
  process.exit(1);
10050
10100
  }
10051
- return { rawInput: readFileSync4(resolved, "utf-8"), source: "file" };
10101
+ return { rawInput: readFileSync5(resolved, "utf-8"), source: "file" };
10052
10102
  } else {
10053
- return { rawInput: readFileSync4(resolve5(chosen), "utf-8"), source: "file" };
10103
+ return { rawInput: readFileSync5(resolve5(chosen), "utf-8"), source: "file" };
10054
10104
  }
10055
10105
  }
10056
10106
  const inputType = await select({
@@ -10067,7 +10117,7 @@ async function resolveInput(cwd) {
10067
10117
  console.error(chalk3.red(`File not found: ${resolved}`));
10068
10118
  process.exit(1);
10069
10119
  }
10070
- return { rawInput: readFileSync4(resolved, "utf-8"), source: "file" };
10120
+ return { rawInput: readFileSync5(resolved, "utf-8"), source: "file" };
10071
10121
  }
10072
10122
  return {
10073
10123
  rawInput: await input({ message: "Paste your feedback / requirements:" }),
@@ -10110,14 +10160,14 @@ function displayIntakeTable(projectName, slices, totalTokens, totalCost) {
10110
10160
  Total: ${totalTokens.toLocaleString()} tokens \u2014 $${totalCost.toFixed(2)}`)
10111
10161
  );
10112
10162
  }
10113
- var intakeCommand = new Command2("intake").description("Extract work items from raw feedback, bug reports, or meeting notes").argument("[text]", "Raw text input to analyze").option("-f, --file <path>", "Read input from a file").option("-p, --project <name>", "Project name").option("-o, --output <path>", "Write markdown spec to file").option("--json", "Output as JSON").option("--markdown", "Output as markdown to stdout").action(async (text3, options) => {
10163
+ var intakeCommand = new Command2("intake").description("Extract work items from raw feedback, bug reports, or meeting notes").argument("[text]", "Raw text input to analyze").option("-f, --file <path>", "Read input from a file").option("-p, --project <name>", "Project name").option("-o, --output <path>", "Write markdown spec to file").option("--json", "Output as JSON").option("--markdown", "Output as markdown to stdout").option("--provider <provider>", "LLM provider (anthropic, openai, auto)", "auto").action(async (text3, options) => {
10114
10164
  try {
10115
10165
  const cwd = process.cwd();
10116
10166
  let projectName = options.project ?? getProjectName();
10117
10167
  if (projectName === "unnamed") {
10118
10168
  projectName = await input({
10119
10169
  message: "Project name:",
10120
- default: basename(cwd)
10170
+ default: basename2(cwd)
10121
10171
  });
10122
10172
  updateProjectConfig({ project: projectName });
10123
10173
  }
@@ -10129,7 +10179,7 @@ var intakeCommand = new Command2("intake").description("Extract work items from
10129
10179
  console.error(chalk3.red(`File not found: ${filePath}`));
10130
10180
  process.exit(1);
10131
10181
  }
10132
- rawInput = readFileSync4(filePath, "utf-8");
10182
+ rawInput = readFileSync5(filePath, "utf-8");
10133
10183
  source = "file";
10134
10184
  } else if (text3) {
10135
10185
  rawInput = text3;
@@ -10146,7 +10196,7 @@ var intakeCommand = new Command2("intake").description("Extract work items from
10146
10196
  { name: "Type/paste text instead", value: "__text__" }
10147
10197
  );
10148
10198
  const chosen = await select({
10149
- message: `Input source (${localFiles.length} file${localFiles.length === 1 ? "" : "s"} found in ${basename(cwd)}):`,
10199
+ message: `Input source (${localFiles.length} file${localFiles.length === 1 ? "" : "s"} found in ${basename2(cwd)}):`,
10150
10200
  choices: fileChoices
10151
10201
  });
10152
10202
  if (chosen === "__text__") {
@@ -10164,10 +10214,10 @@ var intakeCommand = new Command2("intake").description("Extract work items from
10164
10214
  console.error(chalk3.red(`File not found: ${resolved}`));
10165
10215
  process.exit(1);
10166
10216
  }
10167
- rawInput = readFileSync4(resolved, "utf-8");
10217
+ rawInput = readFileSync5(resolved, "utf-8");
10168
10218
  source = "file";
10169
10219
  } else {
10170
- rawInput = readFileSync4(resolve5(chosen), "utf-8");
10220
+ rawInput = readFileSync5(resolve5(chosen), "utf-8");
10171
10221
  source = "file";
10172
10222
  }
10173
10223
  } else {
@@ -10188,7 +10238,7 @@ var intakeCommand = new Command2("intake").description("Extract work items from
10188
10238
  console.error(chalk3.red(`File not found: ${resolved}`));
10189
10239
  process.exit(1);
10190
10240
  }
10191
- rawInput = readFileSync4(resolved, "utf-8");
10241
+ rawInput = readFileSync5(resolved, "utf-8");
10192
10242
  source = "file";
10193
10243
  } else {
10194
10244
  rawInput = await input({
@@ -10198,16 +10248,29 @@ var intakeCommand = new Command2("intake").description("Extract work items from
10198
10248
  }
10199
10249
  }
10200
10250
  }
10201
- if (!process.env.ANTHROPIC_API_KEY) {
10202
- console.error(chalk3.red("ANTHROPIC_API_KEY environment variable is required."));
10203
- console.error(chalk3.dim(" Add it to your .env file or export it:"));
10204
- console.error(chalk3.dim(" export ANTHROPIC_API_KEY=sk-ant-..."));
10205
- process.exit(1);
10251
+ const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
10252
+ const hasOpenAIKey = !!process.env.OPENAI_API_KEY;
10253
+ let provider;
10254
+ if (options.provider && options.provider !== "auto") {
10255
+ provider = options.provider;
10256
+ } else {
10257
+ if (!hasAnthropicKey && !hasOpenAIKey) {
10258
+ console.error(chalk3.red("No LLM provider API key found."));
10259
+ console.error(chalk3.dim(" Set one of:"));
10260
+ console.error(chalk3.dim(" export ANTHROPIC_API_KEY=sk-ant-..."));
10261
+ console.error(chalk3.dim(" export OPENAI_API_KEY=sk-..."));
10262
+ process.exit(1);
10263
+ }
10264
+ provider = hasAnthropicKey ? "anthropic" : "openai";
10206
10265
  }
10207
- console.log(chalk3.dim("\nExtracting work items via Claude..."));
10266
+ console.log(chalk3.dim(`
10267
+ Extracting work items via ${provider === "anthropic" ? "Claude" : "OpenAI"}...`));
10208
10268
  let items;
10209
10269
  try {
10210
- items = await extractWorkItems(rawInput);
10270
+ items = await extractWorkItems(rawInput, {
10271
+ provider,
10272
+ ...provider === "openai" ? { openaiApiKey: process.env.OPENAI_API_KEY } : {}
10273
+ });
10211
10274
  } catch (err) {
10212
10275
  console.error(chalk3.red("Extraction failed:"), err.message);
10213
10276
  process.exit(1);
@@ -10364,7 +10427,7 @@ function writeRegistry(projectName, slices) {
10364
10427
  let existing = [];
10365
10428
  if (existsSync4(registryPath)) {
10366
10429
  try {
10367
- const data = JSON.parse(readFileSync4(registryPath, "utf-8"));
10430
+ const data = JSON.parse(readFileSync5(registryPath, "utf-8"));
10368
10431
  if (Array.isArray(data.features)) {
10369
10432
  const newIds = new Set(features.map((f) => f.id));
10370
10433
  existing = data.features.filter(
@@ -10407,7 +10470,7 @@ async function doConvexSync(result, rawInput, source, projectName) {
10407
10470
  }
10408
10471
 
10409
10472
  // src/build-prompt.ts
10410
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
10473
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync6, existsSync as existsSync5 } from "fs";
10411
10474
  import { resolve as resolve6, dirname as dirname4 } from "path";
10412
10475
  function generateBuildPrompt(opts) {
10413
10476
  const { projectName, workflowState } = opts;
@@ -10415,14 +10478,14 @@ function generateBuildPrompt(opts) {
10415
10478
  let features = [];
10416
10479
  if (existsSync5(regPath)) {
10417
10480
  try {
10418
- const data = JSON.parse(readFileSync5(regPath, "utf-8"));
10481
+ const data = JSON.parse(readFileSync6(regPath, "utf-8"));
10419
10482
  if (Array.isArray(data.features)) features = data.features;
10420
10483
  } catch {
10421
10484
  }
10422
10485
  }
10423
10486
  let specContent = "";
10424
10487
  if (workflowState.specPath && existsSync5(workflowState.specPath)) {
10425
- specContent = readFileSync5(workflowState.specPath, "utf-8");
10488
+ specContent = readFileSync6(workflowState.specPath, "utf-8");
10426
10489
  }
10427
10490
  const queueFeatures = workflowState.buildQueue.map((id) => features.find((f) => f.id === id)).filter((f) => f !== void 0);
10428
10491
  const pct = workflowState.featuresTotal > 0 ? Math.round(workflowState.featuresComplete / workflowState.featuresTotal * 100) : 0;
@@ -10549,38 +10612,70 @@ async function captureIntentFlow(defaultProjectName) {
10549
10612
  choices: VALUE_OPTIONS
10550
10613
  });
10551
10614
  const values = [];
10552
- for (const val of selectedValues) {
10553
- const label = VALUE_OPTIONS.find((o) => o.value === val)?.name ?? val;
10554
- const details = await input2({
10555
- message: `"${label}" \u2014 any specifics? (optional)`
10615
+ let skipRemaining = false;
10616
+ if (selectedValues.length > 0) {
10617
+ const drillDown = await confirm2({
10618
+ message: `Set priorities for each? (${selectedValues.length} selected \u2014 or skip to default all to medium)`,
10619
+ default: false
10556
10620
  });
10557
- const priority = await select2({
10558
- message: `"${label}" \u2014 priority?`,
10559
- choices: [
10560
- { name: "Critical", value: "critical" },
10561
- { name: "High", value: "high" },
10562
- { name: "Medium", value: "medium" },
10563
- { name: "Low", value: "low" }
10564
- ],
10565
- default: "medium"
10566
- });
10567
- values.push({
10568
- name: val,
10569
- description: label,
10570
- priority,
10571
- ...details ? { details } : {}
10572
- });
10573
- }
10574
- let addMore = selectedValues.length === 0;
10575
- if (!addMore && selectedValues.length > 0) {
10576
- addMore = await confirm2({ message: "Add any custom values not in the list?", default: false });
10621
+ if (drillDown) {
10622
+ for (const val of selectedValues) {
10623
+ if (skipRemaining) {
10624
+ const label2 = VALUE_OPTIONS.find((o) => o.value === val)?.name ?? val;
10625
+ values.push({ name: val, description: label2, priority: "medium" });
10626
+ continue;
10627
+ }
10628
+ const label = VALUE_OPTIONS.find((o) => o.value === val)?.name ?? val;
10629
+ const details = await input2({
10630
+ message: `"${label}" \u2014 any specifics? (enter to skip, "done" to skip remaining)`
10631
+ });
10632
+ if (details.toLowerCase() === "done") {
10633
+ values.push({ name: val, description: label, priority: "medium" });
10634
+ skipRemaining = true;
10635
+ continue;
10636
+ }
10637
+ const priority = await select2({
10638
+ message: `"${label}" \u2014 priority?`,
10639
+ choices: [
10640
+ { name: "Critical", value: "critical" },
10641
+ { name: "High", value: "high" },
10642
+ { name: "Medium", value: "medium" },
10643
+ { name: "Low", value: "low" },
10644
+ { name: "Skip remaining (default medium)", value: "skip" }
10645
+ ],
10646
+ default: "medium"
10647
+ });
10648
+ if (priority === "skip") {
10649
+ values.push({ name: val, description: label, priority: "medium", ...details ? { details } : {} });
10650
+ skipRemaining = true;
10651
+ continue;
10652
+ }
10653
+ values.push({
10654
+ name: val,
10655
+ description: label,
10656
+ priority,
10657
+ ...details ? { details } : {}
10658
+ });
10659
+ }
10660
+ } else {
10661
+ for (const val of selectedValues) {
10662
+ const label = VALUE_OPTIONS.find((o) => o.value === val)?.name ?? val;
10663
+ values.push({ name: val, description: label, priority: "medium" });
10664
+ }
10665
+ }
10577
10666
  }
10578
- while (addMore) {
10579
- const customName = await input2({ message: "Value name (or press enter to finish):" });
10580
- if (!customName) break;
10581
- const customDesc = await input2({ message: `Description for "${customName}":` });
10582
- values.push({ name: customName, description: customDesc, priority: "medium" });
10583
- addMore = await confirm2({ message: "Add another?", default: false });
10667
+ if (!skipRemaining) {
10668
+ let addMore = selectedValues.length === 0;
10669
+ if (!addMore && selectedValues.length > 0) {
10670
+ addMore = await confirm2({ message: "Add any custom values not in the list?", default: false });
10671
+ }
10672
+ while (addMore) {
10673
+ const customName = await input2({ message: "Value name (or press enter to finish):" });
10674
+ if (!customName) break;
10675
+ const customDesc = await input2({ message: `Description for "${customName}":` });
10676
+ values.push({ name: customName, description: customDesc, priority: "medium" });
10677
+ addMore = await confirm2({ message: "Add another?", default: false });
10678
+ }
10584
10679
  }
10585
10680
  const wantBudget = await confirm2({ message: "Set a monthly AI budget? (helps with model routing)", default: false });
10586
10681
  let budget;
@@ -10597,14 +10692,14 @@ async function captureIntentFlow(defaultProjectName) {
10597
10692
  let guardrails;
10598
10693
  if (wantGuardrails) {
10599
10694
  const templates = await checkbox({
10600
- message: "Security templates (select all that apply):",
10695
+ message: "Guardrail templates (select all that apply):",
10601
10696
  choices: [
10602
- { name: "OWASP Top 10 (web security)", value: "owasp-top-10" },
10603
- { name: "WCAG AA (accessibility)", value: "wcag-aa" },
10604
- { name: "PCI Basic (payment data) \u2014 coming soon", value: "pci-basic" },
10605
- { name: "HIPAA Basic (health data) \u2014 coming soon", value: "hipaa-basic" },
10606
- { name: "GDPR Basic (privacy) \u2014 coming soon", value: "gdpr-basic" },
10607
- { name: "SOC2 Basic (security/availability) \u2014 coming soon", value: "soc2-basic" }
10697
+ { name: "OWASP Top 10 \u2014 web security (injection, XSS, auth, CSRF, etc.)", value: "owasp-top-10" },
10698
+ { name: "WCAG AA \u2014 accessibility (contrast, keyboard, screen readers, etc.)", value: "wcag-aa" },
10699
+ { name: "PCI DSS \u2014 payment card data (encryption, tokenization, access control)", value: "pci-basic" },
10700
+ { name: "HIPAA \u2014 health data (PHI protection, audit trails, encryption)", value: "hipaa-basic" },
10701
+ { name: "GDPR \u2014 data privacy (consent, deletion rights, data minimization)", value: "gdpr-basic" },
10702
+ { name: "SOC 2 \u2014 security + availability (MFA, logging, incident response)", value: "soc2-basic" }
10608
10703
  ]
10609
10704
  });
10610
10705
  const customRulesStr = await input2({
@@ -10644,6 +10739,67 @@ function printProgress(state) {
10644
10739
  )
10645
10740
  );
10646
10741
  }
10742
+ async function runSettingsFlow(projectName) {
10743
+ let done = false;
10744
+ while (!done) {
10745
+ const setting = await select2({
10746
+ message: "What would you like to update?",
10747
+ choices: [
10748
+ { name: "Re-capture project intent", value: "intent" },
10749
+ { name: "Update budget", value: "budget" },
10750
+ { name: "Reset build mode preference", value: "build-mode" },
10751
+ { name: "Back", value: "back" }
10752
+ ]
10753
+ });
10754
+ switch (setting) {
10755
+ case "intent": {
10756
+ const intentData = await captureIntentFlow(projectName);
10757
+ const { saveIntent } = await import("./dist-XKZLNUDU.js");
10758
+ await saveIntent(process.cwd(), intentData);
10759
+ const { writeProjections } = await import("./dist-KXBSLOHP.js");
10760
+ const files = await writeProjections(process.cwd(), intentData);
10761
+ console.log(chalk4.green(`
10762
+ \u2713 Intent saved to .fathom/intent.yaml`));
10763
+ console.log(chalk4.green(` \u2713 Generated ${files.length} projection files`));
10764
+ console.log();
10765
+ break;
10766
+ }
10767
+ case "budget": {
10768
+ const { loadIntent, saveIntent } = await import("./dist-XKZLNUDU.js");
10769
+ let existing;
10770
+ try {
10771
+ existing = await loadIntent(process.cwd());
10772
+ } catch {
10773
+ console.log(chalk4.yellow(" No intent found. Run intent capture first."));
10774
+ break;
10775
+ }
10776
+ const limitStr = await input2({ message: "Monthly budget (USD):", default: String(existing.budget?.monthly_limit ?? 200) });
10777
+ const limit = parseFloat(limitStr) || 200;
10778
+ const thresholdStr = await input2({ message: "Alert threshold (0-1):", default: String(existing.budget?.alert_threshold ?? 0.8) });
10779
+ const threshold = parseFloat(thresholdStr) || 0.8;
10780
+ existing.budget = {
10781
+ monthly_limit: limit,
10782
+ alert_threshold: threshold,
10783
+ prefer_batch: existing.budget?.prefer_batch ?? false,
10784
+ currency: existing.budget?.currency ?? "USD"
10785
+ };
10786
+ await saveIntent(process.cwd(), existing);
10787
+ console.log(chalk4.green(`
10788
+ \u2713 Budget updated: $${limit}/month, alert at ${(threshold * 100).toFixed(0)}%
10789
+ `));
10790
+ break;
10791
+ }
10792
+ case "build-mode": {
10793
+ updateProjectConfig({ buildMode: void 0 });
10794
+ console.log(chalk4.green("\n \u2713 Build mode preference cleared. You'll be asked next time.\n"));
10795
+ break;
10796
+ }
10797
+ case "back":
10798
+ done = true;
10799
+ break;
10800
+ }
10801
+ }
10802
+ }
10647
10803
  var goCommand = new Command3("go").description("Run the full workflow: intake \u2192 build \u2192 review").option("--from <phase>", "Start from a specific phase (intake, build, review)").option("--auto", "Skip confirmation prompts").action(async (options) => {
10648
10804
  try {
10649
10805
  const projectState = detectProjectState();
@@ -10683,7 +10839,7 @@ var goCommand = new Command3("go").description("Run the full workflow: intake \u
10683
10839
  console.log();
10684
10840
  console.log(chalk4.dim(" No project detected. Let's set one up."));
10685
10841
  console.log();
10686
- const intentData = await captureIntentFlow(basename2(process.cwd()));
10842
+ const intentData = await captureIntentFlow(basename3(process.cwd()));
10687
10843
  projectName = intentData.project;
10688
10844
  const { saveIntent } = await import("./dist-XKZLNUDU.js");
10689
10845
  await saveIntent(process.cwd(), intentData);
@@ -10721,6 +10877,7 @@ var goCommand = new Command3("go").description("Run the full workflow: intake \u
10721
10877
  },
10722
10878
  { name: "New intake \u2014 add more work", value: "intake" },
10723
10879
  { name: "Review \u2014 reconcile recent sessions", value: "review" },
10880
+ { name: "Update intent / project settings", value: "settings" },
10724
10881
  { name: "Status overview", value: "status" }
10725
10882
  ]
10726
10883
  });
@@ -10728,6 +10885,10 @@ var goCommand = new Command3("go").description("Run the full workflow: intake \u
10728
10885
  printStatus(workflowState);
10729
10886
  return;
10730
10887
  }
10888
+ if (action === "settings") {
10889
+ await runSettingsFlow(projectName);
10890
+ return;
10891
+ }
10731
10892
  startPhase = action;
10732
10893
  } else {
10733
10894
  startPhase = "intake";
@@ -10919,8 +11080,8 @@ async function executeBuildMode(mode, promptPath, state) {
10919
11080
  console.log(chalk4.dim(`
10920
11081
  Creating worktree: ${branchName}`));
10921
11082
  try {
10922
- const { execSync } = await import("child_process");
10923
- execSync(`git worktree add "${worktreePath}" -b "${branchName}"`, {
11083
+ const { execSync: execSync2 } = await import("child_process");
11084
+ execSync2(`git worktree add "${worktreePath}" -b "${branchName}"`, {
10924
11085
  stdio: "pipe",
10925
11086
  cwd: process.cwd()
10926
11087
  });
@@ -10952,8 +11113,8 @@ async function executeBuildMode(mode, promptPath, state) {
10952
11113
  return false;
10953
11114
  }
10954
11115
  function launchClaude(promptPath, cwd, firstFeature) {
10955
- return new Promise((resolve23) => {
10956
- const promptContent = readFileSync6(promptPath, "utf-8");
11116
+ return new Promise((resolve25) => {
11117
+ const promptContent = readFileSync7(promptPath, "utf-8");
10957
11118
  const initialPrompt = firstFeature ? `Read the build context in your system prompt. Start working on feature \`${firstFeature}\` \u2014 read the spec reference, plan the implementation, and begin.` : "Read the build context in your system prompt and start working on the first feature in the priority queue.";
10958
11119
  const child = spawn("claude", ["--append-system-prompt", promptContent, initialPrompt], {
10959
11120
  stdio: "inherit",
@@ -10969,11 +11130,11 @@ function launchClaude(promptPath, cwd, firstFeature) {
10969
11130
  } else {
10970
11131
  console.error(chalk4.red(` Failed to launch Claude Code: ${err.message}`));
10971
11132
  }
10972
- resolve23();
11133
+ resolve25();
10973
11134
  });
10974
11135
  child.on("close", () => {
10975
11136
  process.removeListener("SIGINT", sigintHandler);
10976
- resolve23();
11137
+ resolve25();
10977
11138
  });
10978
11139
  });
10979
11140
  }
@@ -10999,7 +11160,7 @@ function printStatus(state) {
10999
11160
 
11000
11161
  // src/commands/analyze.ts
11001
11162
  import { Command as Command4 } from "commander";
11002
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, existsSync as existsSync7 } from "fs";
11163
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, existsSync as existsSync7 } from "fs";
11003
11164
  import { resolve as resolve8 } from "path";
11004
11165
  import chalk5 from "chalk";
11005
11166
  import Table2 from "cli-table3";
@@ -11009,7 +11170,7 @@ var analyzeCommand = new Command4("analyze").description("Analyze a spec and gen
11009
11170
  console.error(chalk5.red(`Spec file not found: ${fullPath}`));
11010
11171
  process.exit(1);
11011
11172
  }
11012
- const content3 = readFileSync7(fullPath, "utf-8");
11173
+ const content3 = readFileSync8(fullPath, "utf-8");
11013
11174
  const data = loadData();
11014
11175
  console.log(chalk5.dim("Parsing spec..."));
11015
11176
  const spec = parseSpec(content3);
@@ -11179,7 +11340,7 @@ var pricingCommand = new Command5("pricing").description("Show current model pri
11179
11340
 
11180
11341
  // src/commands/status.ts
11181
11342
  import { Command as Command6 } from "commander";
11182
- import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
11343
+ import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
11183
11344
  import { resolve as resolve9 } from "path";
11184
11345
  import chalk7 from "chalk";
11185
11346
  import Table4 from "cli-table3";
@@ -11193,7 +11354,7 @@ var statusCommand = new Command6("status").description("Show project status from
11193
11354
  );
11194
11355
  return;
11195
11356
  }
11196
- const registry = JSON.parse(readFileSync8(registryPath, "utf-8"));
11357
+ const registry = JSON.parse(readFileSync9(registryPath, "utf-8"));
11197
11358
  const projectName = options.project ?? registry.project ?? getProjectName();
11198
11359
  const summaryPath = resolve9(
11199
11360
  process.cwd(),
@@ -11205,7 +11366,7 @@ var statusCommand = new Command6("status").description("Show project status from
11205
11366
  const hasTracking = existsSync8(summaryPath);
11206
11367
  const actuals = /* @__PURE__ */ new Map();
11207
11368
  if (hasTracking) {
11208
- const summary = JSON.parse(readFileSync8(summaryPath, "utf-8"));
11369
+ const summary = JSON.parse(readFileSync9(summaryPath, "utf-8"));
11209
11370
  for (const s of summary.sessions) {
11210
11371
  if (!s.featureId || s.status === "untagged") continue;
11211
11372
  const existing = actuals.get(s.featureId) ?? { tokens: 0, sessions: 0 };
@@ -11268,14 +11429,14 @@ Total estimated: ${totalEstimated.toLocaleString()} tokens`));
11268
11429
 
11269
11430
  // src/commands/track.ts
11270
11431
  import { Command as Command7 } from "commander";
11271
- import { readFileSync as readFileSync10, existsSync as existsSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync8 } from "fs";
11432
+ import { readFileSync as readFileSync11, existsSync as existsSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync8 } from "fs";
11272
11433
  import { resolve as resolve10 } from "path";
11273
11434
  import chalk8 from "chalk";
11274
11435
  import Table5 from "cli-table3";
11275
11436
 
11276
11437
  // ../tracker/dist/index.js
11277
- import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync9 } from "fs";
11278
- import { join, basename as basename3 } from "path";
11438
+ import { readFileSync as readFileSync10, readdirSync as readdirSync2, existsSync as existsSync9 } from "fs";
11439
+ import { join, basename as basename4 } from "path";
11279
11440
  import { homedir } from "os";
11280
11441
  import { countTokens } from "@anthropic-ai/tokenizer";
11281
11442
  import { readFileSync as readFileSync22, existsSync as existsSync22 } from "fs";
@@ -11334,7 +11495,7 @@ var Registry = external_exports.object({
11334
11495
  generatedAt: external_exports.string()
11335
11496
  });
11336
11497
  function parseSessionFile(filePath) {
11337
- const content3 = readFileSync9(filePath, "utf-8");
11498
+ const content3 = readFileSync10(filePath, "utf-8");
11338
11499
  const lines = content3.trim().split("\n").filter(Boolean);
11339
11500
  if (lines.length === 0) return null;
11340
11501
  let inputTokens = 0;
@@ -11407,7 +11568,7 @@ function parseSessionFile(filePath) {
11407
11568
  if (typeof text3 === "string") bucket.push(text3);
11408
11569
  }
11409
11570
  }
11410
- const sessionId = basename3(filePath, ".jsonl");
11571
+ const sessionId = basename4(filePath, ".jsonl");
11411
11572
  const activeMinutes = sessionStart && sessionEnd ? (sessionEnd - sessionStart) / 6e4 : void 0;
11412
11573
  const allParts = [...inputParts, ...outputParts, ...conversationParts];
11413
11574
  const conversationText = allParts.join("\n");
@@ -11683,7 +11844,7 @@ var trackCommand = new Command7("track").description("Import Claude Code session
11683
11844
  return;
11684
11845
  }
11685
11846
  const registry = JSON.parse(
11686
- readFileSync10(registryPath, "utf-8")
11847
+ readFileSync11(registryPath, "utf-8")
11687
11848
  );
11688
11849
  const projectName = options.project ?? registry.project ?? getProjectName();
11689
11850
  console.log(chalk8.dim("Scanning for sessions..."));
@@ -11710,7 +11871,7 @@ var trackCommand = new Command7("track").description("Import Claude Code session
11710
11871
  );
11711
11872
  const trackedIds = /* @__PURE__ */ new Set();
11712
11873
  if (!options.backfill && existsSync10(summaryPath)) {
11713
- const summary = JSON.parse(readFileSync10(summaryPath, "utf-8"));
11874
+ const summary = JSON.parse(readFileSync11(summaryPath, "utf-8"));
11714
11875
  for (const s of summary.sessions ?? []) {
11715
11876
  trackedIds.add(s.sessionId);
11716
11877
  }
@@ -11810,7 +11971,7 @@ Total: ${totalTokens.toLocaleString()} tokens across ${results.length} sessions`
11810
11971
  }));
11811
11972
  let existing = { sessions: [] };
11812
11973
  if (existsSync10(summaryPath)) {
11813
- existing = JSON.parse(readFileSync10(summaryPath, "utf-8"));
11974
+ existing = JSON.parse(readFileSync11(summaryPath, "utf-8"));
11814
11975
  }
11815
11976
  existing.sessions.push(...sessionRecords);
11816
11977
  writeFileSync7(
@@ -11863,7 +12024,7 @@ Tracking saved: ${summaryPath}`));
11863
12024
 
11864
12025
  // src/commands/reconcile.ts
11865
12026
  import { Command as Command8 } from "commander";
11866
- import { readFileSync as readFileSync11, existsSync as existsSync11 } from "fs";
12027
+ import { readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
11867
12028
  import { resolve as resolve11 } from "path";
11868
12029
  import chalk9 from "chalk";
11869
12030
  import Table6 from "cli-table3";
@@ -11892,8 +12053,8 @@ var reconcileCommand = new Command8("reconcile").description("Compare estimates
11892
12053
  );
11893
12054
  return;
11894
12055
  }
11895
- const registry = JSON.parse(readFileSync11(registryPath, "utf-8"));
11896
- const summary = JSON.parse(readFileSync11(summaryPath, "utf-8"));
12056
+ const registry = JSON.parse(readFileSync12(registryPath, "utf-8"));
12057
+ const summary = JSON.parse(readFileSync12(summaryPath, "utf-8"));
11897
12058
  const projectName = options.project ?? registry.project ?? getProjectName();
11898
12059
  const data = loadData();
11899
12060
  const actuals = /* @__PURE__ */ new Map();
@@ -12068,7 +12229,7 @@ Overall: ${totalActual.toLocaleString()} / ${totalEstimated.toLocaleString()} to
12068
12229
 
12069
12230
  // src/commands/calibrate.ts
12070
12231
  import { Command as Command9 } from "commander";
12071
- import { readFileSync as readFileSync12, existsSync as existsSync12 } from "fs";
12232
+ import { readFileSync as readFileSync13, existsSync as existsSync12 } from "fs";
12072
12233
  import { resolve as resolve12 } from "path";
12073
12234
  import chalk10 from "chalk";
12074
12235
  import Table7 from "cli-table3";
@@ -12089,8 +12250,8 @@ var calibrateCommand = new Command9("calibrate").description("Show calibration d
12089
12250
  );
12090
12251
  return;
12091
12252
  }
12092
- const registry = JSON.parse(readFileSync12(registryPath, "utf-8"));
12093
- const summary = JSON.parse(readFileSync12(summaryPath, "utf-8"));
12253
+ const registry = JSON.parse(readFileSync13(registryPath, "utf-8"));
12254
+ const summary = JSON.parse(readFileSync13(summaryPath, "utf-8"));
12094
12255
  const projectName = options.project ?? registry.project ?? getProjectName();
12095
12256
  const actuals = /* @__PURE__ */ new Map();
12096
12257
  for (const session of summary.sessions) {
@@ -12200,7 +12361,7 @@ Calibration summary (${drifts.length} features with data):`));
12200
12361
  // src/commands/estimate.ts
12201
12362
  import { Command as Command10 } from "commander";
12202
12363
  import chalk11 from "chalk";
12203
- var estimateCommand = new Command10("estimate").description("Quick single-feature estimate from a description").argument("<description>", "Feature description").option("-c, --complexity <size>", "Override complexity (S/M/L/XL)").option("--category <cat>", "Feature category", "general").option("--no-ai", "Skip AI complexity scoring, use heuristic only").action(
12364
+ var estimateCommand = new Command10("estimate").description("Quick single-feature estimate from a description").argument("<description>", "Feature description").option("-c, --complexity <size>", "Override complexity (S/M/L/XL)").option("--category <cat>", "Feature category", "general").option("--no-ai", "Skip AI complexity scoring, use heuristic only").option("--provider <provider>", "LLM provider for AI scoring (anthropic, openai)").action(
12204
12365
  async (description, options) => {
12205
12366
  const data = loadData();
12206
12367
  const feature = {
@@ -12217,7 +12378,7 @@ var estimateCommand = new Command10("estimate").description("Quick single-featur
12217
12378
  if (options.complexity) {
12218
12379
  complexitySource = "manual";
12219
12380
  } else if (options.ai !== false) {
12220
- const aiResult = await scoreComplexityAI(description);
12381
+ const aiResult = await scoreComplexityAI(description, { provider: options.provider });
12221
12382
  if (aiResult) {
12222
12383
  feature.complexity = aiResult.complexity;
12223
12384
  feature.dependencies = aiResult.dependencies;
@@ -12228,7 +12389,7 @@ var estimateCommand = new Command10("estimate").description("Quick single-featur
12228
12389
  feature.complexity = scoreComplexity(feature);
12229
12390
  console.log(
12230
12391
  chalk11.dim(
12231
- " (AI scoring unavailable \u2014 using heuristic. Set ANTHROPIC_API_KEY for richer estimates)\n"
12392
+ " (AI scoring unavailable \u2014 using heuristic. Set ANTHROPIC_API_KEY or OPENAI_API_KEY for richer estimates)\n"
12232
12393
  )
12233
12394
  );
12234
12395
  }
@@ -12284,7 +12445,7 @@ var estimateCommand = new Command10("estimate").description("Quick single-featur
12284
12445
 
12285
12446
  // src/commands/validate.ts
12286
12447
  import { Command as Command11 } from "commander";
12287
- import { readFileSync as readFileSync13, existsSync as existsSync13 } from "fs";
12448
+ import { readFileSync as readFileSync14, existsSync as existsSync13 } from "fs";
12288
12449
  import { resolve as resolve13 } from "path";
12289
12450
  import chalk12 from "chalk";
12290
12451
  var validateCommand = new Command11("validate").description("Validate a spec file format without running full analysis").argument("<spec>", "Path to the spec markdown file").action((specPath) => {
@@ -12293,7 +12454,7 @@ var validateCommand = new Command11("validate").description("Validate a spec fil
12293
12454
  console.error(chalk12.red(`Spec file not found: ${fullPath}`));
12294
12455
  process.exit(1);
12295
12456
  }
12296
- const content3 = readFileSync13(fullPath, "utf-8");
12457
+ const content3 = readFileSync14(fullPath, "utf-8");
12297
12458
  console.log(chalk12.bold("\nFathom \u2014 Validate Spec"));
12298
12459
  console.log(chalk12.dim("\u2500".repeat(50)));
12299
12460
  console.log(chalk12.dim(`File: ${fullPath}`));
@@ -12362,7 +12523,7 @@ var validateCommand = new Command11("validate").description("Validate a spec fil
12362
12523
 
12363
12524
  // src/commands/velocity.ts
12364
12525
  import { Command as Command12 } from "commander";
12365
- import { readFileSync as readFileSync14, existsSync as existsSync14 } from "fs";
12526
+ import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
12366
12527
  import { resolve as resolve14 } from "path";
12367
12528
  import chalk13 from "chalk";
12368
12529
  import Table8 from "cli-table3";
@@ -12685,8 +12846,8 @@ var velocityCommand = new Command12("velocity").description("Show velocity metri
12685
12846
  );
12686
12847
  return;
12687
12848
  }
12688
- const registry = JSON.parse(readFileSync14(registryPath, "utf-8"));
12689
- const summary = JSON.parse(readFileSync14(summaryPath, "utf-8"));
12849
+ const registry = JSON.parse(readFileSync15(registryPath, "utf-8"));
12850
+ const summary = JSON.parse(readFileSync15(summaryPath, "utf-8"));
12690
12851
  const projectName = options.project ?? registry.project ?? getProjectName();
12691
12852
  const data = loadData();
12692
12853
  const featureDataMap = /* @__PURE__ */ new Map();
@@ -12823,10 +12984,24 @@ Fathom \u2014 Velocity: ${projectName}`));
12823
12984
 
12824
12985
  // src/commands/init.ts
12825
12986
  import { Command as Command13 } from "commander";
12826
- import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync9, existsSync as existsSync15 } from "fs";
12827
- import { resolve as resolve15 } from "path";
12987
+ import {
12988
+ writeFileSync as writeFileSync8,
12989
+ mkdirSync as mkdirSync9,
12990
+ existsSync as existsSync15,
12991
+ readFileSync as readFileSync16,
12992
+ readdirSync as readdirSync3
12993
+ } from "fs";
12994
+ import { resolve as resolve15, dirname as dirname6 } from "path";
12995
+ import { fileURLToPath as fileURLToPath2 } from "url";
12996
+ import { execSync } from "child_process";
12828
12997
  import chalk14 from "chalk";
12829
- var initCommand = new Command13("init").description("Initialize Fathom global config").option("--convex-url <url>", "Convex deployment URL").option("--team <name>", "Team name").option("--admin-key <key>", "Anthropic Admin API key (sk-ant-admin...)").action((options) => {
12998
+ import { confirm as confirm3 } from "@inquirer/prompts";
12999
+ var initCommand = new Command13("init").description("Initialize Fathom: create global config and scaffold project directory").option("--convex-url <url>", "Convex deployment URL").option("--team <name>", "Team name").option("--admin-key <key>", "Anthropic Admin API key (sk-ant-admin...)").option("--project <name>", "Project name (auto-detected from package.json if omitted)").option("--project-dir <dir>", "Project directory (defaults to current directory)").action(async (options) => {
13000
+ const projectDir = options.projectDir ? resolve15(options.projectDir) : process.cwd();
13001
+ const projectName = options.project ?? detectProjectName(projectDir);
13002
+ console.log(chalk14.bold(`
13003
+ Fathom \u2014 Init: ${projectName}`));
13004
+ console.log(chalk14.dim("\u2500".repeat(50)));
12830
13005
  const configDir = resolve15(
12831
13006
  process.env.HOME ?? process.env.USERPROFILE ?? ".",
12832
13007
  ".config",
@@ -12837,9 +13012,7 @@ var initCommand = new Command13("init").description("Initialize Fathom global co
12837
13012
  let existing = {};
12838
13013
  if (existsSync15(configPath)) {
12839
13014
  try {
12840
- existing = JSON.parse(
12841
- __require("fs").readFileSync(configPath, "utf-8")
12842
- );
13015
+ existing = JSON.parse(readFileSync16(configPath, "utf-8"));
12843
13016
  } catch {
12844
13017
  }
12845
13018
  }
@@ -12851,29 +13024,80 @@ var initCommand = new Command13("init").description("Initialize Fathom global co
12851
13024
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
12852
13025
  };
12853
13026
  writeFileSync8(configPath, JSON.stringify(config, null, 2));
12854
- console.log(chalk14.bold("\nFathom \u2014 Init"));
12855
- console.log(chalk14.dim("\u2500".repeat(40)));
13027
+ console.log(chalk14.green(` \u2713 Global config: ${configPath}`));
12856
13028
  if (options.convexUrl) {
12857
- console.log(chalk14.green(` \u2713 Convex URL: ${options.convexUrl}`));
13029
+ console.log(chalk14.dim(` Convex URL: ${options.convexUrl}`));
12858
13030
  }
12859
13031
  if (options.team) {
12860
- console.log(chalk14.green(` \u2713 Team: ${options.team}`));
13032
+ console.log(chalk14.dim(` Team: ${options.team}`));
12861
13033
  }
12862
- if (options.adminKey) {
12863
- console.log(chalk14.green(` \u2713 Admin API key: ${options.adminKey.slice(0, 16)}...`));
13034
+ console.log("");
13035
+ scaffoldProject(projectName, { projectDir });
13036
+ let claudeDetected = false;
13037
+ try {
13038
+ execSync("which claude", { stdio: "ignore" });
13039
+ claudeDetected = true;
13040
+ } catch {
12864
13041
  }
12865
- console.log(chalk14.dim(`
12866
- Config saved: ${configPath}`));
12867
- console.log(
12868
- chalk14.dim(
12869
- " Set CONVEX_URL env var or pass --convex-url to enable sync.\n"
12870
- )
12871
- );
13042
+ if (claudeDetected) {
13043
+ console.log("");
13044
+ console.log(chalk14.cyan(" Claude Code detected!"));
13045
+ const installPlugin = await confirm3({
13046
+ message: "Install Fathom plugin for Claude Code? (adds skills + MCP config)",
13047
+ default: true
13048
+ });
13049
+ if (installPlugin) {
13050
+ const __dirname2 = dirname6(fileURLToPath2(import.meta.url));
13051
+ let pluginSource = resolve15(__dirname2, "../plugins/fathom");
13052
+ if (!existsSync15(pluginSource)) {
13053
+ pluginSource = resolve15(__dirname2, "../../plugins/fathom");
13054
+ }
13055
+ const skillsSource = resolve15(pluginSource, "skills");
13056
+ if (existsSync15(skillsSource)) {
13057
+ const skillDirs = readdirSync3(skillsSource, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
13058
+ for (const skillDir of skillDirs) {
13059
+ const src = resolve15(skillsSource, skillDir, "SKILL.md");
13060
+ if (!existsSync15(src)) continue;
13061
+ const destDir = resolve15(
13062
+ projectDir,
13063
+ ".claude",
13064
+ "skills",
13065
+ "fathom",
13066
+ skillDir
13067
+ );
13068
+ mkdirSync9(destDir, { recursive: true });
13069
+ writeFileSync8(
13070
+ resolve15(destDir, "SKILL.md"),
13071
+ readFileSync16(src, "utf-8")
13072
+ );
13073
+ }
13074
+ console.log(chalk14.green(" \u2713 Claude Code skills installed"));
13075
+ }
13076
+ const pluginJsonSrc = resolve15(
13077
+ pluginSource,
13078
+ ".claude-plugin",
13079
+ "plugin.json"
13080
+ );
13081
+ if (existsSync15(pluginJsonSrc)) {
13082
+ const destDir = resolve15(projectDir, ".claude-plugin");
13083
+ mkdirSync9(destDir, { recursive: true });
13084
+ writeFileSync8(
13085
+ resolve15(destDir, "plugin.json"),
13086
+ readFileSync16(pluginJsonSrc, "utf-8")
13087
+ );
13088
+ console.log(chalk14.green(" \u2713 Plugin manifest installed"));
13089
+ }
13090
+ }
13091
+ }
13092
+ console.log(chalk14.bold(`
13093
+ \u2713 Fathom initialized for "${projectName}"`));
13094
+ console.log(chalk14.dim(" Next: git add .fathom && git commit"));
13095
+ console.log(chalk14.dim(" Then: fathom analyze (to build feature registry)\n"));
12872
13096
  });
12873
13097
 
12874
13098
  // src/commands/rename.ts
12875
13099
  import { Command as Command14 } from "commander";
12876
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync9, existsSync as existsSync16 } from "fs";
13100
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync9, existsSync as existsSync16 } from "fs";
12877
13101
  import { resolve as resolve16 } from "path";
12878
13102
  import chalk15 from "chalk";
12879
13103
  var renameCommand = new Command14("rename").description("Rename the project in all .claude/te/ config files").argument("<name>", "New project name").action((name) => {
@@ -12894,7 +13118,7 @@ var renameCommand = new Command14("rename").description("Rename the project in a
12894
13118
  let updated = 0;
12895
13119
  for (const filePath of jsonFiles) {
12896
13120
  if (!existsSync16(filePath)) continue;
12897
- const data = JSON.parse(readFileSync15(filePath, "utf-8"));
13121
+ const data = JSON.parse(readFileSync17(filePath, "utf-8"));
12898
13122
  if (data.project) {
12899
13123
  oldName = data.project;
12900
13124
  data.project = name;
@@ -12904,7 +13128,7 @@ var renameCommand = new Command14("rename").description("Rename the project in a
12904
13128
  }
12905
13129
  const skillPath = resolve16(baseDir, ".claude", "skills", "fathom", "SKILL.md");
12906
13130
  if (existsSync16(skillPath)) {
12907
- const content3 = readFileSync15(skillPath, "utf-8");
13131
+ const content3 = readFileSync17(skillPath, "utf-8");
12908
13132
  const newContent = content3.replaceAll(oldName, name);
12909
13133
  if (newContent !== content3) {
12910
13134
  writeFileSync9(skillPath, newContent);
@@ -12913,7 +13137,7 @@ var renameCommand = new Command14("rename").description("Rename the project in a
12913
13137
  }
12914
13138
  const hookPath = resolve16(baseDir, ".claude", "hooks", "te-session-sync.sh");
12915
13139
  if (existsSync16(hookPath)) {
12916
- const content3 = readFileSync15(hookPath, "utf-8");
13140
+ const content3 = readFileSync17(hookPath, "utf-8");
12917
13141
  const newContent = content3.replaceAll(oldName, name);
12918
13142
  if (newContent !== content3) {
12919
13143
  writeFileSync9(hookPath, newContent);
@@ -12929,26 +13153,485 @@ var renameCommand = new Command14("rename").description("Rename the project in a
12929
13153
 
12930
13154
  // src/commands/research.ts
12931
13155
  import { Command as Command15 } from "commander";
13156
+ import { writeFileSync as writeFileSync10, existsSync as existsSync17 } from "fs";
13157
+ import { resolve as resolve17 } from "path";
12932
13158
  import chalk16 from "chalk";
12933
- var researchCommand = new Command15("research").description("Check for pricing updates and intelligence improvements").action(async () => {
12934
- console.log(chalk16.bold("\nFathom \u2014 Research Pulse"));
12935
- console.log(chalk16.dim("\u2500".repeat(50)));
12936
- console.log(chalk16.dim(" Checking for updates...\n"));
12937
- console.log(chalk16.yellow(" Research pulse not yet implemented."));
12938
- console.log(chalk16.dim(" This will check for model pricing changes, new guardrail templates,"));
12939
- console.log(chalk16.dim(" and community-calibrated task profiles.\n"));
13159
+
13160
+ // src/fetchers/types.ts
13161
+ var RemoteManifest = external_exports.object({
13162
+ version: external_exports.number(),
13163
+ updated: external_exports.string(),
13164
+ models: external_exports.array(ModelPricing),
13165
+ recommendations: external_exports.record(external_exports.string(), AgentRecommendation)
13166
+ });
13167
+ var IntelligenceSuggestion = external_exports.object({
13168
+ type: external_exports.enum(["new-model", "price-change", "recommendation-change", "deprecation", "general"]),
13169
+ model: external_exports.string().optional(),
13170
+ description: external_exports.string(),
13171
+ confidence: external_exports.enum(["high", "medium", "low"])
12940
13172
  });
13173
+ var IntelligenceResponse = external_exports.object({
13174
+ suggestions: external_exports.array(IntelligenceSuggestion),
13175
+ summary: external_exports.string()
13176
+ });
13177
+
13178
+ // src/fetchers/pricing-fetcher.ts
13179
+ var MANIFEST_URL = "https://raw.githubusercontent.com/anthropics/fathom/main/data/remote-manifest.json";
13180
+ var DEFAULT_TIMEOUT_MS = 1e4;
13181
+ async function fetchPricing(options = {}) {
13182
+ const now = (/* @__PURE__ */ new Date()).toISOString();
13183
+ if (options.offline) {
13184
+ return loadLocalPricing(now);
13185
+ }
13186
+ try {
13187
+ const controller = new AbortController();
13188
+ const timeoutId = setTimeout(
13189
+ () => controller.abort(),
13190
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS
13191
+ );
13192
+ if (options.verbose) {
13193
+ console.log(` Fetching: ${MANIFEST_URL}`);
13194
+ }
13195
+ const response = await fetch(MANIFEST_URL, { signal: controller.signal });
13196
+ clearTimeout(timeoutId);
13197
+ if (!response.ok) {
13198
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
13199
+ }
13200
+ const json = await response.json();
13201
+ const manifest = RemoteManifest.parse(json);
13202
+ return {
13203
+ data: {
13204
+ models: manifest.models,
13205
+ recommendations: manifest.recommendations,
13206
+ manifestVersion: manifest.version,
13207
+ manifestDate: manifest.updated
13208
+ },
13209
+ source: "remote",
13210
+ stale: false,
13211
+ fetchedAt: now
13212
+ };
13213
+ } catch (err) {
13214
+ const errorMsg = err instanceof Error ? err.message : String(err);
13215
+ if (options.verbose) {
13216
+ console.log(` Remote fetch failed: ${errorMsg}`);
13217
+ }
13218
+ return loadLocalPricing(now, errorMsg);
13219
+ }
13220
+ }
13221
+ function loadLocalPricing(fetchedAt, error) {
13222
+ const data = loadData();
13223
+ return {
13224
+ data: {
13225
+ models: data.models,
13226
+ recommendations: data.recommendations,
13227
+ manifestVersion: 0,
13228
+ manifestDate: "local"
13229
+ },
13230
+ source: "local",
13231
+ stale: true,
13232
+ fetchedAt,
13233
+ error: error ?? "offline mode"
13234
+ };
13235
+ }
13236
+
13237
+ // src/fetchers/intelligence-fetcher.ts
13238
+ async function getAnthropicClient3(apiKey) {
13239
+ const { default: AnthropicSDK } = await import("@anthropic-ai/sdk");
13240
+ return new AnthropicSDK({ apiKey: apiKey ?? process.env.ANTHROPIC_API_KEY });
13241
+ }
13242
+ var SYSTEM_PROMPT2 = `You are an AI model pricing analyst. Given the current model pricing and agent recommendations for an AI development workflow tool, review them and suggest updates.
13243
+
13244
+ You should:
13245
+ 1. Flag any models with outdated pricing based on your knowledge
13246
+ 2. Suggest new models that should be added (only well-established models from major providers)
13247
+ 3. Flag any recommendation changes (e.g., if a newer model is better suited for a task category)
13248
+ 4. Note any models that may be deprecated or renamed
13249
+
13250
+ Respond with ONLY valid JSON matching this schema:
13251
+ {
13252
+ "suggestions": [
13253
+ {
13254
+ "type": "new-model" | "price-change" | "recommendation-change" | "deprecation" | "general",
13255
+ "model": "model-id (optional)",
13256
+ "description": "what changed or should change",
13257
+ "confidence": "high" | "medium" | "low"
13258
+ }
13259
+ ],
13260
+ "summary": "one sentence overall assessment"
13261
+ }
13262
+
13263
+ If everything looks current, return an empty suggestions array with a summary saying so.`;
13264
+ async function fetchIntelligence(models, recommendations, options = {}) {
13265
+ const now = (/* @__PURE__ */ new Date()).toISOString();
13266
+ const apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY;
13267
+ if (options.offline || !apiKey) {
13268
+ if (options.verbose) {
13269
+ console.log(
13270
+ apiKey ? " Intelligence: skipped (offline mode)" : " Intelligence: skipped (no ANTHROPIC_API_KEY)"
13271
+ );
13272
+ }
13273
+ return null;
13274
+ }
13275
+ try {
13276
+ if (options.verbose) {
13277
+ console.log(" Intelligence: querying Claude Haiku...");
13278
+ }
13279
+ const client = await getAnthropicClient3(apiKey);
13280
+ const modelsDesc = models.map((m) => `${m.id} (${m.name}): $${m.input_per_mtok}/$${m.output_per_mtok} per MTok, tier=${m.tier}`).join("\n");
13281
+ const recsDesc = Object.entries(recommendations).map(([cat, rec]) => `${cat}: ${rec.model} \u2014 ${rec.reason}`).join("\n");
13282
+ const userPrompt = `Current model pricing:
13283
+ ${modelsDesc}
13284
+
13285
+ Current agent recommendations:
13286
+ ${recsDesc}`;
13287
+ const response = await client.messages.create({
13288
+ model: "claude-haiku-4-5-20251001",
13289
+ max_tokens: 1024,
13290
+ system: SYSTEM_PROMPT2,
13291
+ messages: [{ role: "user", content: userPrompt }]
13292
+ });
13293
+ const textBlock = response.content.find((block) => block.type === "text");
13294
+ if (!textBlock || textBlock.type !== "text") {
13295
+ throw new Error("No text content in response");
13296
+ }
13297
+ let jsonText = textBlock.text.trim();
13298
+ if (jsonText.startsWith("```")) {
13299
+ jsonText = jsonText.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
13300
+ }
13301
+ const parsed = JSON.parse(jsonText);
13302
+ const intelligence = IntelligenceResponse.parse(parsed);
13303
+ return {
13304
+ data: intelligence,
13305
+ source: "api",
13306
+ stale: false,
13307
+ fetchedAt: now
13308
+ };
13309
+ } catch (err) {
13310
+ const errorMsg = err instanceof Error ? err.message : String(err);
13311
+ if (options.verbose) {
13312
+ console.log(` Intelligence fetch failed: ${errorMsg}`);
13313
+ }
13314
+ return null;
13315
+ }
13316
+ }
13317
+
13318
+ // src/fetchers/merge.ts
13319
+ function mergePricingData(localModels, remoteModels, localRecs, remoteRecs, suggestions = []) {
13320
+ const mergedModels = [];
13321
+ const modelChanges = [];
13322
+ const seenIds = /* @__PURE__ */ new Set();
13323
+ for (const remote of remoteModels) {
13324
+ seenIds.add(remote.id);
13325
+ const local = localModels.find((m) => m.id === remote.id);
13326
+ if (!local) {
13327
+ mergedModels.push(remote);
13328
+ modelChanges.push({ id: remote.id, name: remote.name, status: "added" });
13329
+ continue;
13330
+ }
13331
+ const fields = [];
13332
+ const pricingKeys = [
13333
+ "input_per_mtok",
13334
+ "output_per_mtok",
13335
+ "cache_read_per_mtok",
13336
+ "batch_discount"
13337
+ ];
13338
+ for (const key of pricingKeys) {
13339
+ if (local[key] !== remote[key]) {
13340
+ fields.push({ field: key, old: local[key], new: remote[key] });
13341
+ }
13342
+ }
13343
+ if (local.tier !== remote.tier) {
13344
+ fields.push({ field: "tier", old: local.tier, new: remote.tier });
13345
+ }
13346
+ mergedModels.push(remote);
13347
+ modelChanges.push({
13348
+ id: remote.id,
13349
+ name: remote.name,
13350
+ status: fields.length > 0 ? "updated" : "unchanged",
13351
+ fields: fields.length > 0 ? fields : void 0
13352
+ });
13353
+ }
13354
+ for (const local of localModels) {
13355
+ if (!seenIds.has(local.id)) {
13356
+ mergedModels.push(local);
13357
+ modelChanges.push({ id: local.id, name: local.name, status: "unchanged" });
13358
+ }
13359
+ }
13360
+ const mergedRecs = { ...localRecs };
13361
+ const recChanges = [];
13362
+ for (const [cat, remoteRec] of Object.entries(remoteRecs)) {
13363
+ const localRec = localRecs[cat];
13364
+ if (!localRec) {
13365
+ mergedRecs[cat] = remoteRec;
13366
+ recChanges.push({
13367
+ category: cat,
13368
+ status: "added",
13369
+ newModel: remoteRec.model,
13370
+ reason: remoteRec.reason
13371
+ });
13372
+ } else if (localRec.model !== remoteRec.model || localRec.reason !== remoteRec.reason) {
13373
+ mergedRecs[cat] = remoteRec;
13374
+ recChanges.push({
13375
+ category: cat,
13376
+ status: "updated",
13377
+ oldModel: localRec.model,
13378
+ newModel: remoteRec.model,
13379
+ reason: remoteRec.reason
13380
+ });
13381
+ } else {
13382
+ recChanges.push({ category: cat, status: "unchanged" });
13383
+ }
13384
+ }
13385
+ return {
13386
+ models: {
13387
+ merged: mergedModels,
13388
+ changes: modelChanges,
13389
+ added: modelChanges.filter((c) => c.status === "added").length,
13390
+ updated: modelChanges.filter((c) => c.status === "updated").length,
13391
+ unchanged: modelChanges.filter((c) => c.status === "unchanged").length
13392
+ },
13393
+ recommendations: {
13394
+ merged: mergedRecs,
13395
+ changes: recChanges,
13396
+ added: recChanges.filter((c) => c.status === "added").length,
13397
+ updated: recChanges.filter((c) => c.status === "updated").length,
13398
+ unchanged: recChanges.filter((c) => c.status === "unchanged").length
13399
+ },
13400
+ suggestions
13401
+ };
13402
+ }
13403
+ function generateModelsYaml(models) {
13404
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
13405
+ let yaml = `# Model pricing registry \u2014 updated via \`fathom research\`
13406
+ `;
13407
+ yaml += `# Last verified: ${date}
13408
+
13409
+ `;
13410
+ yaml += `models:
13411
+ `;
13412
+ for (const m of models) {
13413
+ yaml += ` - id: ${m.id}
13414
+ `;
13415
+ yaml += ` provider: ${m.provider}
13416
+ `;
13417
+ yaml += ` name: ${m.name}
13418
+ `;
13419
+ yaml += ` input_per_mtok: ${m.input_per_mtok.toFixed(2)}
13420
+ `;
13421
+ yaml += ` output_per_mtok: ${m.output_per_mtok.toFixed(2)}
13422
+ `;
13423
+ yaml += ` cache_read_per_mtok: ${m.cache_read_per_mtok.toFixed(2)}
13424
+ `;
13425
+ yaml += ` batch_discount: ${m.batch_discount.toFixed(2)}
13426
+ `;
13427
+ yaml += ` tier: ${m.tier}
13428
+
13429
+ `;
13430
+ }
13431
+ return yaml;
13432
+ }
13433
+ function generateAgentsYaml(recommendations) {
13434
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
13435
+ let yaml = `# Agent/model recommendation mappings
13436
+ `;
13437
+ yaml += `# Maps task categories to recommended models
13438
+ `;
13439
+ yaml += `# Last updated: ${date}
13440
+
13441
+ `;
13442
+ yaml += `recommendations:
13443
+ `;
13444
+ for (const [category, rec] of Object.entries(recommendations)) {
13445
+ yaml += ` ${category}:
13446
+ `;
13447
+ yaml += ` model: ${rec.model}
13448
+ `;
13449
+ yaml += ` reason: ${rec.reason}
13450
+
13451
+ `;
13452
+ }
13453
+ return yaml;
13454
+ }
13455
+
13456
+ // src/commands/research.ts
13457
+ function findDataDir2() {
13458
+ const bundled = resolve17(import.meta.dirname ?? __dirname, "data");
13459
+ if (existsSync17(resolve17(bundled, "models.yaml"))) return bundled;
13460
+ return resolve17(import.meta.dirname ?? __dirname, "../../../../data");
13461
+ }
13462
+ var researchCommand = new Command15("research").description("Check and update model pricing data (fetches live data)").option("--update", "Update local data files with latest pricing").option("--sync", "Also sync updated pricing to Convex").option("--check", "Check for differences without updating (default)").option("--offline", "Skip remote fetches, use local data only").option("--verbose", "Show fetch sources and timing details").action(async (options) => {
13463
+ console.log(chalk16.bold("\nFathom \u2014 Research: Model Pricing"));
13464
+ console.log(chalk16.dim("\u2500".repeat(55)));
13465
+ const startTime = Date.now();
13466
+ let localData;
13467
+ try {
13468
+ localData = loadData();
13469
+ } catch {
13470
+ console.log(chalk16.yellow(" Warning: could not load local data files"));
13471
+ localData = { models: [], recommendations: {} };
13472
+ }
13473
+ const [pricingResult, intelligenceResult] = await Promise.all([
13474
+ fetchPricing({ offline: options.offline, verbose: options.verbose }),
13475
+ fetchIntelligence(localData.models, localData.recommendations, {
13476
+ offline: options.offline,
13477
+ verbose: options.verbose
13478
+ })
13479
+ ]);
13480
+ if (options.verbose) {
13481
+ const elapsed = Date.now() - startTime;
13482
+ console.log(chalk16.dim(`
13483
+ Fetch time: ${elapsed}ms`));
13484
+ console.log(chalk16.dim(` Pricing source: ${pricingResult.source}`));
13485
+ if (intelligenceResult) {
13486
+ console.log(chalk16.dim(` Intelligence source: ${intelligenceResult.source}`));
13487
+ }
13488
+ }
13489
+ if (pricingResult.stale) {
13490
+ console.log(chalk16.yellow(`
13491
+ \u26A0 Using local data (${pricingResult.error ?? "stale"})`));
13492
+ } else {
13493
+ console.log(chalk16.green(`
13494
+ Pricing: live data (manifest v${pricingResult.data.manifestVersion}, ${pricingResult.data.manifestDate})`));
13495
+ }
13496
+ const mergeResult = mergePricingData(
13497
+ localData.models,
13498
+ pricingResult.data.models,
13499
+ localData.recommendations,
13500
+ pricingResult.data.recommendations,
13501
+ intelligenceResult?.data.suggestions ?? []
13502
+ );
13503
+ displayPricingReport(mergeResult);
13504
+ if (intelligenceResult?.data) {
13505
+ displayIntelligence(intelligenceResult.data);
13506
+ }
13507
+ displayRecommendations(mergeResult);
13508
+ if (options.update) {
13509
+ const dataDir = findDataDir2();
13510
+ const modelsPath = resolve17(dataDir, "models.yaml");
13511
+ const agentsPath = resolve17(dataDir, "agents.yaml");
13512
+ writeFileSync10(modelsPath, generateModelsYaml(mergeResult.models.merged));
13513
+ console.log(chalk16.green(`
13514
+ Updated: ${modelsPath}`));
13515
+ writeFileSync10(agentsPath, generateAgentsYaml(mergeResult.recommendations.merged));
13516
+ console.log(chalk16.green(` Updated: ${agentsPath}`));
13517
+ }
13518
+ if (options.sync) {
13519
+ await syncToConvex(mergeResult);
13520
+ }
13521
+ if (options.verbose) {
13522
+ const totalElapsed = Date.now() - startTime;
13523
+ console.log(chalk16.dim(`
13524
+ Total time: ${totalElapsed}ms`));
13525
+ }
13526
+ console.log();
13527
+ });
13528
+ function displayPricingReport(result) {
13529
+ const { models } = result;
13530
+ if (models.added === 0 && models.updated === 0) {
13531
+ console.log(chalk16.green(` All model pricing is up to date.`));
13532
+ console.log(chalk16.dim(` ${models.merged.length} models checked.`));
13533
+ } else {
13534
+ if (models.updated > 0) {
13535
+ console.log(chalk16.yellow(`
13536
+ ${models.updated} pricing change(s) found:`));
13537
+ for (const change of models.changes) {
13538
+ if (change.status === "updated" && change.fields) {
13539
+ for (const f of change.fields) {
13540
+ console.log(chalk16.dim(` ${change.name}: ${f.field} ${f.old} \u2192 ${f.new}`));
13541
+ }
13542
+ }
13543
+ }
13544
+ }
13545
+ if (models.added > 0) {
13546
+ console.log(chalk16.yellow(`
13547
+ ${models.added} new model(s) found:`));
13548
+ for (const change of models.changes) {
13549
+ if (change.status === "added") {
13550
+ const m = models.merged.find((x) => x.id === change.id);
13551
+ if (m) {
13552
+ console.log(
13553
+ chalk16.dim(` ${m.name} (${m.tier}): $${m.input_per_mtok}/$${m.output_per_mtok} per MTok`)
13554
+ );
13555
+ }
13556
+ }
13557
+ }
13558
+ }
13559
+ }
13560
+ }
13561
+ function displayIntelligence(intelligence) {
13562
+ if (intelligence.suggestions.length === 0) {
13563
+ console.log(chalk16.dim(`
13564
+ Intelligence: ${intelligence.summary}`));
13565
+ return;
13566
+ }
13567
+ console.log(chalk16.cyan(`
13568
+ Intelligence suggestions (${intelligence.suggestions.length}):`));
13569
+ for (const s of intelligence.suggestions) {
13570
+ const badge = s.confidence === "high" ? chalk16.red("HIGH") : s.confidence === "medium" ? chalk16.yellow("MED") : chalk16.dim("LOW");
13571
+ const model = s.model ? chalk16.dim(` [${s.model}]`) : "";
13572
+ console.log(` ${badge} ${s.description}${model}`);
13573
+ }
13574
+ console.log(chalk16.dim(` Summary: ${intelligence.summary}`));
13575
+ console.log(chalk16.dim(` Note: Task profiles are team-calibrated and shown as suggestions only.`));
13576
+ }
13577
+ function displayRecommendations(result) {
13578
+ const { recommendations } = result;
13579
+ if (recommendations.updated > 0 || recommendations.added > 0) {
13580
+ console.log(chalk16.yellow(`
13581
+ Recommendation changes:`));
13582
+ for (const change of recommendations.changes) {
13583
+ if (change.status === "updated") {
13584
+ console.log(chalk16.dim(` ${change.category}: ${change.oldModel} \u2192 ${change.newModel}`));
13585
+ } else if (change.status === "added") {
13586
+ console.log(chalk16.dim(` ${change.category}: ${change.newModel} (new)`));
13587
+ }
13588
+ }
13589
+ }
13590
+ console.log(chalk16.dim("\n Current model recommendations:"));
13591
+ console.log(chalk16.dim(" S/M complexity \u2192 Haiku 4.5 ($1/$5 per MTok) \u2014 fast, cheap"));
13592
+ console.log(chalk16.dim(" L complexity \u2192 Sonnet 4.6 ($3/$15 per MTok) \u2014 balanced"));
13593
+ console.log(chalk16.dim(" XL complexity \u2192 Opus 4.6 ($15/$75 per MTok) \u2014 highest quality"));
13594
+ console.log(
13595
+ chalk16.dim("\n Tip: Enable prompt caching to save 90% on input tokens for repeated context.")
13596
+ );
13597
+ }
13598
+ async function syncToConvex(result) {
13599
+ const client = getConvexClient();
13600
+ if (!client) {
13601
+ logConvexStatus(false);
13602
+ return;
13603
+ }
13604
+ let synced = 0;
13605
+ for (const m of result.models.merged) {
13606
+ try {
13607
+ await client.mutation(api.modelPricing.upsert, {
13608
+ model: m.name,
13609
+ provider: m.provider,
13610
+ inputPerMTok: m.input_per_mtok,
13611
+ outputPerMTok: m.output_per_mtok,
13612
+ cacheReadPerMTok: m.cache_read_per_mtok,
13613
+ batchDiscount: m.batch_discount
13614
+ });
13615
+ synced++;
13616
+ } catch {
13617
+ }
13618
+ }
13619
+ logConvexStatus(synced > 0);
13620
+ if (synced > 0) {
13621
+ console.log(chalk16.dim(` ${synced} models synced to Convex.`));
13622
+ }
13623
+ }
12941
13624
 
12942
13625
  // src/commands/project.ts
12943
13626
  import { Command as Command16 } from "commander";
12944
- import { existsSync as existsSync17 } from "fs";
12945
- import { resolve as resolve17 } from "path";
13627
+ import { existsSync as existsSync18 } from "fs";
13628
+ import { resolve as resolve18 } from "path";
12946
13629
  import chalk17 from "chalk";
12947
13630
  var projectCommand = new Command16("project").description("View or update project intent and regenerate projections").option("--regenerate", "Regenerate projection files from current intent").action(async (options) => {
12948
13631
  try {
12949
13632
  const cwd = process.cwd();
12950
- const intentPath = resolve17(cwd, ".fathom", "intent.yaml");
12951
- if (!existsSync17(intentPath)) {
13633
+ const intentPath = resolve18(cwd, ".fathom", "intent.yaml");
13634
+ if (!existsSync18(intentPath)) {
12952
13635
  console.log(chalk17.yellow("\n No project intent found."));
12953
13636
  console.log(chalk17.dim(" Run `fathom` to set up a project with intent capture.\n"));
12954
13637
  return;
@@ -13007,13 +13690,13 @@ var projectCommand = new Command16("project").description("View or update projec
13007
13690
  console.log(chalk17.bold(" Opinions:") + " " + intent.tech.opinions.join(", "));
13008
13691
  }
13009
13692
  }
13010
- const projectionsDir = resolve17(cwd, ".fathom", "projections");
13011
- const hasProjections = existsSync17(projectionsDir);
13693
+ const projectionsDir = resolve18(cwd, ".fathom", "projections");
13694
+ const hasProjections = existsSync18(projectionsDir);
13012
13695
  console.log(chalk17.bold("\n Projections:") + (hasProjections ? chalk17.green(" generated") : chalk17.yellow(" not generated")));
13013
13696
  if (hasProjections) {
13014
13697
  const targets = ["claude/SKILL.md", "cursor/.cursorrules", "generic/SYSTEM_PROMPT.md"];
13015
13698
  for (const t of targets) {
13016
- const exists = existsSync17(resolve17(projectionsDir, t));
13699
+ const exists = existsSync18(resolve18(projectionsDir, t));
13017
13700
  console.log(` ${exists ? "\u2713" : "\xB7"} ${t}`);
13018
13701
  }
13019
13702
  }
@@ -13030,14 +13713,14 @@ var projectCommand = new Command16("project").description("View or update projec
13030
13713
 
13031
13714
  // src/commands/report.ts
13032
13715
  import { Command as Command17 } from "commander";
13033
- import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, existsSync as existsSync18 } from "fs";
13034
- import { resolve as resolve18, join as join4 } from "path";
13716
+ import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync10, existsSync as existsSync19 } from "fs";
13717
+ import { resolve as resolve19, join as join4 } from "path";
13035
13718
  import chalk18 from "chalk";
13036
13719
 
13037
13720
  // ../store/dist/index.js
13038
13721
  import { randomUUID } from "crypto";
13039
13722
  import { mkdir, readFile, rename, writeFile } from "fs/promises";
13040
- import { dirname as dirname6, join as join3 } from "path";
13723
+ import { dirname as dirname7, join as join3 } from "path";
13041
13724
  import { readFile as readFile2 } from "fs/promises";
13042
13725
  import { join as join22 } from "path";
13043
13726
  var OverheadBreakdownSchema = external_exports.object({
@@ -13228,7 +13911,7 @@ function generateId() {
13228
13911
  return randomUUID();
13229
13912
  }
13230
13913
  async function ensureDir(path) {
13231
- await mkdir(dirname6(path), { recursive: true });
13914
+ await mkdir(dirname7(path), { recursive: true });
13232
13915
  }
13233
13916
  async function readJsonFile(path) {
13234
13917
  try {
@@ -14661,12 +15344,12 @@ Fathom \u2014 Project Report: ${projectName}`));
14661
15344
  "reports",
14662
15345
  `${projectName}-${today}.${ext}`
14663
15346
  );
14664
- const outputPath = options.output ? resolve18(options.output) : defaultPath;
14665
- const dir = resolve18(outputPath, "..");
14666
- if (!existsSync18(dir)) {
15347
+ const outputPath = options.output ? resolve19(options.output) : defaultPath;
15348
+ const dir = resolve19(outputPath, "..");
15349
+ if (!existsSync19(dir)) {
14667
15350
  mkdirSync10(dir, { recursive: true });
14668
15351
  }
14669
- writeFileSync10(outputPath, report, "utf-8");
15352
+ writeFileSync11(outputPath, report, "utf-8");
14670
15353
  console.log(
14671
15354
  chalk18.green(`
14672
15355
  \u2713 Report generated: ${outputPath}`)
@@ -14682,17 +15365,17 @@ Fathom \u2014 Project Report: ${projectName}`));
14682
15365
 
14683
15366
  // src/commands/contribute.ts
14684
15367
  import { Command as Command18 } from "commander";
14685
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync11, existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
14686
- import { resolve as resolve19 } from "path";
15368
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, existsSync as existsSync20, mkdirSync as mkdirSync11 } from "fs";
15369
+ import { resolve as resolve20 } from "path";
14687
15370
  import chalk19 from "chalk";
14688
- import { confirm as confirm3 } from "@inquirer/prompts";
15371
+ import { confirm as confirm4 } from "@inquirer/prompts";
14689
15372
  function loadCalibratedProfiles() {
14690
15373
  const profiles = [];
14691
- const registryPath = resolve19(process.cwd(), ".claude", "te", "registry.json");
14692
- const summaryPath = resolve19(process.cwd(), ".claude", "te", "tracking", "summary.json");
14693
- if (existsSync19(registryPath) && existsSync19(summaryPath)) {
14694
- const registry = JSON.parse(readFileSync16(registryPath, "utf-8"));
14695
- const summary = JSON.parse(readFileSync16(summaryPath, "utf-8"));
15374
+ const registryPath = resolve20(process.cwd(), ".claude", "te", "registry.json");
15375
+ const summaryPath = resolve20(process.cwd(), ".claude", "te", "tracking", "summary.json");
15376
+ if (existsSync20(registryPath) && existsSync20(summaryPath)) {
15377
+ const registry = JSON.parse(readFileSync18(registryPath, "utf-8"));
15378
+ const summary = JSON.parse(readFileSync18(summaryPath, "utf-8"));
14696
15379
  const actuals = /* @__PURE__ */ new Map();
14697
15380
  for (const session of summary.sessions ?? []) {
14698
15381
  if (!session.featureId || session.status === "untagged") continue;
@@ -14769,7 +15452,7 @@ var contributeCommand = new Command18("contribute").description("Share anonymize
14769
15452
  }
14770
15453
  if (!options.yes) {
14771
15454
  try {
14772
- const confirmed = await confirm3({
15455
+ const confirmed = await confirm4({
14773
15456
  message: "Share this data?",
14774
15457
  default: false
14775
15458
  });
@@ -14782,12 +15465,12 @@ var contributeCommand = new Command18("contribute").description("Share anonymize
14782
15465
  return;
14783
15466
  }
14784
15467
  }
14785
- const contributionsDir = resolve19(process.cwd(), ".fathom", "contributions");
15468
+ const contributionsDir = resolve20(process.cwd(), ".fathom", "contributions");
14786
15469
  mkdirSync11(contributionsDir, { recursive: true });
14787
15470
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
14788
- const outputPath = resolve19(contributionsDir, `${today}.json`);
15471
+ const outputPath = resolve20(contributionsDir, `${today}.json`);
14789
15472
  const json = exportContribution(dataPoints);
14790
- writeFileSync11(outputPath, json, "utf-8");
15473
+ writeFileSync12(outputPath, json, "utf-8");
14791
15474
  console.log(chalk19.green(`
14792
15475
  \u2713 Contribution saved to .fathom/contributions/${today}.json`));
14793
15476
  console.log(chalk19.green(" \u2713 Thank you! This helps make Fathom better for everyone."));
@@ -14796,14 +15479,14 @@ var contributeCommand = new Command18("contribute").description("Share anonymize
14796
15479
 
14797
15480
  // src/commands/feedback.ts
14798
15481
  import { Command as Command19 } from "commander";
14799
- import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync12, readFileSync as readFileSync17, existsSync as existsSync20 } from "fs";
14800
- import { resolve as resolve20, join as join5 } from "path";
15482
+ import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync12, readFileSync as readFileSync19, existsSync as existsSync21 } from "fs";
15483
+ import { resolve as resolve21, join as join5 } from "path";
14801
15484
  import chalk20 from "chalk";
14802
15485
  function getIntentSummary() {
14803
15486
  try {
14804
- const intentPath = resolve20(process.cwd(), ".fathom", "intent.yaml");
14805
- if (!existsSync20(intentPath)) return {};
14806
- const content3 = readFileSync17(intentPath, "utf-8");
15487
+ const intentPath = resolve21(process.cwd(), ".fathom", "intent.yaml");
15488
+ if (!existsSync21(intentPath)) return {};
15489
+ const content3 = readFileSync19(intentPath, "utf-8");
14807
15490
  const projectMatch = content3.match(/^project:\s*(.+)$/m);
14808
15491
  const descMatch = content3.match(/^description:\s*["']?(.+?)["']?\s*$/m);
14809
15492
  return {
@@ -14823,7 +15506,7 @@ function getSystemInfo() {
14823
15506
  };
14824
15507
  }
14825
15508
  function saveFeedback(type, description) {
14826
- const feedbackDir = resolve20(process.cwd(), ".fathom", "feedback");
15509
+ const feedbackDir = resolve21(process.cwd(), ".fathom", "feedback");
14827
15510
  mkdirSync12(feedbackDir, { recursive: true });
14828
15511
  const sys = getSystemInfo();
14829
15512
  const intent = getIntentSummary();
@@ -14861,7 +15544,7 @@ function saveFeedback(type, description) {
14861
15544
  "Review this file, add any additional details, then email to feedback@fathom.dev"
14862
15545
  );
14863
15546
  lines.push("");
14864
- writeFileSync12(filepath, lines.join("\n"));
15547
+ writeFileSync13(filepath, lines.join("\n"));
14865
15548
  return filepath;
14866
15549
  }
14867
15550
  var feedbackCommand = new Command19("feedback").description("How to report bugs and request features").option("--bug <description>", "Save a bug report to .fathom/feedback/").option(
@@ -14924,8 +15607,8 @@ import { Command as Command20 } from "commander";
14924
15607
  import chalk21 from "chalk";
14925
15608
 
14926
15609
  // ../sync/dist/index.js
14927
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, existsSync as existsSync21 } from "fs";
14928
- import { resolve as resolve21, dirname as dirname7 } from "path";
15610
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13, existsSync as existsSync23 } from "fs";
15611
+ import { resolve as resolve22, dirname as dirname8 } from "path";
14929
15612
  var SyncStatusSchema = external_exports.enum([
14930
15613
  "backlog",
14931
15614
  "todo",
@@ -15605,20 +16288,20 @@ function createProvider(config, apiKey) {
15605
16288
  }
15606
16289
  function getLedgerPath(basePath) {
15607
16290
  const base = basePath ?? process.cwd();
15608
- return resolve21(base, LEDGER_DIR, LEDGER_FILE);
16291
+ return resolve22(base, LEDGER_DIR, LEDGER_FILE);
15609
16292
  }
15610
16293
  function loadLedger(ledgerPath) {
15611
- if (existsSync21(ledgerPath)) {
16294
+ if (existsSync23(ledgerPath)) {
15612
16295
  try {
15613
- return JSON.parse(readFileSync18(ledgerPath, "utf-8"));
16296
+ return JSON.parse(readFileSync20(ledgerPath, "utf-8"));
15614
16297
  } catch {
15615
16298
  }
15616
16299
  }
15617
16300
  return { provider: "", projectKey: "", items: [], lastSync: 0 };
15618
16301
  }
15619
16302
  function saveLedger(ledgerPath, ledger) {
15620
- mkdirSync13(dirname7(ledgerPath), { recursive: true });
15621
- writeFileSync13(ledgerPath, JSON.stringify(ledger, null, 2) + "\n");
16303
+ mkdirSync13(dirname8(ledgerPath), { recursive: true });
16304
+ writeFileSync14(ledgerPath, JSON.stringify(ledger, null, 2) + "\n");
15622
16305
  }
15623
16306
  function createSyncManager(config, options) {
15624
16307
  const apiKey = resolveApiKey(config);
@@ -15677,14 +16360,14 @@ function createSyncManager(config, options) {
15677
16360
  }
15678
16361
 
15679
16362
  // src/commands/sync.ts
15680
- import { readFileSync as readFileSync19, existsSync as existsSync23 } from "fs";
15681
- import { resolve as resolve22 } from "path";
16363
+ import { readFileSync as readFileSync21, existsSync as existsSync24 } from "fs";
16364
+ import { resolve as resolve23 } from "path";
15682
16365
  function loadSyncConfig(options) {
15683
- const configPath = resolve22(process.cwd(), ".fathom", "sync-config.json");
16366
+ const configPath = resolve23(process.cwd(), ".fathom", "sync-config.json");
15684
16367
  let config = {};
15685
- if (existsSync23(configPath)) {
16368
+ if (existsSync24(configPath)) {
15686
16369
  try {
15687
- config = JSON.parse(readFileSync19(configPath, "utf-8"));
16370
+ config = JSON.parse(readFileSync21(configPath, "utf-8"));
15688
16371
  } catch {
15689
16372
  }
15690
16373
  }
@@ -15697,10 +16380,10 @@ function loadSyncConfig(options) {
15697
16380
  };
15698
16381
  }
15699
16382
  function loadPendingSlices() {
15700
- const registryPath = resolve22(process.cwd(), ".claude", "te", "registry.json");
15701
- if (!existsSync23(registryPath)) return [];
16383
+ const registryPath = resolve23(process.cwd(), ".claude", "te", "registry.json");
16384
+ if (!existsSync24(registryPath)) return [];
15702
16385
  try {
15703
- const data = JSON.parse(readFileSync19(registryPath, "utf-8"));
16386
+ const data = JSON.parse(readFileSync21(registryPath, "utf-8"));
15704
16387
  if (!Array.isArray(data.features)) return [];
15705
16388
  return data.features.map((f) => ({
15706
16389
  sliceId: f.id,
@@ -15795,8 +16478,94 @@ Fathom \u2014 Sync to ${config.provider.charAt(0).toUpperCase() + config.provide
15795
16478
  }
15796
16479
  });
15797
16480
 
16481
+ // src/commands/install-plugin.ts
16482
+ import { Command as Command21 } from "commander";
16483
+ import {
16484
+ writeFileSync as writeFileSync15,
16485
+ readFileSync as readFileSync23,
16486
+ mkdirSync as mkdirSync14,
16487
+ existsSync as existsSync25,
16488
+ readdirSync as readdirSync4
16489
+ } from "fs";
16490
+ import { resolve as resolve24, dirname as dirname9 } from "path";
16491
+ import { fileURLToPath as fileURLToPath3 } from "url";
16492
+ import chalk22 from "chalk";
16493
+ var installPluginCommand = new Command21("install-plugin").description(
16494
+ "Install Fathom plugin for Claude Code (copies skills + MCP config)"
16495
+ ).option("--project-dir <dir>", "Target project directory", ".").action((options) => {
16496
+ const projectDir = resolve24(options.projectDir);
16497
+ const __dirname2 = dirname9(fileURLToPath3(import.meta.url));
16498
+ let pluginSource = resolve24(__dirname2, "../plugins/fathom");
16499
+ if (!existsSync25(pluginSource)) {
16500
+ pluginSource = resolve24(__dirname2, "../../plugins/fathom");
16501
+ }
16502
+ if (!existsSync25(pluginSource)) {
16503
+ console.error(
16504
+ chalk22.red("Error: Plugin source files not found at " + pluginSource)
16505
+ );
16506
+ console.error(
16507
+ chalk22.dim("If installed via npm, ensure fathom-cli >= 0.3.3")
16508
+ );
16509
+ process.exit(1);
16510
+ }
16511
+ console.log(chalk22.bold("\nFathom \u2014 Install Plugin"));
16512
+ console.log(chalk22.dim("\u2500".repeat(50)));
16513
+ const skillsSource = resolve24(pluginSource, "skills");
16514
+ if (existsSync25(skillsSource)) {
16515
+ const skillDirs = readdirSync4(skillsSource, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
16516
+ for (const skillDir of skillDirs) {
16517
+ const src = resolve24(skillsSource, skillDir, "SKILL.md");
16518
+ if (!existsSync25(src)) continue;
16519
+ const destDir = resolve24(
16520
+ projectDir,
16521
+ ".claude",
16522
+ "skills",
16523
+ "fathom",
16524
+ skillDir
16525
+ );
16526
+ mkdirSync14(destDir, { recursive: true });
16527
+ writeFileSync15(resolve24(destDir, "SKILL.md"), readFileSync23(src, "utf-8"));
16528
+ console.log(chalk22.green(` \u2713 .claude/skills/fathom/${skillDir}/SKILL.md`));
16529
+ }
16530
+ }
16531
+ const pluginJsonSrc = resolve24(
16532
+ pluginSource,
16533
+ ".claude-plugin",
16534
+ "plugin.json"
16535
+ );
16536
+ if (existsSync25(pluginJsonSrc)) {
16537
+ const destDir = resolve24(projectDir, ".claude-plugin");
16538
+ mkdirSync14(destDir, { recursive: true });
16539
+ writeFileSync15(
16540
+ resolve24(destDir, "plugin.json"),
16541
+ readFileSync23(pluginJsonSrc, "utf-8")
16542
+ );
16543
+ console.log(chalk22.green(" \u2713 .claude-plugin/plugin.json"));
16544
+ }
16545
+ const mcpPath = resolve24(projectDir, ".mcp.json");
16546
+ let mcpConfig = { mcpServers: {} };
16547
+ if (existsSync25(mcpPath)) {
16548
+ try {
16549
+ mcpConfig = JSON.parse(readFileSync23(mcpPath, "utf-8"));
16550
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
16551
+ } catch {
16552
+ mcpConfig = { mcpServers: {} };
16553
+ }
16554
+ }
16555
+ mcpConfig.mcpServers.fathom = {
16556
+ command: "npx",
16557
+ args: ["-y", "fathom-mcp@latest", "--project-dir", "."]
16558
+ };
16559
+ writeFileSync15(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n");
16560
+ console.log(chalk22.green(" \u2713 .mcp.json (MCP auto-discovery)"));
16561
+ console.log(chalk22.bold("\n\u2713 Fathom plugin installed"));
16562
+ console.log(
16563
+ chalk22.dim(" Skills and MCP config are ready for Claude Code\n")
16564
+ );
16565
+ });
16566
+
15798
16567
  // src/index.ts
15799
- var program = new Command21();
16568
+ var program = new Command22();
15800
16569
  program.name("fathom").description("Workflow intelligence platform for AI-augmented development").version(VERSION);
15801
16570
  program.addCommand(goCommand, { isDefault: true });
15802
16571
  program.addCommand(intakeCommand);
@@ -15813,6 +16582,7 @@ program.addCommand(projectCommand);
15813
16582
  program.addCommand(contributeCommand);
15814
16583
  program.addCommand(feedbackCommand);
15815
16584
  program.addCommand(syncCommand);
16585
+ program.addCommand(installPluginCommand);
15816
16586
  program.addCommand(statusCommand);
15817
16587
  program.addCommand(pricingCommand);
15818
16588
  program.addCommand(validateCommand);