fathom-cli 0.3.1 → 0.3.4

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.
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-token-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;
@@ -10676,6 +10739,67 @@ function printProgress(state) {
10676
10739
  )
10677
10740
  );
10678
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
+ }
10679
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) => {
10680
10804
  try {
10681
10805
  const projectState = detectProjectState();
@@ -10715,7 +10839,7 @@ var goCommand = new Command3("go").description("Run the full workflow: intake \u
10715
10839
  console.log();
10716
10840
  console.log(chalk4.dim(" No project detected. Let's set one up."));
10717
10841
  console.log();
10718
- const intentData = await captureIntentFlow(basename2(process.cwd()));
10842
+ const intentData = await captureIntentFlow(basename3(process.cwd()));
10719
10843
  projectName = intentData.project;
10720
10844
  const { saveIntent } = await import("./dist-XKZLNUDU.js");
10721
10845
  await saveIntent(process.cwd(), intentData);
@@ -10753,6 +10877,7 @@ var goCommand = new Command3("go").description("Run the full workflow: intake \u
10753
10877
  },
10754
10878
  { name: "New intake \u2014 add more work", value: "intake" },
10755
10879
  { name: "Review \u2014 reconcile recent sessions", value: "review" },
10880
+ { name: "Update intent / project settings", value: "settings" },
10756
10881
  { name: "Status overview", value: "status" }
10757
10882
  ]
10758
10883
  });
@@ -10760,6 +10885,10 @@ var goCommand = new Command3("go").description("Run the full workflow: intake \u
10760
10885
  printStatus(workflowState);
10761
10886
  return;
10762
10887
  }
10888
+ if (action === "settings") {
10889
+ await runSettingsFlow(projectName);
10890
+ return;
10891
+ }
10763
10892
  startPhase = action;
