fathom-cli 0.1.0 → 0.1.2

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
@@ -1,10 +1,39 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- __commonJS,
4
- __export,
5
- __require,
6
- __toESM
7
- } from "./chunk-SEGVTWSK.js";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
10
+ }) : x)(function(x) {
11
+ if (typeof require !== "undefined") return require.apply(this, arguments);
12
+ throw Error('Dynamic require of "' + x + '" is not supported');
13
+ });
14
+ var __commonJS = (cb, mod) => function __require2() {
15
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
16
+ };
17
+ var __export = (target, all2) => {
18
+ for (var name in all2)
19
+ __defProp(target, name, { get: all2[name], enumerable: true });
20
+ };
21
+ var __copyProps = (to, from, except, desc) => {
22
+ if (from && typeof from === "object" || typeof from === "function") {
23
+ for (let key of __getOwnPropNames(from))
24
+ if (!__hasOwnProp.call(to, key) && key !== except)
25
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
26
+ }
27
+ return to;
28
+ };
29
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
30
+ // If the importer is in node compatibility mode or this is not an ESM
31
+ // file that has been converted to a CommonJS file using a Babel-
32
+ // compatible transform (i.e. "__esModule" has not been set), then set
33
+ // "default" to the CommonJS "module.exports" for node compatibility.
34
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
35
+ mod
36
+ ));
8
37
 
9
38
  // ../../node_modules/.pnpm/extend@3.0.2/node_modules/extend/index.js
