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