10764
10893
  } else {
10765
10894
  startPhase = "intake";
@@ -10951,8 +11080,8 @@ async function executeBuildMode(mode, promptPath, state) {
10951
11080
  console.log(chalk4.dim(`
10952
11081
  Creating worktree: ${branchName}`));
10953
11082
  try {
10954
- const { execSync } = await import("child_process");
10955
- execSync(`git worktree add "${worktreePath}" -b "${branchName}"`, {
11083
+ const { execSync: execSync2 } = await import("child_process");
11084
+ execSync2(`git worktree add "${worktreePath}" -b "${branchName}"`, {
10956
11085
  stdio: "pipe",
10957
11086
  cwd: process.cwd()
10958
11087
  });
@@ -10984,8 +11113,8 @@ async function executeBuildMode(mode, promptPath, state) {
10984
11113
  return false;
10985
11114
  }
10986
11115
  function launchClaude(promptPath, cwd, firstFeature) {
10987
- return new Promise((resolve23) => {
10988
- const promptContent = readFileSync6(promptPath, "utf-8");
11116
+ return new Promise((resolve25) => {
11117
+ const promptContent = readFileSync7(promptPath, "utf-8");
10989
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.";
10990
11119
  const child = spawn("claude", ["--append-system-prompt", promptContent, initialPrompt], {
10991
11120
  stdio: "inherit",
@@ -11001,11 +11130,11 @@ function launchClaude(promptPath, cwd, firstFeature) {
11001
11130
  } else {
11002
11131
  console.error(chalk4.red(` Failed to launch Claude Code: ${err.message}`));
11003
11132
  }
11004
- resolve23();
11133
+ resolve25();
11005
11134
  });
11006
11135
  child.on("close", () => {
11007
11136
  process.removeListener("SIGINT", sigintHandler);
11008
- resolve23();
11137
+ resolve25();
11009
11138
  });
11010
11139
  });
11011
11140
  }
@@ -11031,7 +11160,7 @@ function printStatus(state) {
11031
11160
 
11032
11161
  // src/commands/analyze.ts
11033
11162
  import { Command as Command4 } from "commander";
11034
- 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";
11035
11164
  import { resolve as resolve8 } from "path";
11036
11165
  import chalk5 from "chalk";
11037
11166
  import Table2 from "cli-table3";
@@ -11041,7 +11170,7 @@ var analyzeCommand = new Command4("analyze").description("Analyze a spec and gen
11041
11170
  console.error(chalk5.red(`Spec file not found: ${fullPath}`));
11042
11171
  process.exit(1);
11043
11172
  }
11044
- const content3 = readFileSync7(fullPath, "utf-8");
11173
+ const content3 = readFileSync8(fullPath, "utf-8");
11045
11174
  const data = loadData();
11046
11175
  console.log(chalk5.dim("Parsing spec..."));
11047
11176
  const spec = parseSpec(content3);
@@ -11211,7 +11340,7 @@ var pricingCommand = new Command5("pricing").description("Show current model pri
11211
11340
 
11212
11341
  // src/commands/status.ts
11213
11342
  import { Command as Command6 } from "commander";
11214
- import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
11343
+ import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
11215
11344
  import { resolve as resolve9 } from "path";
11216
11345
  import chalk7 from "chalk";
11217
11346
  import Table4 from "cli-table3";
@@ -11225,7 +11354,7 @@ var statusCommand = new Command6("status").description("Show project status from
11225
11354
  );
11226
11355
  return;
11227
11356
  }
11228
- const registry = JSON.parse(readFileSync8(registryPath, "utf-8"));
11357
+ const registry = JSON.parse(readFileSync9(registryPath, "utf-8"));
11229
11358
  const projectName = options.project ?? registry.project ?? getProjectName();
11230
11359
  const summaryPath = resolve9(
11231
11360
  process.cwd(),
@@ -11237,7 +11366,7 @@ var statusCommand = new Command6("status").description("Show project status from
11237
11366
  const hasTracking = existsSync8(summaryPath);
11238
11367
  const actuals = /* @__PURE__ */ new Map();
11239
11368
  if (hasTracking) {
11240
- const summary = JSON.parse(readFileSync8(summaryPath, "utf-8"));
11369
+ const summary = JSON.parse(readFileSync9(summaryPath, "utf-8"));
11241
11370
  for (const s of summary.sessions) {
11242
11371
  if (!s.featureId || s.status === "untagged") continue;
11243
11372
  const existing = actuals.get(s.featureId) ?? { tokens: 0, sessions: 0 };
@@ -11300,14 +11429,14 @@ Total estimated: ${totalEstimated.toLocaleString()} tokens`));
11300
11429
 
11301
11430
  // src/commands/track.ts
11302
11431
  import { Command as Command7 } from "commander";
11303
- 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";
11304
11433
  import { resolve as resolve10 } from "path";
11305
11434
  import chalk8 from "chalk";
11306
11435
  import Table5 from "cli-table3";
11307
11436
 
11308
11437
  // ../tracker/dist/index.js
11309
- import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync9 } from "fs";
11310
- 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";
11311
11440
  import { homedir } from "os";
11312
11441
  import { countTokens } from "@anthropic-ai/tokenizer";
11313
11442
  import { readFileSync as readFileSync22, existsSync as existsSync22 } from "fs";
@@ -11366,7 +11495,7 @@ var Registry = external_exports.object({
11366
11495
  generatedAt: external_exports.string()
11367
11496
  });
11368
11497
  function parseSessionFile(filePath) {
11369
- const content3 = readFileSync9(filePath, "utf-8");
11498
+ const content3 = readFileSync10(filePath, "utf-8");
11370
11499
  const lines = content3.trim().split("\n").filter(Boolean);
11371
11500
  if (lines.length === 0) return null;
11372
11501
  let inputTokens = 0;
@@ -11439,7 +11568,7 @@ function parseSessionFile(filePath) {
11439
11568
  if (typeof text3 === "string") bucket.push(text3);
11440
11569
  }
11441
11570
  }
11442
- const sessionId = basename3(filePath, ".jsonl");
11571
+ const sessionId = basename4(filePath, ".jsonl");
11443
11572
  const activeMinutes = sessionStart && sessionEnd ? (sessionEnd - sessionStart) / 6e4 : void 0;
11444
11573
  const allParts = [...inputParts, ...outputParts, ...conversationParts];
11445
11574
  const conversationText = allParts.join("\n");
@@ -11715,7 +11844,7 @@ var trackCommand = new Command7("track").description("Import Claude Code session
11715
11844
  return;
11716
11845
  }
11717
11846
  const registry = JSON.parse(
11718
- readFileSync10(registryPath, "utf-8")
11847
+ readFileSync11(registryPath, "utf-8")
11719
11848
  );
11720
11849
  const projectName = options.project ?? registry.project ?? getProjectName();
11721
11850
  console.log(chalk8.dim("Scanning for sessions..."));
@@ -11742,7 +11871,7 @@ var trackCommand = new Command7("track").description("Import Claude Code session
11742
11871
  );
11743
11872
  const trackedIds = /* @__PURE__ */ new Set();
11744
11873
  if (!options.backfill && existsSync10(summaryPath)) {
11745
- const summary = JSON.parse(readFileSync10(summaryPath, "utf-8"));
11874
+ const summary = JSON.parse(readFileSync11(summaryPath, "utf-8"));
11746
11875
  for (const s of summary.sessions ?? []) {
11747
11876
  trackedIds.add(s.sessionId);
11748
11877
  }
@@ -11842,7 +11971,7 @@ Total: ${totalTokens.toLocaleString()} tokens across ${results.length} sessions`
11842
11971
  }));
11843
11972
  let existing = { sessions: [] };
11844
11973
  if (existsSync10(summaryPath)) {
11845
- existing = JSON.parse(readFileSync10(summaryPath, "utf-8"));
11974
+ existing = JSON.parse(readFileSync11(summaryPath, "utf-8"));
11846
11975
  }
11847
11976
  existing.sessions.push(...sessionRecords);
11848
11977
  writeFileSync7(
@@ -11895,7 +12024,7 @@ Tracking saved: ${summaryPath}`));
11895
12024
 
11896
12025
  // src/commands/reconcile.ts
11897
12026
  import { Command as Command8 } from "commander";
11898
- import { readFileSync as readFileSync11, existsSync as existsSync11 } from "fs";
12027
+ import { readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
11899
12028
  import { resolve as resolve11 } from "path";
11900
12029
  import chalk9 from "chalk";
11901
12030
  import Table6 from "cli-table3";
@@ -11924,8 +12053,8 @@ var reconcileCommand = new Command8("reconcile").description("Compare estimates
11924
12053
  );
11925
12054
  return;
11926
12055
  }
11927
- const registry = JSON.parse(readFileSync11(registryPath, "utf-8"));
11928
- 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"));
11929
12058
  const projectName = options.project ?? registry.project ?? getProjectName();
11930
12059
  const data = loadData();
11931
12060
  const actuals = /* @__PURE__ */ new Map();
@@ -12100,7 +12229,7 @@ Overall: ${totalActual.toLocaleString()} / ${totalEstimated.toLocaleString()} to
12100
12229
 
12101
12230
  // src/commands/calibrate.ts
12102
12231
  import { Command as Command9 } from "commander";
12103
- import { readFileSync as readFileSync12, existsSync as existsSync12 } from "fs";
12232
+ import { readFileSync as readFileSync13, existsSync as existsSync12 } from "fs";
12104
12233
  import { resolve as resolve12 } from "path";
12105
12234
  import chalk10 from "chalk";
12106
12235
  import Table7 from "cli-table3";
@@ -12121,8 +12250,8 @@ var calibrateCommand = new Command9("calibrate").description("Show calibration d
12121
12250
  );
12122
12251
  return;
12123
12252
  }
12124
- const registry = JSON.parse(readFileSync12(registryPath, "utf-8"));
12125
- 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"));
12126
12255
  const projectName = options.project ?? registry.project ?? getProjectName();
12127
12256
  const actuals = /* @__PURE__ */ new Map();
12128
12257
  for (const session of summary.sessions) {
@@ -12232,7 +12361,7 @@ Calibration summary (${drifts.length} features with data):`));
12232
12361
  // src/commands/estimate.ts
12233
12362
  import { Command as Command10 } from "commander";
12234
12363
  import chalk11 from "chalk";
12235
- 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(
12236
12365
  async (description, options) => {
12237
12366
  const data = loadData();
12238
12367
  const feature = {
@@ -12249,7 +12378,7 @@ var estimateCommand = new Command10("estimate").description("Quick single-featur
12249
12378
  if (options.complexity) {
12250
12379
  complexitySource = "manual";
12251
12380
  } else if (options.ai !== false) {
12252
- const aiResult = await scoreComplexityAI(description);
12381
+ const aiResult = await scoreComplexityAI(description, { provider: options.provider });
12253
12382
  if (aiResult) {
12254
12383
  feature.complexity = aiResult.complexity;
12255
12384
  feature.dependencies = aiResult.dependencies;
@@ -12260,7 +12389,7 @@ var estimateCommand = new Command10("estimate").description("Quick single-featur
12260
12389
  feature.complexity = scoreComplexity(feature);
12261
12390
  console.log(
12262
12391
  chalk11.dim(
12263
- " (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"
12264
12393
  )
12265
12394
  );
12266
12395
  }
@@ -12316,7 +12445,7 @@ var estimateCommand = new Command10("estimate").description("Quick single-featur
12316
12445
 
12317
12446
  // src/commands/validate.ts
12318
12447
  import { Command as Command11 } from "commander";
12319
- import { readFileSync as readFileSync13, existsSync as existsSync13 } from "fs";
12448
+ import { readFileSync as readFileSync14, existsSync as existsSync13 } from "fs";
12320
12449
  import { resolve as resolve13 } from "path";
12321
12450
  import chalk12 from "chalk";
12322
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) => {
@@ -12325,7 +12454,7 @@ var validateCommand = new Command11("validate").description("Validate a spec fil
12325
12454
  console.error(chalk12.red(`Spec file not found: ${fullPath}`));
12326
12455
  process.exit(1);
12327
12456
  }
12328
- const content3 = readFileSync13(fullPath, "utf-8");
12457
+ const content3 = readFileSync14(fullPath, "utf-8");
12329
12458
  console.log(chalk12.bold("\nFathom \u2014 Validate Spec"));
12330
12459
  console.log(chalk12.dim("\u2500".repeat(50)));
12331
12460
  console.log(chalk12.dim(`File: ${fullPath}`));
@@ -12394,7 +12523,7 @@ var validateCommand = new Command11("validate").description("Validate a spec fil
12394
12523
 
12395
12524
  // src/commands/velocity.ts
12396
12525
  import { Command as Command12 } from "commander";
12397
- import { readFileSync as readFileSync14, existsSync as existsSync14 } from "fs";
12526
+ import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
12398
12527
  import { resolve as resolve14 } from "path";
12399
12528
  import chalk13 from "chalk";
12400
12529
  import Table8 from "cli-table3";
@@ -12717,8 +12846,8 @@ var velocityCommand = new Command12("velocity").description("Show velocity metri
12717
12846
  );
12718
12847
  return;
12719
12848
  }
12720
- const registry = JSON.parse(readFileSync14(registryPath, "utf-8"));
12721
- 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"));
12722
12851
  const projectName = options.project ?? registry.project ?? getProjectName();
12723
12852
  const data = loadData();
12724
12853
  const featureDataMap = /* @__PURE__ */ new Map();
@@ -12855,10 +12984,24 @@ Fathom \u2014 Velocity: ${projectName}`));
12855
12984
 
12856
12985
  // src/commands/init.ts
12857
12986
  import { Command as Command13 } from "commander";
12858
- import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync9, existsSync as existsSync15 } from "fs";
12859
- 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";
12860
12997
  import chalk14 from "chalk";
12861
- 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)));
12862
13005
  const configDir = resolve15(
12863
13006
  process.env.HOME ?? process.env.USERPROFILE ?? ".",
12864
13007
  ".config",
@@ -12869,9 +13012,7 @@ var initCommand = new Command13("init").description("Initialize Fathom global co
12869
13012
  let existing = {};
12870
13013
  if (existsSync15(configPath)) {
12871
13014
  try {
12872
- existing = JSON.parse(
12873
- __require("fs").readFileSync(configPath, "utf-8")
12874
- );
13015
+ existing = JSON.parse(readFileSync16(configPath, "utf-8"));
12875
13016
  } catch {
12876
13017
  }
12877
13018
  }
@@ -12883,29 +13024,80 @@ var initCommand = new Command13("init").description("Initialize Fathom global co
12883
13024
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
12884
13025
  };
12885
13026
  writeFileSync8(configPath, JSON.stringify(config, null, 2));
12886
- console.log(chalk14.bold("\nFathom \u2014 Init"));
12887
- console.log(chalk14.dim("\u2500".repeat(40)));
13027
+ console.log(chalk14.green(` \u2713 Global config: ${configPath}`));
12888
13028
  if (options.convexUrl) {
12889
- console.log(chalk14.green(` \u2713 Convex URL: ${options.convexUrl}`));
13029
+ console.log(chalk14.dim(` Convex URL: ${options.convexUrl}`));
12890
13030
  }
12891
13031
  if (options.team) {
12892
- console.log(chalk14.green(` \u2713 Team: ${options.team}`));
13032
+ console.log(chalk14.dim(` Team: ${options.team}`));
12893
13033
  }
12894
- if (options.adminKey) {
12895
- 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 {
12896
13041
  }
12897
- console.log(chalk14.dim(`
12898
- Config saved: ${configPath}`));
12899
- console.log(
12900
- chalk14.dim(
12901
- " Set CONVEX_URL env var or pass --convex-url to enable sync.\n"
12902
- )
12903
- );
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"));
12904
13096
  });
12905
13097
 
12906
13098
  // src/commands/rename.ts
12907
13099
  import { Command as Command14 } from "commander";
12908
- 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";
12909
13101
  import { resolve as resolve16 } from "path";
12910
13102
  import chalk15 from "chalk";
12911
13103
  var renameCommand = new Command14("rename").description("Rename the project in all .claude/te/ config files").argument("<name>", "New project name").action((name) => {
@@ -12926,7 +13118,7 @@ var renameCommand = new Command14("rename").description("Rename the project in a
12926
13118
  let updated = 0;
12927
13119
  for (const filePath of jsonFiles) {
12928
13120
  if (!existsSync16(filePath)) continue;
12929
- const data = JSON.parse(readFileSync15(filePath, "utf-8"));
13121
+ const data = JSON.parse(readFileSync17(filePath, "utf-8"));
12930
13122
  if (data.project) {
12931
13123
  oldName = data.project;
12932
13124
  data.project = name;
@@ -12936,7 +13128,7 @@ var renameCommand = new Command14("rename").description("Rename the project in a
12936
13128
  }
12937
13129
  const skillPath = resolve16(baseDir, ".claude", "skills", "fathom", "SKILL.md");
12938
13130
  if (existsSync16(skillPath)) {
12939
- const content3 = readFileSync15(skillPath, "utf-8");
13131
+ const content3 = readFileSync17(skillPath, "utf-8");
12940
13132
  const newContent = content3.replaceAll(oldName, name);
12941
13133
  if (newContent !== content3) {
12942
13134
  writeFileSync9(skillPath, newContent);
@@ -12945,7 +13137,7 @@ var renameCommand = new Command14("rename").description("Rename the project in a
12945
13137
  }
12946
13138
  const hookPath = resolve16(baseDir, ".claude", "hooks", "te-session-sync.sh");
12947
13139
  if (existsSync16(hookPath)) {
12948
- const content3 = readFileSync15(hookPath, "utf-8");
13140
+ const content3 = readFileSync17(hookPath, "utf-8");
12949
13141
  const newContent = content3.replaceAll(oldName, name);
12950
13142
  if (newContent !== content3) {
12951
13143
  writeFileSync9(hookPath, newContent);
@@ -12961,26 +13153,485 @@ var renameCommand = new Command14("rename").description("Rename the project in a
12961
13153
 
12962
13154
  // src/commands/research.ts
12963
13155
  import { Command as Command15 } from "commander";
13156
+ import { writeFileSync as writeFileSync10, existsSync as existsSync17 } from "fs";
13157
+ import { resolve as resolve17 } from "path";
12964
13158
  import chalk16 from "chalk";
12965
- var researchCommand = new Command15("research").description("Check for pricing updates and intelligence improvements").action(async () => {
12966
- console.log(chalk16.bold("\nFathom \u2014 Research Pulse"));
12967
- console.log(chalk16.dim("\u2500".repeat(50)));
12968
- console.log(chalk16.dim(" Checking for updates...\n"));
12969
- console.log(chalk16.yellow(" Research pulse not yet implemented."));
12970
- console.log(chalk16.dim(" This will check for model pricing changes, new guardrail templates,"));
12971
- 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"])
13172
+ });
13173
+ var IntelligenceResponse = external_exports.object({
13174
+ suggestions: external_exports.array(IntelligenceSuggestion),
13175
+ summary: external_exports.string()
12972
13176
  });
12973
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
+ }
13624
+
12974
13625
  // src/commands/project.ts
12975
13626
  import { Command as Command16 } from "commander";
12976
- import { existsSync as existsSync17 } from "fs";
12977
- import { resolve as resolve17 } from "path";
13627
+ import { existsSync as existsSync18 } from "fs";
13628
+ import { resolve as resolve18 } from "path";
12978
13629
  import chalk17 from "chalk";
12979
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) => {
12980
13631
  try {
12981
13632
  const cwd = process.cwd();
12982
- const intentPath = resolve17(cwd, ".fathom", "intent.yaml");
12983
- if (!existsSync17(intentPath)) {
13633
+ const intentPath = resolve18(cwd, ".fathom", "intent.yaml");
13634
+ if (!existsSync18(intentPath)) {
12984
13635
  console.log(chalk17.yellow("\n No project intent found."));
12985
13636
  console.log(chalk17.dim(" Run `fathom` to set up a project with intent capture.\n"));
12986
13637
  return;
@@ -13039,13 +13690,13 @@ var projectCommand = new Command16("project").description("View or update projec
13039
13690
  console.log(chalk17.bold(" Opinions:") + " " + intent.tech.opinions.join(", "));
13040
13691
  }
13041
13692
  }
13042
- const projectionsDir = resolve17(cwd, ".fathom", "projections");
13043
- const hasProjections = existsSync17(projectionsDir);
13693
+ const projectionsDir = resolve18(cwd, ".fathom", "projections");
13694
+ const hasProjections = existsSync18(projectionsDir);
13044
13695
  console.log(chalk17.bold("\n Projections:") + (hasProjections ? chalk17.green(" generated") : chalk17.yellow(" not generated")));
13045
13696
  if (hasProjections) {
13046
13697
  const targets = ["claude/SKILL.md", "cursor/.cursorrules", "generic/SYSTEM_PROMPT.md"];
13047
13698
  for (const t of targets) {
13048
- const exists = existsSync17(resolve17(projectionsDir, t));
13699
+ const exists = existsSync18(resolve18(projectionsDir, t));
13049
13700
  console.log(` ${exists ? "\u2713" : "\xB7"} ${t}`);
13050
13701
  }
13051
13702
  }
@@ -13062,14 +13713,14 @@ var projectCommand = new Command16("project").description("View or update projec
13062
13713
 
13063
13714
  // src/commands/report.ts
13064
13715
  import { Command as Command17 } from "commander";
13065
- import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, existsSync as existsSync18 } from "fs";
13066
- 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";
13067
13718
  import chalk18 from "chalk";
13068
13719
 
13069
13720
  // ../store/dist/index.js
13070
13721
  import { randomUUID } from "crypto";
13071
13722
  import { mkdir, readFile, rename, writeFile } from "fs/promises";
13072
- import { dirname as dirname6, join as join3 } from "path";
13723
+ import { dirname as dirname7, join as join3 } from "path";
13073
13724
  import { readFile as readFile2 } from "fs/promises";
13074
13725
  import { join as join22 } from "path";
13075
13726
  var OverheadBreakdownSchema = external_exports.object({
@@ -13260,7 +13911,7 @@ function generateId() {
13260
13911
  return randomUUID();
13261
13912
  }
13262
13913
  async function ensureDir(path) {
13263
- await mkdir(dirname6(path), { recursive: true });
13914
+ await mkdir(dirname7(path), { recursive: true });
13264
13915
  }
13265
13916
  async function readJsonFile(path) {
13266
13917
  try {
@@ -14693,12 +15344,12 @@ Fathom \u2014 Project Report: ${projectName}`));
14693
15344
  "reports",
14694
15345
  `${projectName}-${today}.${ext}`
14695
15346
  );
14696
- const outputPath = options.output ? resolve18(options.output) : defaultPath;
14697
- const dir = resolve18(outputPath, "..");
14698
- if (!existsSync18(dir)) {
15347
+ const outputPath = options.output ? resolve19(options.output) : defaultPath;
15348
+ const dir = resolve19(outputPath, "..");
15349
+ if (!existsSync19(dir)) {
14699
15350
  mkdirSync10(dir, { recursive: true });
14700
15351
  }
14701
- writeFileSync10(outputPath, report, "utf-8");
15352
+ writeFileSync11(outputPath, report, "utf-8");
14702
15353
  console.log(
14703
15354
  chalk18.green(`
14704
15355
  \u2713 Report generated: ${outputPath}`)
@@ -14714,17 +15365,17 @@ Fathom \u2014 Project Report: ${projectName}`));
14714
15365
 
14715
15366
  // src/commands/contribute.ts
14716
15367
  import { Command as Command18 } from "commander";
14717
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync11, existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
14718
- 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";
14719
15370
  import chalk19 from "chalk";
14720
- import { confirm as confirm3 } from "@inquirer/prompts";
15371
+ import { confirm as confirm4 } from "@inquirer/prompts";
14721
15372
  function loadCalibratedProfiles() {
14722
15373
  const profiles = [];
14723
- const registryPath = resolve19(process.cwd(), ".claude", "te", "registry.json");
14724
- const summaryPath = resolve19(process.cwd(), ".claude", "te", "tracking", "summary.json");
14725
- if (existsSync19(registryPath) && existsSync19(summaryPath)) {
14726
- const registry = JSON.parse(readFileSync16(registryPath, "utf-8"));
14727
- 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"));
14728
15379
  const actuals = /* @__PURE__ */ new Map();
14729
15380
  for (const session of summary.sessions ?? []) {
14730
15381
  if (!session.featureId || session.status === "untagged") continue;
@@ -14801,7 +15452,7 @@ var contributeCommand = new Command18("contribute").description("Share anonymize
14801
15452
  }
14802
15453
  if (!options.yes) {
14803
15454
  try {
14804
- const confirmed = await confirm3({
15455
+ const confirmed = await confirm4({
14805
15456
  message: "Share this data?",
14806
15457
  default: false
14807
15458
  });
@@ -14814,12 +15465,12 @@ var contributeCommand = new Command18("contribute").description("Share anonymize
14814
15465
  return;
14815
15466
  }
14816
15467
  }
14817
- const contributionsDir = resolve19(process.cwd(), ".fathom", "contributions");
15468
+ const contributionsDir = resolve20(process.cwd(), ".fathom", "contributions");
14818
15469
  mkdirSync11(contributionsDir, { recursive: true });
14819
15470
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
14820
- const outputPath = resolve19(contributionsDir, `${today}.json`);
15471
+ const outputPath = resolve20(contributionsDir, `${today}.json`);
14821
15472
  const json = exportContribution(dataPoints);
14822
- writeFileSync11(outputPath, json, "utf-8");
15473
+ writeFileSync12(outputPath, json, "utf-8");
14823
15474
  console.log(chalk19.green(`
14824
15475
  \u2713 Contribution saved to .fathom/contributions/${today}.json`));
14825
15476
  console.log(chalk19.green(" \u2713 Thank you! This helps make Fathom better for everyone."));
@@ -14828,14 +15479,14 @@ var contributeCommand = new Command18("contribute").description("Share anonymize
14828
15479
 
14829
15480
  // src/commands/feedback.ts
14830
15481
  import { Command as Command19 } from "commander";
14831
- import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync12, readFileSync as readFileSync17, existsSync as existsSync20 } from "fs";
14832
- 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";
14833
15484
  import chalk20 from "chalk";
14834
15485
  function getIntentSummary() {
14835
15486
  try {
14836
- const intentPath = resolve20(process.cwd(), ".fathom", "intent.yaml");
14837
- if (!existsSync20(intentPath)) return {};
14838
- 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");
14839
15490
  const projectMatch = content3.match(/^project:\s*(.+)$/m);
14840
15491
  const descMatch = content3.match(/^description:\s*["']?(.+?)["']?\s*$/m);
14841
15492
  return {
@@ -14855,7 +15506,7 @@ function getSystemInfo() {
14855
15506
  };
14856
15507
  }
14857
15508
  function saveFeedback(type, description) {
14858
- const feedbackDir = resolve20(process.cwd(), ".fathom", "feedback");
15509
+ const feedbackDir = resolve21(process.cwd(), ".fathom", "feedback");
14859
15510
  mkdirSync12(feedbackDir, { recursive: true });
14860
15511
  const sys = getSystemInfo();
14861
15512
  const intent = getIntentSummary();
@@ -14893,7 +15544,7 @@ function saveFeedback(type, description) {
14893
15544
  "Review this file, add any additional details, then email to feedback@fathom.dev"
14894
15545
  );
14895
15546
  lines.push("");
14896
- writeFileSync12(filepath, lines.join("\n"));
15547
+ writeFileSync13(filepath, lines.join("\n"));
14897
15548
  return filepath;
14898
15549
  }
14899
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(
@@ -14956,8 +15607,8 @@ import { Command as Command20 } from "commander";
14956
15607
  import chalk21 from "chalk";
14957
15608
 
14958
15609
  // ../sync/dist/index.js
14959
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, existsSync as existsSync21 } from "fs";
14960
- 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";
14961
15612
  var SyncStatusSchema = external_exports.enum([
14962
15613
  "backlog",
14963
15614
  "todo",
@@ -15637,20 +16288,20 @@ function createProvider(config, apiKey) {
15637
16288
  }
15638
16289
  function getLedgerPath(basePath) {
15639
16290
  const base = basePath ?? process.cwd();
15640
- return resolve21(base, LEDGER_DIR, LEDGER_FILE);
16291
+ return resolve22(base, LEDGER_DIR, LEDGER_FILE);
15641
16292
  }
15642
16293
  function loadLedger(ledgerPath) {
15643
- if (existsSync21(ledgerPath)) {
16294
+ if (existsSync23(ledgerPath)) {
15644
16295
  try {
15645
- return JSON.parse(readFileSync18(ledgerPath, "utf-8"));
16296
+ return JSON.parse(readFileSync20(ledgerPath, "utf-8"));
15646
16297
  } catch {
15647
16298
  }
15648
16299
  }
15649
16300
  return { provider: "", projectKey: "", items: [], lastSync: 0 };
15650
16301
  }
15651
16302
  function saveLedger(ledgerPath, ledger) {
15652
- mkdirSync13(dirname7(ledgerPath), { recursive: true });
15653
- writeFileSync13(ledgerPath, JSON.stringify(ledger, null, 2) + "\n");
16303
+ mkdirSync13(dirname8(ledgerPath), { recursive: true });
16304
+ writeFileSync14(ledgerPath, JSON.stringify(ledger, null, 2) + "\n");
15654
16305
  }
15655
16306
  function createSyncManager(config, options) {
15656
16307
  const apiKey = resolveApiKey(config);
@@ -15709,14 +16360,14 @@ function createSyncManager(config, options) {
15709
16360
  }
15710
16361
 
15711
16362
  // src/commands/sync.ts
15712
- import { readFileSync as readFileSync19, existsSync as existsSync23 } from "fs";
15713
- import { resolve as resolve22 } from "path";
16363
+ import { readFileSync as readFileSync21, existsSync as existsSync24 } from "fs";
16364
+ import { resolve as resolve23 } from "path";
15714
16365
  function loadSyncConfig(options) {
15715
- const configPath = resolve22(process.cwd(), ".fathom", "sync-config.json");
16366
+ const configPath = resolve23(process.cwd(), ".fathom", "sync-config.json");
15716
16367
  let config = {};
15717
- if (existsSync23(configPath)) {
16368
+ if (existsSync24(configPath)) {
15718
16369
  try {
15719
- config = JSON.parse(readFileSync19(configPath, "utf-8"));
16370
+ config = JSON.parse(readFileSync21(configPath, "utf-8"));
15720
16371
  } catch {
15721
16372
  }
15722
16373
  }
@@ -15729,10 +16380,10 @@ function loadSyncConfig(options) {
15729
16380
  };
15730
16381
  }
15731
16382
  function loadPendingSlices() {
15732
- const registryPath = resolve22(process.cwd(), ".claude", "te", "registry.json");
15733
- if (!existsSync23(registryPath)) return [];
16383
+ const registryPath = resolve23(process.cwd(), ".claude", "te", "registry.json");
16384
+ if (!existsSync24(registryPath)) return [];
15734
16385
  try {
15735
- const data = JSON.parse(readFileSync19(registryPath, "utf-8"));
16386
+ const data = JSON.parse(readFileSync21(registryPath, "utf-8"));
15736
16387
  if (!Array.isArray(data.features)) return [];
15737
16388
  return data.features.map((f) => ({
15738
16389
  sliceId: f.id,
@@ -15827,8 +16478,94 @@ Fathom \u2014 Sync to ${config.provider.charAt(0).toUpperCase() + config.provide
15827
16478
  }
15828
16479
  });
15829
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-token-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
+
15830
16567
  // src/index.ts
15831
- var program = new Command21();
16568
+ var program = new Command22();
15832
16569
  program.name("fathom").description("Workflow intelligence platform for AI-augmented development").version(VERSION);
15833
16570
  program.addCommand(goCommand, { isDefault: true });
15834
16571
  program.addCommand(intakeCommand);
@@ -15845,6 +16582,7 @@ program.addCommand(projectCommand);
15845
16582
  program.addCommand(contributeCommand);
15846
16583
  program.addCommand(feedbackCommand);
15847
16584
  program.addCommand(syncCommand);
16585
+ program.addCommand(installPluginCommand);
15848
16586
  program.addCommand(statusCommand);
15849
16587
  program.addCommand(pricingCommand);
15850
16588
  program.addCommand(validateCommand);