fathom-cli 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/index.js +924 -186
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/plugins/fathom/.claude-plugin/plugin.json +13 -0
- package/plugins/fathom/.mcp.json +8 -0
- package/plugins/fathom/skills/budget/.gitkeep +0 -0
- package/plugins/fathom/skills/budget/SKILL.md +40 -0
- package/plugins/fathom/skills/budget-warning/.gitkeep +0 -0
- package/plugins/fathom/skills/budget-warning/SKILL.md +52 -0
- package/plugins/fathom/skills/cost-preview/.gitkeep +0 -0
- package/plugins/fathom/skills/cost-preview/SKILL.md +70 -0
- package/plugins/fathom/skills/estimate/.gitkeep +0 -0
- package/plugins/fathom/skills/estimate/SKILL.md +58 -0
- package/plugins/fathom/skills/gsd/SKILL.md +35 -0
- package/plugins/fathom/skills/projections/.gitkeep +0 -0
- package/plugins/fathom/skills/projections/SKILL.md +36 -0
- package/plugins/fathom/skills/recommend-model/.gitkeep +0 -0
- package/plugins/fathom/skills/recommend-model/SKILL.md +53 -0
- package/plugins/fathom/skills/status/.gitkeep +0 -0
- package/plugins/fathom/skills/status/SKILL.md +42 -0
- package/plugins/fathom/tests/integrations.test.ts +66 -0
- package/plugins/fathom/tests/plugin.test.ts +49 -0
- package/plugins/fathom/tests/skills.test.ts +135 -0
package/dist/index.js
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
__commonJS,
|
|
8
8
|
__export,
|
|
9
|
-
__require,
|
|
10
9
|
__toESM
|
|
11
10
|
} from "./chunk-SEGVTWSK.js";
|
|
12
11
|
|
|
@@ -103,7 +102,7 @@ var require_extend = __commonJS({
|
|
|
103
102
|
|
|
104
103
|
// src/index.ts
|
|
105
104
|
import "dotenv/config";
|
|
106
|
-
import { Command as
|
|
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(
|
|
484
|
-
assertNonEmpty(
|
|
485
|
-
assertPart(
|
|
486
|
-
this.path = default2.join(this.dirname || "",
|
|
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(
|
|
506
|
+
set dirname(dirname10) {
|
|
508
507
|
assertPath(this.basename, "dirname");
|
|
509
|
-
this.path = default2.join(
|
|
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(
|
|
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 (
|
|
1140
|
-
|
|
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(
|
|
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 (
|
|
1257
|
-
|
|
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
|
|
4086
|
-
if (
|
|
4087
|
-
events =
|
|
4088
|
-
called.push(
|
|
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
|
|
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
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
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 =
|
|
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
|
|
9224
|
-
import { resolve as resolve7, basename as
|
|
9239
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, cpSync, mkdirSync as mkdirSync6 } from "fs";
|
|
9240
|
+
import { resolve as resolve7, basename as basename3 } from "path";
|
|
9225
9241
|
import { spawn } from "child_process";
|
|
9226
9242
|
import chalk4 from "chalk";
|
|
9227
9243
|
import { select as select2, confirm as confirm2, input as input2, checkbox } from "@inquirer/prompts";
|
|
@@ -9302,11 +9318,21 @@ function updateWorkflowState(updates) {
|
|
|
9302
9318
|
|
|
9303
9319
|
// src/commands/setup.ts
|
|
9304
9320
|
import { Command } from "commander";
|
|
9305
|
-
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
|
|
9306
|
-
import { resolve as resolve4 } from "path";
|
|
9321
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
9322
|
+
import { basename, resolve as resolve4 } from "path";
|
|
9307
9323
|
import chalk from "chalk";
|
|
9324
|
+
function detectProjectName(dir) {
|
|
9325
|
+
try {
|
|
9326
|
+
const pkg = JSON.parse(readFileSync4(resolve4(dir, "package.json"), "utf-8"));
|
|
9327
|
+
if (pkg.name && typeof pkg.name === "string") {
|
|
9328
|
+
return pkg.name.replace(/^@[^/]+\//, "");
|
|
9329
|
+
}
|
|
9330
|
+
} catch {
|
|
9331
|
+
}
|
|
9332
|
+
return basename(dir);
|
|
9333
|
+
}
|
|
9308
9334
|
function scaffoldProject(projectName, opts = {}) {
|
|
9309
|
-
const baseDir = process.cwd();
|
|
9335
|
+
const baseDir = opts.projectDir ?? process.cwd();
|
|
9310
9336
|
const log = opts.quiet ? () => {
|
|
9311
9337
|
} : (msg) => console.log(msg);
|
|
9312
9338
|
const fathomDir = resolve4(baseDir, ".fathom");
|
|
@@ -9390,9 +9416,25 @@ function scaffoldProject(projectName, opts = {}) {
|
|
|
9390
9416
|
} else {
|
|
9391
9417
|
log(chalk.dim(" \xB7 .claude/hooks/te-session-sync.sh (exists)"));
|
|
9392
9418
|
}
|
|
9419
|
+
const mcpPath = resolve4(baseDir, ".mcp.json");
|
|
9420
|
+
let mcpConfig = { mcpServers: {} };
|
|
9421
|
+
if (existsSync3(mcpPath)) {
|
|
9422
|
+
try {
|
|
9423
|
+
mcpConfig = JSON.parse(readFileSync4(mcpPath, "utf-8"));
|
|
9424
|
+
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
9425
|
+
} catch {
|
|
9426
|
+
mcpConfig = { mcpServers: {} };
|
|
9427
|
+
}
|
|
9428
|
+
}
|
|
9429
|
+
mcpConfig.mcpServers.fathom = {
|
|
9430
|
+
command: "npx",
|
|
9431
|
+
args: ["-y", "fathom-mcp@latest", "--project-dir", "."]
|
|
9432
|
+
};
|
|
9433
|
+
writeFileSync3(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n");
|
|
9434
|
+
log(chalk.green(" \u2713 .mcp.json (MCP auto-discovery)"));
|
|
9393
9435
|
}
|
|
9394
|
-
var setupCommand = new Command("setup").description("Scaffold .fathom/ project directory + .claude/ skill + hooks").option("-p, --project <name>", "Project name"
|
|
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
|
|
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
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
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
|
-
|
|
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 ${
|
|
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:
|
|
10101
|
+
return { rawInput: readFileSync5(resolved, "utf-8"), source: "file" };
|
|
10052
10102
|
} else {
|
|
10053
|
-
return { rawInput:
|
|
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:
|
|
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:
|
|
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 =
|
|
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 ${
|
|
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 =
|
|
10217
|
+
rawInput = readFileSync5(resolved, "utf-8");
|
|
10168
10218
|
source = "file";
|
|
10169
10219
|
} else {
|
|
10170
|
-
rawInput =
|
|
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 =
|
|
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
|
-
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
|
|
10205
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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((
|
|
10988
|
-
const promptContent =
|
|
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
|
-
|
|
11133
|
+
resolve25();
|
|
11005
11134
|
});
|
|
11006
11135
|
child.on("close", () => {
|
|
11007
11136
|
process.removeListener("SIGINT", sigintHandler);
|
|
11008
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
11310
|
-
import { join, basename as
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
11928
|
-
const summary = JSON.parse(
|
|
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
|
|
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(
|
|
12125
|
-
const summary = JSON.parse(
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
12721
|
-
const summary = JSON.parse(
|
|
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 {
|
|
12859
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
13029
|
+
console.log(chalk14.dim(` Convex URL: ${options.convexUrl}`));
|
|
12890
13030
|
}
|
|
12891
13031
|
if (options.team) {
|
|
12892
|
-
console.log(chalk14.
|
|
13032
|
+
console.log(chalk14.dim(` Team: ${options.team}`));
|
|
12893
13033
|
}
|
|
12894
|
-
|
|
12895
|
-
|
|
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
|
-
|
|
12898
|
-
|
|
12899
|
-
|
|
12900
|
-
|
|
12901
|
-
"
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
12966
|
-
|
|
12967
|
-
|
|
12968
|
-
|
|
12969
|
-
|
|
12970
|
-
|
|
12971
|
-
|
|
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
|
|
12977
|
-
import { resolve as
|
|
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 =
|
|
12983
|
-
if (!
|
|
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 =
|
|
13043
|
-
const hasProjections =
|
|
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 =
|
|
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
|
|
13066
|
-
import { resolve as
|
|
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
|
|
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(
|
|
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 ?
|
|
14697
|
-
const dir =
|
|
14698
|
-
if (!
|
|
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
|
-
|
|
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
|
|
14718
|
-
import { resolve as
|
|
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
|
|
15371
|
+
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
14721
15372
|
function loadCalibratedProfiles() {
|
|
14722
15373
|
const profiles = [];
|
|
14723
|
-
const registryPath =
|
|
14724
|
-
const summaryPath =
|
|
14725
|
-
if (
|
|
14726
|
-
const registry = JSON.parse(
|
|
14727
|
-
const summary = JSON.parse(
|
|
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
|
|
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 =
|
|
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 =
|
|
15471
|
+
const outputPath = resolve20(contributionsDir, `${today}.json`);
|
|
14821
15472
|
const json = exportContribution(dataPoints);
|
|
14822
|
-
|
|
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
|
|
14832
|
-
import { resolve as
|
|
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 =
|
|
14837
|
-
if (!
|
|
14838
|
-
const content3 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
14960
|
-
import { resolve as
|
|
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
|
|
16291
|
+
return resolve22(base, LEDGER_DIR, LEDGER_FILE);
|
|
15641
16292
|
}
|
|
15642
16293
|
function loadLedger(ledgerPath) {
|
|
15643
|
-
if (
|
|
16294
|
+
if (existsSync23(ledgerPath)) {
|
|
15644
16295
|
try {
|
|
15645
|
-
return JSON.parse(
|
|
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(
|
|
15653
|
-
|
|
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
|
|
15713
|
-
import { resolve as
|
|
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 =
|
|
16366
|
+
const configPath = resolve23(process.cwd(), ".fathom", "sync-config.json");
|
|
15716
16367
|
let config = {};
|
|
15717
|
-
if (
|
|
16368
|
+
if (existsSync24(configPath)) {
|
|
15718
16369
|
try {
|
|
15719
|
-
config = JSON.parse(
|
|
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 =
|
|
15733
|
-
if (!
|
|
16383
|
+
const registryPath = resolve23(process.cwd(), ".claude", "te", "registry.json");
|
|
16384
|
+
if (!existsSync24(registryPath)) return [];
|
|
15734
16385
|
try {
|
|
15735
|
-
const data = JSON.parse(
|
|
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-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
|
|
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);
|