10
39
  var require_extend = __commonJS({
@@ -98,7 +127,7 @@ var require_extend = __commonJS({
98
127
  });
99
128
 
100
129
  // src/index.ts
101
- import { Command as Command13 } from "commander";
130
+ import { Command as Command14 } from "commander";
102
131
 
103
132
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
104
133
  var external_exports = {};
@@ -5141,7 +5170,7 @@ var Processor = class _Processor extends CallableInstance {
5141
5170
  assertParser("process", this.parser || this.Parser);
5142
5171
  assertCompiler("process", this.compiler || this.Compiler);
5143
5172
  return done ? executor(void 0, done) : new Promise(executor);
5144
- function executor(resolve12, reject) {
5173
+ function executor(resolve13, reject) {
5145
5174
  const realFile = vfile(file);
5146
5175
  const parseTree = (
5147
5176
  /** @type {HeadTree extends undefined ? Node : HeadTree} */
@@ -5172,8 +5201,8 @@ var Processor = class _Processor extends CallableInstance {
5172
5201
  function realDone(error, file2) {
5173
5202
  if (error || !file2) {
5174
5203
  reject(error);
5175
- } else if (resolve12) {
5176
- resolve12(file2);
5204
+ } else if (resolve13) {
5205
+ resolve13(file2);
5177
5206
  } else {
5178
5207
  ok(done, "`done` is defined if `resolve` is not");
5179
5208
  done(void 0, file2);
@@ -5275,7 +5304,7 @@ var Processor = class _Processor extends CallableInstance {
5275
5304
  file = void 0;
5276
5305
  }
5277
5306
  return done ? executor(void 0, done) : new Promise(executor);
5278
- function executor(resolve12, reject) {
5307
+ function executor(resolve13, reject) {
5279
5308
  ok(
5280
5309
  typeof file !== "function",
5281
5310
  "`file` can\u2019t be a `done` anymore, we checked"
@@ -5289,8 +5318,8 @@ var Processor = class _Processor extends CallableInstance {
5289
5318
  );
5290
5319
  if (error) {
5291
5320
  reject(error);
5292
- } else if (resolve12) {
5293
- resolve12(resultingTree);
5321
+ } else if (resolve13) {
5322
+ resolve13(resultingTree);
5294
5323
  } else {
5295
5324
  ok(done, "`done` is defined if `resolve` is not");
5296
5325
  done(void 0, resultingTree, file2);
@@ -8118,10 +8147,10 @@ function resolveAll(constructs2, events, context) {
8118
8147
  const called = [];
8119
8148
  let index2 = -1;
8120
8149
  while (++index2 < constructs2.length) {
8121
- const resolve12 = constructs2[index2].resolveAll;
8122
- if (resolve12 && !called.includes(resolve12)) {
8123
- events = resolve12(events, context);
8124
- called.push(resolve12);
8150
+ const resolve13 = constructs2[index2].resolveAll;
8151
+ if (resolve13 && !called.includes(resolve13)) {
8152
+ events = resolve13(events, context);
8153
+ called.push(resolve13);
8125
8154
  }
8126
8155
  }
8127
8156
  return events;
@@ -15638,6 +15667,62 @@ function generateRegistry(project, features, estimates) {
15638
15667
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
15639
15668
  };
15640
15669
  }
15670
+ async function getAnthropicClient(apiKey) {
15671
+ const { default: AnthropicSDK } = await import("@anthropic-ai/sdk");
15672
+ return new AnthropicSDK({ apiKey: apiKey ?? process.env.ANTHROPIC_API_KEY });
15673
+ }
15674
+ var AIComplexityResult = external_exports.object({
15675
+ complexity: Complexity,
15676
+ taskTypes: external_exports.array(TaskType),
15677
+ dependencies: external_exports.array(external_exports.string()),
15678
+ aiTasks: external_exports.array(external_exports.string()),
15679
+ rationale: external_exports.string()
15680
+ });
15681
+ var TASK_TYPE_VALUES = TaskType.options.join(", ");
15682
+ var SYSTEM_PROMPT = `You are a technical complexity scorer for software features. Given a feature description, analyze it and return a JSON object with:
15683
+
15684
+ - complexity: One of "S", "M", "L", "XL"
15685
+ - S: Single file, simple change, <30 min
15686
+ - M: 2-5 files, typical feature, 1-3 hours
15687
+ - L: 5-15 files, multiple integrations, 3-8 hours
15688
+ - XL: 15+ files, system-wide changes, 8+ hours
15689
+ - taskTypes: Array of applicable task types from: ${TASK_TYPE_VALUES}
15690
+ - dependencies: Array of likely external dependencies or integrations (e.g., "OAuth provider", "database migration")
15691
+ - aiTasks: Array of 2-5 discrete AI coding tasks (e.g., "scaffold-auth-flow", "write-api-tests")
15692
+ - rationale: One sentence explaining the complexity assessment
15693
+
15694
+ Respond with ONLY a JSON object. No markdown, no explanation.`;
15695
+ async function scoreComplexityAI(description, options = {}) {
15696
+ try {
15697
+ const client = options.client ?? await getAnthropicClient(options.apiKey);
15698
+ const model = options.model ?? "claude-haiku-4-5-20251001";
15699
+ const response = await client.messages.create({
15700
+ model,
15701
+ max_tokens: 1024,
15702
+ system: SYSTEM_PROMPT,
15703
+ messages: [
15704
+ {
15705
+ role: "user",
15706
+ content: `Analyze the complexity of this feature:
15707
+
15708
+ ${description}`
15709
+ }
15710
+ ]
15711
+ });
15712
+ const textBlock = response.content.find((block) => block.type === "text");
15713
+ if (!textBlock || textBlock.type !== "text") {
15714
+ return null;
15715
+ }
15716
+ let jsonText = textBlock.text.trim();
15717
+ if (jsonText.startsWith("```")) {
15718
+ jsonText = jsonText.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
15719
+ }
15720
+ const parsed = JSON.parse(jsonText);
15721
+ return AIComplexityResult.parse(parsed);
15722
+ } catch {
15723
+ return null;
15724
+ }
15725
+ }
15641
15726
  var VERSION = "0.1.0";
15642
15727
 
15643
15728
  // src/commands/analyze.ts
@@ -16957,8 +17042,8 @@ Calibration summary (${drifts.length} features with data):`));
16957
17042
  // src/commands/estimate.ts
16958
17043
  import { Command as Command8 } from "commander";
16959
17044
  import chalk9 from "chalk";
16960
- var estimateCommand = new Command8("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").action(
16961
- (description, options) => {
17045
+ var estimateCommand = new Command8("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(
17046
+ async (description, options) => {
16962
17047
  const data = loadData();
16963
17048
  const feature = {
16964
17049
  id: "quick-estimate",
@@ -16969,7 +17054,27 @@ var estimateCommand = new Command8("estimate").description("Quick single-feature
16969
17054
  dependencies: [],
16970
17055
  aiTasks: []
16971
17056
  };
16972
- if (!options.complexity) {
17057
+ let complexitySource = "heuristic";
17058
+ let rationale;
17059
+ if (options.complexity) {
17060
+ complexitySource = "manual";
17061
+ } else if (options.ai !== false) {
17062
+ const aiResult = await scoreComplexityAI(description);
17063
+ if (aiResult) {
17064
+ feature.complexity = aiResult.complexity;
17065
+ feature.dependencies = aiResult.dependencies;
17066
+ feature.aiTasks = aiResult.aiTasks;
17067
+ complexitySource = "AI";
17068
+ rationale = aiResult.rationale;
17069
+ } else {
17070
+ feature.complexity = scoreComplexity(feature);
17071
+ console.log(
17072
+ chalk9.dim(
17073
+ " (AI scoring unavailable \u2014 using heuristic. Set ANTHROPIC_API_KEY for richer estimates)\n"
17074
+ )
17075
+ );
17076
+ }
17077
+ } else {
16973
17078
  feature.complexity = scoreComplexity(feature);
16974
17079
  }
16975
17080
  const taskTypes = mapTaskTypes(feature);
@@ -16978,7 +17083,12 @@ var estimateCommand = new Command8("estimate").description("Quick single-feature
16978
17083
  console.log(chalk9.bold("\nFathom \u2014 Quick Estimate"));
16979
17084
  console.log(chalk9.dim("\u2500".repeat(50)));
16980
17085
  console.log(` Feature: ${feature.name}`);
16981
- console.log(` Complexity: ${feature.complexity}`);
17086
+ console.log(
17087
+ ` Complexity: ${feature.complexity} ${chalk9.dim(`(${complexitySource})`)}`
17088
+ );
17089
+ if (rationale) {
17090
+ console.log(` Rationale: ${chalk9.dim(rationale)}`);
17091
+ }
16982
17092
  console.log(` Tasks: ${taskTypes.join(", ")}`);
16983
17093
  console.log(` Model: ${rec.model} (${rec.reason})`);
16984
17094
  console.log(chalk9.dim("\u2500".repeat(50)));
@@ -17603,8 +17713,8 @@ var IntakeResult = external_exports.object({
17603
17713
  totalEstimatedCost: external_exports.number(),
17604
17714
  createdAt: external_exports.string()
17605
17715
  });
17606
- async function getAnthropicClient(apiKey) {
17607
- const { default: AnthropicSDK } = await import("./sdk-OEGEIPVM.js");
17716
+ async function getAnthropicClient2(apiKey) {
17717
+ const { default: AnthropicSDK } = await import("@anthropic-ai/sdk");
17608
17718
  return new AnthropicSDK({ apiKey: apiKey ?? process.env.ANTHROPIC_API_KEY });
17609
17719
  }
17610
17720
  var EXTRACTION_SYSTEM_PROMPT = `You are a technical project analyst. Your job is to extract discrete, actionable work items from raw input (user feedback, bug reports, meeting notes, design handoffs, etc.).
@@ -17624,8 +17734,8 @@ Split compound requests into separate items. Identify implicit requirements. Be
17624
17734
  Respond with ONLY a JSON array of items. No markdown, no explanation.`;
17625
17735
  var ExtractionResponse = external_exports.array(ExtractedItem);
17626
17736
  async function extractWorkItems(rawInput, options = {}) {
17627
- const client = options.client ?? await getAnthropicClient(options.apiKey);
17628
- const model = options.model ?? "claude-sonnet-4-6-20250514";
17737
+ const client = options.client ?? await getAnthropicClient2(options.apiKey);
17738
+ const model = options.model ?? "claude-sonnet-4-20250514";
17629
17739
  const response = await client.messages.create({
17630
17740
  model,
17631
17741
  max_tokens: 4096,
@@ -17941,8 +18051,479 @@ var initCommand = new Command12("init").description("Initialize Fathom global co
17941
18051
  );
17942
18052
  });
17943
18053
 
18054
+ // src/commands/research.ts
18055
+ import { Command as Command13 } from "commander";
18056
+ import { writeFileSync as writeFileSync5, existsSync as existsSync12 } from "fs";
18057
+ import { resolve as resolve12 } from "path";
18058
+ import chalk14 from "chalk";
18059
+
18060
+ // src/fetchers/types.ts
18061
+ var RemoteManifest = external_exports.object({
18062
+ version: external_exports.number(),
18063
+ updated: external_exports.string(),
18064
+ models: external_exports.array(ModelPricing),
18065
+ recommendations: external_exports.record(external_exports.string(), AgentRecommendation)
18066
+ });
18067
+ var IntelligenceSuggestion = external_exports.object({
18068
+ type: external_exports.enum(["new-model", "price-change", "recommendation-change", "deprecation", "general"]),
18069
+ model: external_exports.string().optional(),
18070
+ description: external_exports.string(),
18071
+ confidence: external_exports.enum(["high", "medium", "low"])
18072
+ });
18073
+ var IntelligenceResponse = external_exports.object({
18074
+ suggestions: external_exports.array(IntelligenceSuggestion),
18075
+ summary: external_exports.string()
18076
+ });
18077
+
18078
+ // src/fetchers/pricing-fetcher.ts
18079
+ var MANIFEST_URL = "https://raw.githubusercontent.com/anthropics/fathom/main/data/remote-manifest.json";
18080
+ var DEFAULT_TIMEOUT_MS = 1e4;
18081
+ async function fetchPricing(options = {}) {
18082
+ const now = (/* @__PURE__ */ new Date()).toISOString();
18083
+ if (options.offline) {
18084
+ return loadLocalPricing(now);
18085
+ }
18086
+ try {
18087
+ const controller = new AbortController();
18088
+ const timeoutId = setTimeout(
18089
+ () => controller.abort(),
18090
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS
18091
+ );
18092
+ if (options.verbose) {
18093
+ console.log(` Fetching: ${MANIFEST_URL}`);
18094
+ }
18095
+ const response = await fetch(MANIFEST_URL, { signal: controller.signal });
18096
+ clearTimeout(timeoutId);
18097
+ if (!response.ok) {
18098
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
18099
+ }
18100
+ const json2 = await response.json();
18101
+ const manifest = RemoteManifest.parse(json2);
18102
+ return {
18103
+ data: {
18104
+ models: manifest.models,
18105
+ recommendations: manifest.recommendations,
18106
+ manifestVersion: manifest.version,
18107
+ manifestDate: manifest.updated
18108
+ },
18109
+ source: "remote",
18110
+ stale: false,
18111
+ fetchedAt: now
18112
+ };
18113
+ } catch (err) {
18114
+ const errorMsg = err instanceof Error ? err.message : String(err);
18115
+ if (options.verbose) {
18116
+ console.log(` Remote fetch failed: ${errorMsg}`);
18117
+ }
18118
+ return loadLocalPricing(now, errorMsg);
18119
+ }
18120
+ }
18121
+ function loadLocalPricing(fetchedAt, error) {
18122
+ const data = loadData();
18123
+ return {
18124
+ data: {
18125
+ models: data.models,
18126
+ recommendations: data.recommendations,
18127
+ manifestVersion: 0,
18128
+ manifestDate: "local"
18129
+ },
18130
+ source: "local",
18131
+ stale: true,
18132
+ fetchedAt,
18133
+ error: error ?? "offline mode"
18134
+ };
18135
+ }
18136
+
18137
+ // src/fetchers/intelligence-fetcher.ts
18138
+ async function getAnthropicClient3(apiKey) {
18139
+ const { default: AnthropicSDK } = await import("@anthropic-ai/sdk");
18140
+ return new AnthropicSDK({ apiKey: apiKey ?? process.env.ANTHROPIC_API_KEY });
18141
+ }
18142
+ 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.
18143
+
18144
+ You should:
18145
+ 1. Flag any models with outdated pricing based on your knowledge
18146
+ 2. Suggest new models that should be added (only well-established models from major providers)
18147
+ 3. Flag any recommendation changes (e.g., if a newer model is better suited for a task category)
18148
+ 4. Note any models that may be deprecated or renamed
18149
+
18150
+ Respond with ONLY valid JSON matching this schema:
18151
+ {
18152
+ "suggestions": [
18153
+ {
18154
+ "type": "new-model" | "price-change" | "recommendation-change" | "deprecation" | "general",
18155
+ "model": "model-id (optional)",
18156
+ "description": "what changed or should change",
18157
+ "confidence": "high" | "medium" | "low"
18158
+ }
18159
+ ],
18160
+ "summary": "one sentence overall assessment"
18161
+ }
18162
+
18163
+ If everything looks current, return an empty suggestions array with a summary saying so.`;
18164
+ async function fetchIntelligence(models, recommendations, options = {}) {
18165
+ const now = (/* @__PURE__ */ new Date()).toISOString();
18166
+ const apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY;
18167
+ if (options.offline || !apiKey) {
18168
+ if (options.verbose) {
18169
+ console.log(
18170
+ apiKey ? " Intelligence: skipped (offline mode)" : " Intelligence: skipped (no ANTHROPIC_API_KEY)"
18171
+ );
18172
+ }
18173
+ return null;
18174
+ }
18175
+ try {
18176
+ if (options.verbose) {
18177
+ console.log(" Intelligence: querying Claude Haiku...");
18178
+ }
18179
+ const client = await getAnthropicClient3(apiKey);
18180
+ const modelsDesc = models.map((m) => `${m.id} (${m.name}): $${m.input_per_mtok}/$${m.output_per_mtok} per MTok, tier=${m.tier}`).join("\n");
18181
+ const recsDesc = Object.entries(recommendations).map(([cat, rec]) => `${cat}: ${rec.model} \u2014 ${rec.reason}`).join("\n");
18182
+ const userPrompt = `Current model pricing:
18183
+ ${modelsDesc}
18184
+
18185
+ Current agent recommendations:
18186
+ ${recsDesc}`;
18187
+ const response = await client.messages.create({
18188
+ model: "claude-haiku-4-5-20251001",
18189
+ max_tokens: 1024,
18190
+ system: SYSTEM_PROMPT2,
18191
+ messages: [{ role: "user", content: userPrompt }]
18192
+ });
18193
+ const textBlock = response.content.find((block) => block.type === "text");
18194
+ if (!textBlock || textBlock.type !== "text") {
18195
+ throw new Error("No text content in response");
18196
+ }
18197
+ let jsonText = textBlock.text.trim();
18198
+ if (jsonText.startsWith("```")) {
18199
+ jsonText = jsonText.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
18200
+ }
18201
+ const parsed = JSON.parse(jsonText);
18202
+ const intelligence = IntelligenceResponse.parse(parsed);
18203
+ return {
18204
+ data: intelligence,
18205
+ source: "api",
18206
+ stale: false,
18207
+ fetchedAt: now
18208
+ };
18209
+ } catch (err) {
18210
+ const errorMsg = err instanceof Error ? err.message : String(err);
18211
+ if (options.verbose) {
18212
+ console.log(` Intelligence fetch failed: ${errorMsg}`);
18213
+ }
18214
+ return null;
18215
+ }
18216
+ }
18217
+
18218
+ // src/fetchers/merge.ts
18219
+ function mergePricingData(localModels, remoteModels, localRecs, remoteRecs, suggestions = []) {
18220
+ const mergedModels = [];
18221
+ const modelChanges = [];
18222
+ const seenIds = /* @__PURE__ */ new Set();
18223
+ for (const remote of remoteModels) {
18224
+ seenIds.add(remote.id);
18225
+ const local = localModels.find((m) => m.id === remote.id);
18226
+ if (!local) {
18227
+ mergedModels.push(remote);
18228
+ modelChanges.push({ id: remote.id, name: remote.name, status: "added" });
18229
+ continue;
18230
+ }
18231
+ const fields = [];
18232
+ const pricingKeys = [
18233
+ "input_per_mtok",
18234
+ "output_per_mtok",
18235
+ "cache_read_per_mtok",
18236
+ "batch_discount"
18237
+ ];
18238
+ for (const key of pricingKeys) {
18239
+ if (local[key] !== remote[key]) {
18240
+ fields.push({ field: key, old: local[key], new: remote[key] });
18241
+ }
18242
+ }
18243
+ if (local.tier !== remote.tier) {
18244
+ fields.push({ field: "tier", old: local.tier, new: remote.tier });
18245
+ }
18246
+ mergedModels.push(remote);
18247
+ modelChanges.push({
18248
+ id: remote.id,
18249
+ name: remote.name,
18250
+ status: fields.length > 0 ? "updated" : "unchanged",
18251
+ fields: fields.length > 0 ? fields : void 0
18252
+ });
18253
+ }
18254
+ for (const local of localModels) {
18255
+ if (!seenIds.has(local.id)) {
18256
+ mergedModels.push(local);
18257
+ modelChanges.push({ id: local.id, name: local.name, status: "unchanged" });
18258
+ }
18259
+ }
18260
+ const mergedRecs = { ...localRecs };
18261
+ const recChanges = [];
18262
+ for (const [cat, remoteRec] of Object.entries(remoteRecs)) {
18263
+ const localRec = localRecs[cat];
18264
+ if (!localRec) {
18265
+ mergedRecs[cat] = remoteRec;
18266
+ recChanges.push({
18267
+ category: cat,
18268
+ status: "added",
18269
+ newModel: remoteRec.model,
18270
+ reason: remoteRec.reason
18271
+ });
18272
+ } else if (localRec.model !== remoteRec.model || localRec.reason !== remoteRec.reason) {
18273
+ mergedRecs[cat] = remoteRec;
18274
+ recChanges.push({
18275
+ category: cat,
18276
+ status: "updated",
18277
+ oldModel: localRec.model,
18278
+ newModel: remoteRec.model,
18279
+ reason: remoteRec.reason
18280
+ });
18281
+ } else {
18282
+ recChanges.push({ category: cat, status: "unchanged" });
18283
+ }
18284
+ }
18285
+ return {
18286
+ models: {
18287
+ merged: mergedModels,
18288
+ changes: modelChanges,
18289
+ added: modelChanges.filter((c) => c.status === "added").length,
18290
+ updated: modelChanges.filter((c) => c.status === "updated").length,
18291
+ unchanged: modelChanges.filter((c) => c.status === "unchanged").length
18292
+ },
18293
+ recommendations: {
18294
+ merged: mergedRecs,
18295
+ changes: recChanges,
18296
+ added: recChanges.filter((c) => c.status === "added").length,
18297
+ updated: recChanges.filter((c) => c.status === "updated").length,
18298
+ unchanged: recChanges.filter((c) => c.status === "unchanged").length
18299
+ },
18300
+ suggestions
18301
+ };
18302
+ }
18303
+ function generateModelsYaml(models) {
18304
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
18305
+ let yaml = `# Model pricing registry \u2014 updated via \`fathom research\`
18306
+ `;
18307
+ yaml += `# Last verified: ${date}
18308
+
18309
+ `;
18310
+ yaml += `models:
18311
+ `;
18312
+ for (const m of models) {
18313
+ yaml += ` - id: ${m.id}
18314
+ `;
18315
+ yaml += ` provider: ${m.provider}
18316
+ `;
18317
+ yaml += ` name: ${m.name}
18318
+ `;
18319
+ yaml += ` input_per_mtok: ${m.input_per_mtok.toFixed(2)}
18320
+ `;
18321
+ yaml += ` output_per_mtok: ${m.output_per_mtok.toFixed(2)}
18322
+ `;
18323
+ yaml += ` cache_read_per_mtok: ${m.cache_read_per_mtok.toFixed(2)}
18324
+ `;
18325
+ yaml += ` batch_discount: ${m.batch_discount.toFixed(2)}
18326
+ `;
18327
+ yaml += ` tier: ${m.tier}
18328
+
18329
+ `;
18330
+ }
18331
+ return yaml;
18332
+ }
18333
+ function generateAgentsYaml(recommendations) {
18334
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
18335
+ let yaml = `# Agent/model recommendation mappings
18336
+ `;
18337
+ yaml += `# Maps task categories to recommended models
18338
+ `;
18339
+ yaml += `# Last updated: ${date}
18340
+
18341
+ `;
18342
+ yaml += `recommendations:
18343
+ `;
18344
+ for (const [category, rec] of Object.entries(recommendations)) {
18345
+ yaml += ` ${category}:
18346
+ `;
18347
+ yaml += ` model: ${rec.model}
18348
+ `;
18349
+ yaml += ` reason: ${rec.reason}
18350
+
18351
+ `;
18352
+ }
18353
+ return yaml;
18354
+ }
18355
+
18356
+ // src/commands/research.ts
18357
+ function findDataDir2() {
18358
+ const bundled = resolve12(import.meta.dirname ?? __dirname, "data");
18359
+ if (existsSync12(resolve12(bundled, "models.yaml"))) return bundled;
18360
+ return resolve12(import.meta.dirname ?? __dirname, "../../../../data");
18361
+ }
18362
+ var researchCommand = new Command13("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) => {
18363
+ console.log(chalk14.bold("\nFathom \u2014 Research: Model Pricing"));
18364
+ console.log(chalk14.dim("\u2500".repeat(55)));
18365
+ const startTime = Date.now();
18366
+ let localData;
18367
+ try {
18368
+ localData = loadData();
18369
+ } catch {
18370
+ console.log(chalk14.yellow(" Warning: could not load local data files"));
18371
+ localData = { models: [], recommendations: {} };
18372
+ }
18373
+ const [pricingResult, intelligenceResult] = await Promise.all([
18374
+ fetchPricing({ offline: options.offline, verbose: options.verbose }),
18375
+ fetchIntelligence(localData.models, localData.recommendations, {
18376
+ offline: options.offline,
18377
+ verbose: options.verbose
18378
+ })
18379
+ ]);
18380
+ if (options.verbose) {
18381
+ const elapsed = Date.now() - startTime;
18382
+ console.log(chalk14.dim(`
18383
+ Fetch time: ${elapsed}ms`));
18384
+ console.log(chalk14.dim(` Pricing source: ${pricingResult.source}`));
18385
+ if (intelligenceResult) {
18386
+ console.log(chalk14.dim(` Intelligence source: ${intelligenceResult.source}`));
18387
+ }
18388
+ }
18389
+ if (pricingResult.stale) {
18390
+ console.log(chalk14.yellow(`
18391
+ \u26A0 Using local data (${pricingResult.error ?? "stale"})`));
18392
+ } else {
18393
+ console.log(chalk14.green(`
18394
+ Pricing: live data (manifest v${pricingResult.data.manifestVersion}, ${pricingResult.data.manifestDate})`));
18395
+ }
18396
+ const mergeResult = mergePricingData(
18397
+ localData.models,
18398
+ pricingResult.data.models,
18399
+ localData.recommendations,
18400
+ pricingResult.data.recommendations,
18401
+ intelligenceResult?.data.suggestions ?? []
18402
+ );
18403
+ displayPricingReport(mergeResult);
18404
+ if (intelligenceResult?.data) {
18405
+ displayIntelligence(intelligenceResult.data);
18406
+ }
18407
+ displayRecommendations(mergeResult);
18408
+ if (options.update) {
18409
+ const dataDir = findDataDir2();
18410
+ const modelsPath = resolve12(dataDir, "models.yaml");
18411
+ const agentsPath = resolve12(dataDir, "agents.yaml");
18412
+ writeFileSync5(modelsPath, generateModelsYaml(mergeResult.models.merged));
18413
+ console.log(chalk14.green(`
18414
+ Updated: ${modelsPath}`));
18415
+ writeFileSync5(agentsPath, generateAgentsYaml(mergeResult.recommendations.merged));
18416
+ console.log(chalk14.green(` Updated: ${agentsPath}`));
18417
+ }
18418
+ if (options.sync) {
18419
+ await syncToConvex(mergeResult);
18420
+ }
18421
+ if (options.verbose) {
18422
+ const totalElapsed = Date.now() - startTime;
18423
+ console.log(chalk14.dim(`
18424
+ Total time: ${totalElapsed}ms`));
18425
+ }
18426
+ console.log();
18427
+ });
18428
+ function displayPricingReport(result) {
18429
+ const { models } = result;
18430
+ if (models.added === 0 && models.updated === 0) {
18431
+ console.log(chalk14.green(` All model pricing is up to date.`));
18432
+ console.log(chalk14.dim(` ${models.merged.length} models checked.`));
18433
+ } else {
18434
+ if (models.updated > 0) {
18435
+ console.log(chalk14.yellow(`
18436
+ ${models.updated} pricing change(s) found:`));
18437
+ for (const change of models.changes) {
18438
+ if (change.status === "updated" && change.fields) {
18439
+ for (const f of change.fields) {
18440
+ console.log(chalk14.dim(` ${change.name}: ${f.field} ${f.old} \u2192 ${f.new}`));
18441
+ }
18442
+ }
18443
+ }
18444
+ }
18445
+ if (models.added > 0) {
18446
+ console.log(chalk14.yellow(`
18447
+ ${models.added} new model(s) found:`));
18448
+ for (const change of models.changes) {
18449
+ if (change.status === "added") {
18450
+ const m = models.merged.find((x) => x.id === change.id);
18451
+ if (m) {
18452
+ console.log(
18453
+ chalk14.dim(` ${m.name} (${m.tier}): $${m.input_per_mtok}/$${m.output_per_mtok} per MTok`)
18454
+ );
18455
+ }
18456
+ }
18457
+ }
18458
+ }
18459
+ }
18460
+ }
18461
+ function displayIntelligence(intelligence) {
18462
+ if (intelligence.suggestions.length === 0) {
18463
+ console.log(chalk14.dim(`
18464
+ Intelligence: ${intelligence.summary}`));
18465
+ return;
18466
+ }
18467
+ console.log(chalk14.cyan(`
18468
+ Intelligence suggestions (${intelligence.suggestions.length}):`));
18469
+ for (const s of intelligence.suggestions) {
18470
+ const badge = s.confidence === "high" ? chalk14.red("HIGH") : s.confidence === "medium" ? chalk14.yellow("MED") : chalk14.dim("LOW");
18471
+ const model = s.model ? chalk14.dim(` [${s.model}]`) : "";
18472
+ console.log(` ${badge} ${s.description}${model}`);
18473
+ }
18474
+ console.log(chalk14.dim(` Summary: ${intelligence.summary}`));
18475
+ console.log(chalk14.dim(` Note: Task profiles are team-calibrated and shown as suggestions only.`));
18476
+ }
18477
+ function displayRecommendations(result) {
18478
+ const { recommendations } = result;
18479
+ if (recommendations.updated > 0 || recommendations.added > 0) {
18480
+ console.log(chalk14.yellow(`
18481
+ Recommendation changes:`));
18482
+ for (const change of recommendations.changes) {
18483
+ if (change.status === "updated") {
18484
+ console.log(chalk14.dim(` ${change.category}: ${change.oldModel} \u2192 ${change.newModel}`));
18485
+ } else if (change.status === "added") {
18486
+ console.log(chalk14.dim(` ${change.category}: ${change.newModel} (new)`));
18487
+ }
18488
+ }
18489
+ }
18490
+ console.log(chalk14.dim("\n Current model recommendations:"));
18491
+ console.log(chalk14.dim(" S/M complexity \u2192 Haiku 4.5 ($1/$5 per MTok) \u2014 fast, cheap"));
18492
+ console.log(chalk14.dim(" L complexity \u2192 Sonnet 4.6 ($3/$15 per MTok) \u2014 balanced"));
18493
+ console.log(chalk14.dim(" XL complexity \u2192 Opus 4.6 ($15/$75 per MTok) \u2014 highest quality"));
18494
+ console.log(
18495
+ chalk14.dim("\n Tip: Enable prompt caching to save 90% on input tokens for repeated context.")
18496
+ );
18497
+ }
18498
+ async function syncToConvex(result) {
18499
+ const client = getConvexClient();
18500
+ if (!client) {
18501
+ logConvexStatus(false);
18502
+ return;
18503
+ }
18504
+ let synced = 0;
18505
+ for (const m of result.models.merged) {
18506
+ try {
18507
+ await client.mutation(api.modelPricing.upsert, {
18508
+ model: m.name,
18509
+ provider: m.provider,
18510
+ inputPerMTok: m.input_per_mtok,
18511
+ outputPerMTok: m.output_per_mtok,
18512
+ cacheReadPerMTok: m.cache_read_per_mtok,
18513
+ batchDiscount: m.batch_discount
18514
+ });
18515
+ synced++;
18516
+ } catch {
18517
+ }
18518
+ }
18519
+ logConvexStatus(synced > 0);
18520
+ if (synced > 0) {
18521
+ console.log(chalk14.dim(` ${synced} models synced to Convex.`));
18522
+ }
18523
+ }
18524
+
17944
18525
  // src/index.ts
17945
- var program = new Command13();
18526
+ var program = new Command14();
17946
18527
  program.name("fathom").description("Workflow intelligence platform for AI-augmented development").version(VERSION);
17947
18528
  program.addCommand(analyzeCommand);
17948
18529
  program.addCommand(pricingCommand);
@@ -17956,6 +18537,7 @@ program.addCommand(validateCommand);
17956
18537
  program.addCommand(velocityCommand);
17957
18538
  program.addCommand(intakeCommand);
17958
18539
  program.addCommand(initCommand);
18540
+ program.addCommand(researchCommand);
17959
18541
  program.parse();
17960
18542
  /*! Bundled license information:
17961
18543