fathom-cli 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-TXIC6BY4.js → chunk-E6YGKUJB.js} +12 -5
- package/dist/chunk-E6YGKUJB.js.map +1 -0
- package/dist/{dist-KXBSLOHP.js → dist-HPXMBCZX.js} +17 -6
- package/dist/{dist-KXBSLOHP.js.map → dist-HPXMBCZX.js.map} +1 -1
- package/dist/{dist-XKZLNUDU.js → dist-VMLJKWUO.js} +2 -2
- package/dist/index.js +1177 -519
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/plugins/fathom/tests/integrations.test.ts +3 -1
- package/dist/chunk-TXIC6BY4.js.map +0 -1
- /package/dist/{dist-XKZLNUDU.js.map → dist-VMLJKWUO.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -102,7 +102,7 @@ var require_extend = __commonJS({
|
|
|
102
102
|
|
|
103
103
|
// src/index.ts
|
|
104
104
|
import "dotenv/config";
|
|
105
|
-
import { Command as
|
|
105
|
+
import { Command as Command24 } from "commander";
|
|
106
106
|
|
|
107
107
|
// ../../node_modules/.pnpm/bail@2.0.2/node_modules/bail/index.js
|
|
108
108
|
function bail(error) {
|
|
@@ -1104,7 +1104,7 @@ var Processor = class _Processor extends CallableInstance {
|
|
|
1104
1104
|
assertParser("process", this.parser || this.Parser);
|
|
1105
1105
|
assertCompiler("process", this.compiler || this.Compiler);
|
|
1106
1106
|
return done ? executor(void 0, done) : new Promise(executor);
|
|
1107
|
-
function executor(
|
|
1107
|
+
function executor(resolve26, reject) {
|
|
1108
1108
|
const realFile = vfile(file);
|
|
1109
1109
|
const parseTree = (
|
|
1110
1110
|
/** @type {HeadTree extends undefined ? Node : HeadTree} */
|
|
@@ -1135,8 +1135,8 @@ var Processor = class _Processor extends CallableInstance {
|
|
|
1135
1135
|
function realDone(error, file2) {
|
|
1136
1136
|
if (error || !file2) {
|
|
1137
1137
|
reject(error);
|
|
1138
|
-
} else if (
|
|
1139
|
-
|
|
1138
|
+
} else if (resolve26) {
|
|
1139
|
+
resolve26(file2);
|
|
1140
1140
|
} else {
|
|
1141
1141
|
ok(done, "`done` is defined if `resolve` is not");
|
|
1142
1142
|
done(void 0, file2);
|
|
@@ -1238,7 +1238,7 @@ var Processor = class _Processor extends CallableInstance {
|
|
|
1238
1238
|
file = void 0;
|
|
1239
1239
|
}
|
|
1240
1240
|
return done ? executor(void 0, done) : new Promise(executor);
|
|
1241
|
-
function executor(
|
|
1241
|
+
function executor(resolve26, reject) {
|
|
1242
1242
|
ok(
|
|
1243
1243
|
typeof file !== "function",
|
|
1244
1244
|
"`file` can\u2019t be a `done` anymore, we checked"
|
|
@@ -1252,8 +1252,8 @@ var Processor = class _Processor extends CallableInstance {
|
|
|
1252
1252
|
);
|
|
1253
1253
|
if (error) {
|
|
1254
1254
|
reject(error);
|
|
1255
|
-
} else if (
|
|
1256
|
-
|
|
1255
|
+
} else if (resolve26) {
|
|
1256
|
+
resolve26(resultingTree);
|
|
1257
1257
|
} else {
|
|
1258
1258
|
ok(done, "`done` is defined if `resolve` is not");
|
|
1259
1259
|
done(void 0, resultingTree, file2);
|
|
@@ -4081,10 +4081,10 @@ function resolveAll(constructs2, events, context) {
|
|
|
4081
4081
|
const called = [];
|
|
4082
4082
|
let index2 = -1;
|
|
4083
4083
|
while (++index2 < constructs2.length) {
|
|
4084
|
-
const
|
|
4085
|
-
if (
|
|
4086
|
-
events =
|
|
4087
|
-
called.push(
|
|
4084
|
+
const resolve26 = constructs2[index2].resolveAll;
|
|
4085
|
+
if (resolve26 && !called.includes(resolve26)) {
|
|
4086
|
+
events = resolve26(events, context);
|
|
4087
|
+
called.push(resolve26);
|
|
4088
4088
|
}
|
|
4089
4089
|
}
|
|
4090
4090
|
return events;
|
|
@@ -8533,7 +8533,12 @@ var FeatureEstimate = external_exports.object({
|
|
|
8533
8533
|
errorRecovery: external_exports.number(),
|
|
8534
8534
|
planning: external_exports.number(),
|
|
8535
8535
|
review: external_exports.number()
|
|
8536
|
-
})
|
|
8536
|
+
}),
|
|
8537
|
+
confidence: external_exports.object({
|
|
8538
|
+
low: external_exports.object({ tokens: external_exports.number(), cost: external_exports.number() }),
|
|
8539
|
+
expected: external_exports.object({ tokens: external_exports.number(), cost: external_exports.number() }),
|
|
8540
|
+
high: external_exports.object({ tokens: external_exports.number(), cost: external_exports.number() })
|
|
8541
|
+
}).optional()
|
|
8537
8542
|
});
|
|
8538
8543
|
var SensitivityScenario = external_exports.object({
|
|
8539
8544
|
name: external_exports.string(),
|
|
@@ -8751,7 +8756,7 @@ var AgentsFile = external_exports.object({
|
|
|
8751
8756
|
});
|
|
8752
8757
|
function loadYaml(filePath, schema) {
|
|
8753
8758
|
const content3 = readFileSync(filePath, "utf-8");
|
|
8754
|
-
const raw = jsYaml.load(content3);
|
|
8759
|
+
const raw = jsYaml.load(content3, { schema: jsYaml.JSON_SCHEMA });
|
|
8755
8760
|
return schema.parse(raw);
|
|
8756
8761
|
}
|
|
8757
8762
|
function findDataDir() {
|
|
@@ -8764,18 +8769,32 @@ function findDataDir() {
|
|
|
8764
8769
|
return resolve(base, "../../../data");
|
|
8765
8770
|
}
|
|
8766
8771
|
}
|
|
8767
|
-
|
|
8772
|
+
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
8773
|
+
var cacheMap = /* @__PURE__ */ new Map();
|
|
8774
|
+
function loadData(dataDir, ttlMs) {
|
|
8768
8775
|
const dir = dataDir ?? findDataDir();
|
|
8776
|
+
const resolvedDir = resolve(dir);
|
|
8777
|
+
const ttl = ttlMs ?? DEFAULT_TTL_MS;
|
|
8778
|
+
if (ttl > 0) {
|
|
8779
|
+
const cached = cacheMap.get(resolvedDir);
|
|
8780
|
+
if (cached && Date.now() - cached.timestamp < ttl) {
|
|
8781
|
+
return cached.data;
|
|
8782
|
+
}
|
|
8783
|
+
}
|
|
8769
8784
|
const modelsFile = loadYaml(resolve(dir, "models.yaml"), ModelsFile);
|
|
8770
8785
|
const profilesFile = loadYaml(resolve(dir, "task-profiles.yaml"), TaskProfilesFile);
|
|
8771
8786
|
const agentsFile = loadYaml(resolve(dir, "agents.yaml"), AgentsFile);
|
|
8772
|
-
|
|
8787
|
+
const data = {
|
|
8773
8788
|
models: modelsFile.models,
|
|
8774
8789
|
profiles: profilesFile.profiles,
|
|
8775
8790
|
complexityMultipliers: profilesFile.complexity_multipliers,
|
|
8776
8791
|
overhead: profilesFile.overhead_multipliers,
|
|
8777
8792
|
recommendations: agentsFile.recommendations
|
|
8778
8793
|
};
|
|
8794
|
+
if (ttl > 0) {
|
|
8795
|
+
cacheMap.set(resolvedDir, { data, timestamp: Date.now() });
|
|
8796
|
+
}
|
|
8797
|
+
return data;
|
|
8779
8798
|
}
|
|
8780
8799
|
function getProfile(profiles, taskType) {
|
|
8781
8800
|
return profiles[taskType];
|
|
@@ -8861,6 +8880,20 @@ function estimateFeature(feature, data) {
|
|
|
8861
8880
|
const overheadOutput = Math.round(baseOutput * multiplier + breakdown.toolCalls * 0.75);
|
|
8862
8881
|
const overheadTotal = overheadInput + overheadOutput;
|
|
8863
8882
|
const cost = calculateCost(overheadInput, overheadOutput, pricing);
|
|
8883
|
+
const confidence = {
|
|
8884
|
+
low: {
|
|
8885
|
+
tokens: Math.round(overheadTotal * 0.7),
|
|
8886
|
+
cost: Math.round(cost * 0.7 * 100) / 100
|
|
8887
|
+
},
|
|
8888
|
+
expected: {
|
|
8889
|
+
tokens: overheadTotal,
|
|
8890
|
+
cost
|
|
8891
|
+
},
|
|
8892
|
+
high: {
|
|
8893
|
+
tokens: Math.round(overheadTotal * 1.5),
|
|
8894
|
+
cost: Math.round(cost * 1.5 * 100) / 100
|
|
8895
|
+
}
|
|
8896
|
+
};
|
|
8864
8897
|
return {
|
|
8865
8898
|
featureId: feature.id,
|
|
8866
8899
|
featureName: feature.name,
|
|
@@ -8871,7 +8904,8 @@ function estimateFeature(feature, data) {
|
|
|
8871
8904
|
baseTokens: { input: baseInput, output: baseOutput, total: baseTotal },
|
|
8872
8905
|
withOverhead: { input: overheadInput, output: overheadOutput, total: overheadTotal },
|
|
8873
8906
|
estimatedCost: cost,
|
|
8874
|
-
overheadBreakdown: breakdown
|
|
8907
|
+
overheadBreakdown: breakdown,
|
|
8908
|
+
confidence
|
|
8875
8909
|
};
|
|
8876
8910
|
}
|
|
8877
8911
|
function estimateAll(features, data) {
|
|
@@ -10005,7 +10039,40 @@ async function syncVelocity(data) {
|
|
|
10005
10039
|
}
|
|
10006
10040
|
}
|
|
10007
10041
|
|
|
10042
|
+
// src/ux/spinner.ts
|
|
10043
|
+
var FRAMES = ["\u28CB", "\u28D9", "\u28F9", "\u28F8", "\u28FC", "\u28F4", "\u28E6", "\u28E7", "\u28C7", "\u28CF"];
|
|
10044
|
+
var INTERVAL_MS = 80;
|
|
10045
|
+
async function withSpinner(text3, fn) {
|
|
10046
|
+
const start = Date.now();
|
|
10047
|
+
const isTTY = process.stderr.isTTY;
|
|
10048
|
+
if (!isTTY) {
|
|
10049
|
+
return fn();
|
|
10050
|
+
}
|
|
10051
|
+
let frameIndex = 0;
|
|
10052
|
+
const timer = setInterval(() => {
|
|
10053
|
+
const frame = FRAMES[frameIndex % FRAMES.length];
|
|
10054
|
+
process.stderr.write(`\r\x1B[K${frame} ${text3}`);
|
|
10055
|
+
frameIndex++;
|
|
10056
|
+
}, INTERVAL_MS);
|
|
10057
|
+
try {
|
|
10058
|
+
const result = await fn();
|
|
10059
|
+
clearInterval(timer);
|
|
10060
|
+
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
10061
|
+
process.stderr.write(`\r\x1B[K\x1B[32m\u2713\x1B[0m ${text3} (${elapsed}s)
|
|
10062
|
+
`);
|
|
10063
|
+
return result;
|
|
10064
|
+
} catch (error) {
|
|
10065
|
+
clearInterval(timer);
|
|
10066
|
+
process.stderr.write(`\r\x1B[K\x1B[31m\u2717\x1B[0m ${text3}
|
|
10067
|
+
`);
|
|
10068
|
+
throw error;
|
|
10069
|
+
}
|
|
10070
|
+
}
|
|
10071
|
+
|
|
10008
10072
|
// src/commands/intake.ts
|
|
10073
|
+
function isWithinBoundary(resolvedPath, boundary) {
|
|
10074
|
+
return resolvedPath.startsWith(boundary);
|
|
10075
|
+
}
|
|
10009
10076
|
function findLocalMarkdownFiles() {
|
|
10010
10077
|
try {
|
|
10011
10078
|
const cwd = process.cwd();
|
|
@@ -10027,16 +10094,24 @@ async function runIntakeFlow(projectName) {
|
|
|
10027
10094
|
process.exit(1);
|
|
10028
10095
|
}
|
|
10029
10096
|
const provider = hasAnthropicKey ? "anthropic" : "openai";
|
|
10030
|
-
console.log(chalk3.dim(`
|
|
10031
|
-
Extracting work items via ${provider === "anthropic" ? "Claude" : "OpenAI"}...`));
|
|
10032
10097
|
let items;
|
|
10033
10098
|
try {
|
|
10034
|
-
items = await
|
|
10035
|
-
provider
|
|
10036
|
-
|
|
10037
|
-
|
|
10099
|
+
items = await withSpinner(
|
|
10100
|
+
`Extracting work items via ${provider === "anthropic" ? "Claude" : "OpenAI"}...`,
|
|
10101
|
+
async () => extractWorkItems(rawInput, {
|
|
10102
|
+
provider,
|
|
10103
|
+
...provider === "openai" ? { openaiApiKey: process.env.OPENAI_API_KEY } : {}
|
|
10104
|
+
})
|
|
10105
|
+
);
|
|
10038
10106
|
} catch (err) {
|
|
10039
|
-
|
|
10107
|
+
let errMsg = err.message;
|
|
10108
|
+
for (const envKey of ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"]) {
|
|
10109
|
+
const val = process.env[envKey];
|
|
10110
|
+
if (val && errMsg.includes(val)) {
|
|
10111
|
+
errMsg = errMsg.replaceAll(val, val.length <= 6 ? "***" : val.slice(0, 3) + "***" + val.slice(-3));
|
|
10112
|
+
}
|
|
10113
|
+
}
|
|
10114
|
+
console.error(chalk3.red(" Extraction failed:"), errMsg);
|
|
10040
10115
|
process.exit(1);
|
|
10041
10116
|
}
|
|
10042
10117
|
if (items.length === 0) {
|
|
@@ -10094,6 +10169,10 @@ async function resolveInput(cwd) {
|
|
|
10094
10169
|
} else if (chosen === "__other__") {
|
|
10095
10170
|
const filePath = await input({ message: "Path to input file:", default: cwd + "/" });
|
|
10096
10171
|
const resolved = resolve5(filePath.trim());
|
|
10172
|
+
if (!isWithinBoundary(resolved, cwd)) {
|
|
10173
|
+
console.error(chalk3.red("Path traversal rejected: file must be within project directory"));
|
|
10174
|
+
process.exit(1);
|
|
10175
|
+
}
|
|
10097
10176
|
if (!existsSync4(resolved)) {
|
|
10098
10177
|
console.error(chalk3.red(`File not found: ${resolved}`));
|
|
10099
10178
|
process.exit(1);
|
|
@@ -10113,6 +10192,10 @@ async function resolveInput(cwd) {
|
|
|
10113
10192
|
if (inputType === "file") {
|
|
10114
10193
|
const filePath = await input({ message: "Path to input file:", default: cwd + "/" });
|
|
10115
10194
|
const resolved = resolve5(filePath.trim());
|
|
10195
|
+
if (!isWithinBoundary(resolved, cwd)) {
|
|
10196
|
+
console.error(chalk3.red("Path traversal rejected: file must be within project directory"));
|
|
10197
|
+
process.exit(1);
|
|
10198
|
+
}
|
|
10116
10199
|
if (!existsSync4(resolved)) {
|
|
10117
10200
|
console.error(chalk3.red(`File not found: ${resolved}`));
|
|
10118
10201
|
process.exit(1);
|
|
@@ -10175,6 +10258,10 @@ var intakeCommand = new Command2("intake").description("Extract work items from
|
|
|
10175
10258
|
let source;
|
|
10176
10259
|
if (options.file) {
|
|
10177
10260
|
const filePath = resolve5(options.file);
|
|
10261
|
+
if (!isWithinBoundary(filePath, cwd)) {
|
|
10262
|
+
console.error(chalk3.red("Path traversal rejected: file must be within project directory"));
|
|
10263
|
+
process.exit(1);
|
|
10264
|
+
}
|
|
10178
10265
|
if (!existsSync4(filePath)) {
|
|
10179
10266
|
console.error(chalk3.red(`File not found: ${filePath}`));
|
|
10180
10267
|
process.exit(1);
|
|
@@ -10210,6 +10297,10 @@ var intakeCommand = new Command2("intake").description("Extract work items from
|
|
|
10210
10297
|
default: cwd + "/"
|
|
10211
10298
|
});
|
|
10212
10299
|
const resolved = resolve5(filePath.trim());
|
|
10300
|
+
if (!isWithinBoundary(resolved, cwd)) {
|
|
10301
|
+
console.error(chalk3.red("Path traversal rejected: file must be within project directory"));
|
|
10302
|
+
process.exit(1);
|
|
10303
|
+
}
|
|
10213
10304
|
if (!existsSync4(resolved)) {
|
|
10214
10305
|
console.error(chalk3.red(`File not found: ${resolved}`));
|
|
10215
10306
|
process.exit(1);
|
|
@@ -10234,6 +10325,10 @@ var intakeCommand = new Command2("intake").description("Extract work items from
|
|
|
10234
10325
|
default: cwd + "/"
|
|
10235
10326
|
});
|
|
10236
10327
|
const resolved = resolve5(filePath.trim());
|
|
10328
|
+
if (!isWithinBoundary(resolved, cwd)) {
|
|
10329
|
+
console.error(chalk3.red("Path traversal rejected: file must be within project directory"));
|
|
10330
|
+
process.exit(1);
|
|
10331
|
+
}
|
|
10237
10332
|
if (!existsSync4(resolved)) {
|
|
10238
10333
|
console.error(chalk3.red(`File not found: ${resolved}`));
|
|
10239
10334
|
process.exit(1);
|
|
@@ -10263,16 +10358,24 @@ var intakeCommand = new Command2("intake").description("Extract work items from
|
|
|
10263
10358
|
}
|
|
10264
10359
|
provider = hasAnthropicKey ? "anthropic" : "openai";
|
|
10265
10360
|
}
|
|
10266
|
-
console.log(chalk3.dim(`
|
|
10267
|
-
Extracting work items via ${provider === "anthropic" ? "Claude" : "OpenAI"}...`));
|
|
10268
10361
|
let items;
|
|
10269
10362
|
try {
|
|
10270
|
-
items = await
|
|
10271
|
-
provider
|
|
10272
|
-
|
|
10273
|
-
|
|
10363
|
+
items = await withSpinner(
|
|
10364
|
+
`Extracting work items via ${provider === "anthropic" ? "Claude" : "OpenAI"}...`,
|
|
10365
|
+
async () => extractWorkItems(rawInput, {
|
|
10366
|
+
provider,
|
|
10367
|
+
...provider === "openai" ? { openaiApiKey: process.env.OPENAI_API_KEY } : {}
|
|
10368
|
+
})
|
|
10369
|
+
);
|
|
10274
10370
|
} catch (err) {
|
|
10275
|
-
|
|
10371
|
+
let errMsg = err.message;
|
|
10372
|
+
for (const envKey of ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"]) {
|
|
10373
|
+
const val = process.env[envKey];
|
|
10374
|
+
if (val && errMsg.includes(val)) {
|
|
10375
|
+
errMsg = errMsg.replaceAll(val, val.length <= 6 ? "***" : val.slice(0, 3) + "***" + val.slice(-3));
|
|
10376
|
+
}
|
|
10377
|
+
}
|
|
10378
|
+
console.error(chalk3.red("Extraction failed:"), errMsg);
|
|
10276
10379
|
process.exit(1);
|
|
10277
10380
|
}
|
|
10278
10381
|
if (items.length === 0) {
|
|
@@ -10754,9 +10857,9 @@ async function runSettingsFlow(projectName) {
|
|
|
10754
10857
|
switch (setting) {
|
|
10755
10858
|
case "intent": {
|
|
10756
10859
|
const intentData = await captureIntentFlow(projectName);
|
|
10757
|
-
const { saveIntent } = await import("./dist-
|
|
10860
|
+
const { saveIntent } = await import("./dist-VMLJKWUO.js");
|
|
10758
10861
|
await saveIntent(process.cwd(), intentData);
|
|
10759
|
-
const { writeProjections } = await import("./dist-
|
|
10862
|
+
const { writeProjections } = await import("./dist-HPXMBCZX.js");
|
|
10760
10863
|
const files = await writeProjections(process.cwd(), intentData);
|
|
10761
10864
|
console.log(chalk4.green(`
|
|
10762
10865
|
\u2713 Intent saved to .fathom/intent.yaml`));
|
|
@@ -10765,7 +10868,7 @@ async function runSettingsFlow(projectName) {
|
|
|
10765
10868
|
break;
|
|
10766
10869
|
}
|
|
10767
10870
|
case "budget": {
|
|
10768
|
-
const { loadIntent, saveIntent } = await import("./dist-
|
|
10871
|
+
const { loadIntent, saveIntent } = await import("./dist-VMLJKWUO.js");
|
|
10769
10872
|
let existing;
|
|
10770
10873
|
try {
|
|
10771
10874
|
existing = await loadIntent(process.cwd());
|
|
@@ -10823,11 +10926,11 @@ var goCommand = new Command3("go").description("Run the full workflow: intake \u
|
|
|
10823
10926
|
});
|
|
10824
10927
|
if (captureNow) {
|
|
10825
10928
|
const intentData = await captureIntentFlow(projectName);
|
|
10826
|
-
const { saveIntent } = await import("./dist-
|
|
10929
|
+
const { saveIntent } = await import("./dist-VMLJKWUO.js");
|
|
10827
10930
|
await saveIntent(process.cwd(), intentData);
|
|
10828
10931
|
projectName = intentData.project;
|
|
10829
10932
|
updateProjectConfig({ project: projectName });
|
|
10830
|
-
const { writeProjections } = await import("./dist-
|
|
10933
|
+
const { writeProjections } = await import("./dist-HPXMBCZX.js");
|
|
10831
10934
|
const files = await writeProjections(process.cwd(), intentData);
|
|
10832
10935
|
console.log(chalk4.green(`
|
|
10833
10936
|
\u2713 Intent saved to .fathom/intent.yaml`));
|
|
@@ -10841,11 +10944,11 @@ var goCommand = new Command3("go").description("Run the full workflow: intake \u
|
|
|
10841
10944
|
console.log();
|
|
10842
10945
|
const intentData = await captureIntentFlow(basename3(process.cwd()));
|
|
10843
10946
|
projectName = intentData.project;
|
|
10844
|
-
const { saveIntent } = await import("./dist-
|
|
10947
|
+
const { saveIntent } = await import("./dist-VMLJKWUO.js");
|
|
10845
10948
|
await saveIntent(process.cwd(), intentData);
|
|
10846
10949
|
scaffoldProject(projectName, { quiet: true });
|
|
10847
10950
|
updateProjectConfig({ project: projectName });
|
|
10848
|
-
const { writeProjections } = await import("./dist-
|
|
10951
|
+
const { writeProjections } = await import("./dist-HPXMBCZX.js");
|
|
10849
10952
|
const files = await writeProjections(process.cwd(), intentData);
|
|
10850
10953
|
console.log(chalk4.green(`
|
|
10851
10954
|
\u2713 Project "${projectName}" initialized`));
|
|
@@ -11075,16 +11178,23 @@ async function executeBuildMode(mode, promptPath, state) {
|
|
|
11075
11178
|
console.log(chalk4.yellow(" No features in queue for worktree."));
|
|
11076
11179
|
return false;
|
|
11077
11180
|
}
|
|
11181
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(featureId)) {
|
|
11182
|
+
console.log(chalk4.red(` Invalid feature ID: "${featureId}" \u2014 only letters, digits, hyphens, underscores allowed`));
|
|
11183
|
+
return false;
|
|
11184
|
+
}
|
|
11078
11185
|
const worktreePath = resolve7(process.cwd(), ".worktrees", featureId);
|
|
11079
11186
|
const branchName = `feat/${featureId}`;
|
|
11080
11187
|
console.log(chalk4.dim(`
|
|
11081
11188
|
Creating worktree: ${branchName}`));
|
|
11082
11189
|
try {
|
|
11083
|
-
const {
|
|
11084
|
-
|
|
11190
|
+
const { spawnSync } = await import("child_process");
|
|
11191
|
+
const result = spawnSync("git", ["worktree", "add", worktreePath, "-b", branchName], {
|
|
11085
11192
|
stdio: "pipe",
|
|
11086
11193
|
cwd: process.cwd()
|
|
11087
11194
|
});
|
|
11195
|
+
if (result.status !== 0) {
|
|
11196
|
+
throw new Error(result.stderr?.toString() || "git worktree failed");
|
|
11197
|
+
}
|
|
11088
11198
|
console.log(chalk4.green(` \u2713 Worktree created at ${worktreePath}`));
|
|
11089
11199
|
} catch {
|
|
11090
11200
|
console.log(chalk4.yellow(` \u26A0 Worktree creation failed \u2014 launching in current directory`));
|
|
@@ -11113,7 +11223,7 @@ async function executeBuildMode(mode, promptPath, state) {
|
|
|
11113
11223
|
return false;
|
|
11114
11224
|
}
|
|
11115
11225
|
function launchClaude(promptPath, cwd, firstFeature) {
|
|
11116
|
-
return new Promise((
|
|
11226
|
+
return new Promise((resolve26) => {
|
|
11117
11227
|
const promptContent = readFileSync7(promptPath, "utf-8");
|
|
11118
11228
|
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.";
|
|
11119
11229
|
const child = spawn("claude", ["--append-system-prompt", promptContent, initialPrompt], {
|
|
@@ -11130,11 +11240,11 @@ function launchClaude(promptPath, cwd, firstFeature) {
|
|
|
11130
11240
|
} else {
|
|
11131
11241
|
console.error(chalk4.red(` Failed to launch Claude Code: ${err.message}`));
|
|
11132
11242
|
}
|
|
11133
|
-
|
|
11243
|
+
resolve26();
|
|
11134
11244
|
});
|
|
11135
11245
|
child.on("close", () => {
|
|
11136
11246
|
process.removeListener("SIGINT", sigintHandler);
|
|
11137
|
-
|
|
11247
|
+
resolve26();
|
|
11138
11248
|
});
|
|
11139
11249
|
});
|
|
11140
11250
|
}
|
|
@@ -11312,21 +11422,24 @@ var pricingCommand = new Command5("pricing").description("Show current model pri
|
|
|
11312
11422
|
if (options.sync) {
|
|
11313
11423
|
const client = getConvexClient();
|
|
11314
11424
|
if (client) {
|
|
11315
|
-
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
|
|
11319
|
-
|
|
11320
|
-
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
|
|
11325
|
-
|
|
11326
|
-
|
|
11327
|
-
|
|
11425
|
+
const synced = await withSpinner("Fetching latest pricing data...", async () => {
|
|
11426
|
+
let count = 0;
|
|
11427
|
+
for (const m of data.models) {
|
|
11428
|
+
try {
|
|
11429
|
+
await client.mutation(api.modelPricing.upsert, {
|
|
11430
|
+
model: m.name,
|
|
11431
|
+
provider: "anthropic",
|
|
11432
|
+
inputPerMTok: m.input_per_mtok,
|
|
11433
|
+
outputPerMTok: m.output_per_mtok,
|
|
11434
|
+
cacheReadPerMTok: m.cache_read_per_mtok,
|
|
11435
|
+
batchDiscount: m.batch_discount
|
|
11436
|
+
});
|
|
11437
|
+
count++;
|
|
11438
|
+
} catch {
|
|
11439
|
+
}
|
|
11328
11440
|
}
|
|
11329
|
-
|
|
11441
|
+
return count;
|
|
11442
|
+
});
|
|
11330
11443
|
logConvexStatus(synced > 0);
|
|
11331
11444
|
if (synced > 0) {
|
|
11332
11445
|
console.log(chalk6.dim(` ${synced} models synced to Convex.`));
|
|
@@ -11354,7 +11467,13 @@ var statusCommand = new Command6("status").description("Show project status from
|
|
|
11354
11467
|
);
|
|
11355
11468
|
return;
|
|
11356
11469
|
}
|
|
11357
|
-
|
|
11470
|
+
let registry;
|
|
11471
|
+
try {
|
|
11472
|
+
registry = JSON.parse(readFileSync9(registryPath, "utf-8"));
|
|
11473
|
+
} catch {
|
|
11474
|
+
console.error(`Failed to parse ${registryPath}: file contains invalid JSON`);
|
|
11475
|
+
return process.exit(1);
|
|
11476
|
+
}
|
|
11358
11477
|
const projectName = options.project ?? registry.project ?? getProjectName();
|
|
11359
11478
|
const summaryPath = resolve9(
|
|
11360
11479
|
process.cwd(),
|
|
@@ -11366,7 +11485,13 @@ var statusCommand = new Command6("status").description("Show project status from
|
|
|
11366
11485
|
const hasTracking = existsSync8(summaryPath);
|
|
11367
11486
|
const actuals = /* @__PURE__ */ new Map();
|
|
11368
11487
|
if (hasTracking) {
|
|
11369
|
-
|
|
11488
|
+
let summary;
|
|
11489
|
+
try {
|
|
11490
|
+
summary = JSON.parse(readFileSync9(summaryPath, "utf-8"));
|
|
11491
|
+
} catch {
|
|
11492
|
+
console.error(`Failed to parse ${summaryPath}: file contains invalid JSON`);
|
|
11493
|
+
return process.exit(1);
|
|
11494
|
+
}
|
|
11370
11495
|
for (const s of summary.sessions) {
|
|
11371
11496
|
if (!s.featureId || s.status === "untagged") continue;
|
|
11372
11497
|
const existing = actuals.get(s.featureId) ?? { tokens: 0, sessions: 0 };
|
|
@@ -11832,399 +11957,542 @@ function applyStatsAttribution(sessions, cache) {
|
|
|
11832
11957
|
}
|
|
11833
11958
|
|
|
11834
11959
|
// src/commands/track.ts
|
|
11835
|
-
|
|
11960
|
+
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
11961
|
+
var trackCommand = new Command7("track").description("Import Claude Code sessions and auto-tag to features").option("-p, --project <name>", "Project name").option("--registry <path>", "Path to registry.json").option("--sessions-dir <path>", "Path to sessions directory").option("--backfill", "Process all existing sessions (not just new)").option("--dry-run", "Show what would change without modifying files").option("-y, --yes", "Skip confirmation prompt").action(
|
|
11836
11962
|
async (options) => {
|
|
11837
|
-
|
|
11838
|
-
|
|
11963
|
+
try {
|
|
11964
|
+
const registryPath = options.registry ?? resolve10(process.cwd(), ".claude", "te", "registry.json");
|
|
11965
|
+
if (!existsSync10(registryPath)) {
|
|
11966
|
+
console.log(
|
|
11967
|
+
chalk8.yellow(
|
|
11968
|
+
"No registry found. Run `fathom analyze <spec> --project <name>` first."
|
|
11969
|
+
)
|
|
11970
|
+
);
|
|
11971
|
+
return;
|
|
11972
|
+
}
|
|
11973
|
+
let registry;
|
|
11974
|
+
try {
|
|
11975
|
+
registry = JSON.parse(readFileSync11(registryPath, "utf-8"));
|
|
11976
|
+
} catch {
|
|
11977
|
+
console.error(`Failed to parse ${registryPath}: file contains invalid JSON`);
|
|
11978
|
+
return process.exit(1);
|
|
11979
|
+
}
|
|
11980
|
+
const projectName = options.project ?? registry.project ?? getProjectName();
|
|
11981
|
+
console.log(chalk8.dim("Scanning for sessions..."));
|
|
11982
|
+
const sessionFiles = findSessionFiles(options.sessionsDir);
|
|
11983
|
+
if (sessionFiles.length === 0) {
|
|
11984
|
+
console.log(
|
|
11985
|
+
chalk8.yellow("No session files found. Run some Claude Code sessions first.")
|
|
11986
|
+
);
|
|
11987
|
+
return;
|
|
11988
|
+
}
|
|
11989
|
+
const trackingDir = resolve10(
|
|
11990
|
+
process.cwd(),
|
|
11991
|
+
".claude",
|
|
11992
|
+
"te",
|
|
11993
|
+
"tracking",
|
|
11994
|
+
"sessions"
|
|
11995
|
+
);
|
|
11996
|
+
const summaryPath = resolve10(
|
|
11997
|
+
process.cwd(),
|
|
11998
|
+
".claude",
|
|
11999
|
+
"te",
|
|
12000
|
+
"tracking",
|
|
12001
|
+
"summary.json"
|
|
12002
|
+
);
|
|
12003
|
+
const trackedIds = /* @__PURE__ */ new Set();
|
|
12004
|
+
if (!options.backfill && existsSync10(summaryPath)) {
|
|
12005
|
+
let summary;
|
|
12006
|
+
try {
|
|
12007
|
+
summary = JSON.parse(readFileSync11(summaryPath, "utf-8"));
|
|
12008
|
+
} catch {
|
|
12009
|
+
console.error(`Failed to parse ${summaryPath}: file contains invalid JSON`);
|
|
12010
|
+
process.exit(1);
|
|
12011
|
+
}
|
|
12012
|
+
for (const s of summary.sessions ?? []) {
|
|
12013
|
+
trackedIds.add(s.sessionId);
|
|
12014
|
+
}
|
|
12015
|
+
}
|
|
12016
|
+
const results = [];
|
|
12017
|
+
for (const file of sessionFiles) {
|
|
12018
|
+
const session = parseSessionFile(file);
|
|
12019
|
+
if (!session) continue;
|
|
12020
|
+
if (trackedIds.has(session.sessionId)) continue;
|
|
12021
|
+
const tag = autoTag(session, registry.features);
|
|
12022
|
+
results.push({ session, tag });
|
|
12023
|
+
}
|
|
12024
|
+
if (results.length === 0) {
|
|
12025
|
+
console.log(
|
|
12026
|
+
chalk8.dim("No new sessions to process.")
|
|
12027
|
+
);
|
|
12028
|
+
return;
|
|
12029
|
+
}
|
|
12030
|
+
const statsCache = readStatsCache();
|
|
12031
|
+
let attributionSummary = null;
|
|
12032
|
+
if (statsCache) {
|
|
12033
|
+
const allSessions = results.map((r) => r.session);
|
|
12034
|
+
const attribution = applyStatsAttribution(allSessions, statsCache);
|
|
12035
|
+
if (attribution.upgraded > 0) {
|
|
12036
|
+
attributionSummary = `Stats-cache: ${attribution.upgraded} sessions upgraded, ${attribution.unchanged} had API data, ${attribution.unattributed} unattributed`;
|
|
12037
|
+
}
|
|
12038
|
+
}
|
|
11839
12039
|
console.log(
|
|
11840
|
-
chalk8.
|
|
11841
|
-
|
|
12040
|
+
chalk8.bold(`
|
|
12041
|
+
Fathom \u2014 Track: ${projectName} (${results.length} sessions)`)
|
|
12042
|
+
);
|
|
12043
|
+
console.log(chalk8.dim("\u2500".repeat(65)));
|
|
12044
|
+
const table = new Table5({
|
|
12045
|
+
head: [
|
|
12046
|
+
chalk8.white("Session"),
|
|
12047
|
+
chalk8.white("Feature"),
|
|
12048
|
+
chalk8.white("Confidence"),
|
|
12049
|
+
chalk8.white("Tag"),
|
|
12050
|
+
chalk8.white("Tokens"),
|
|
12051
|
+
chalk8.white("Token Src")
|
|
12052
|
+
],
|
|
12053
|
+
colAligns: ["left", "left", "center", "center", "right", "center"]
|
|
12054
|
+
});
|
|
12055
|
+
let totalTokens = 0;
|
|
12056
|
+
let autoTagged = 0;
|
|
12057
|
+
let likely = 0;
|
|
12058
|
+
let untagged = 0;
|
|
12059
|
+
for (const { session, tag } of results) {
|
|
12060
|
+
const tokens = session.inputTokens + session.outputTokens;
|
|
12061
|
+
totalTokens += tokens;
|
|
12062
|
+
const featureLabel = tag.featureId ? registry.features.find((f) => f.id === tag.featureId)?.name ?? tag.featureId : chalk8.dim("\u2014");
|
|
12063
|
+
const confidenceColor = tag.status === "auto-tagged" ? chalk8.green : tag.status === "likely" ? chalk8.yellow : chalk8.dim;
|
|
12064
|
+
if (tag.status === "auto-tagged") autoTagged++;
|
|
12065
|
+
else if (tag.status === "likely") likely++;
|
|
12066
|
+
else untagged++;
|
|
12067
|
+
const srcColor = session.tokenSource === "api" ? chalk8.green : session.tokenSource === "stats-cache" ? chalk8.cyan : session.tokenSource === "tokenizer" ? chalk8.yellow : chalk8.dim;
|
|
12068
|
+
table.push([
|
|
12069
|
+
session.sessionId.slice(0, 12) + "\u2026",
|
|
12070
|
+
featureLabel,
|
|
12071
|
+
confidenceColor(`${(tag.confidence * 100).toFixed(0)}%`),
|
|
12072
|
+
tag.source,
|
|
12073
|
+
tokens.toLocaleString(),
|
|
12074
|
+
srcColor(session.tokenSource)
|
|
12075
|
+
]);
|
|
12076
|
+
}
|
|
12077
|
+
console.log(table.toString());
|
|
12078
|
+
console.log(
|
|
12079
|
+
chalk8.dim(
|
|
12080
|
+
`
|
|
12081
|
+
Total: ${totalTokens.toLocaleString()} tokens across ${results.length} sessions`
|
|
11842
12082
|
)
|
|
11843
12083
|
);
|
|
11844
|
-
return;
|
|
11845
|
-
}
|
|
11846
|
-
const registry = JSON.parse(
|
|
11847
|
-
readFileSync11(registryPath, "utf-8")
|
|
11848
|
-
);
|
|
11849
|
-
const projectName = options.project ?? registry.project ?? getProjectName();
|
|
11850
|
-
console.log(chalk8.dim("Scanning for sessions..."));
|
|
11851
|
-
const sessionFiles = findSessionFiles(options.sessionsDir);
|
|
11852
|
-
if (sessionFiles.length === 0) {
|
|
11853
12084
|
console.log(
|
|
11854
|
-
chalk8.
|
|
12085
|
+
chalk8.dim(
|
|
12086
|
+
`Tags: ${autoTagged} auto-tagged, ${likely} likely, ${untagged} untagged`
|
|
12087
|
+
)
|
|
11855
12088
|
);
|
|
11856
|
-
|
|
12089
|
+
if (attributionSummary) {
|
|
12090
|
+
console.log(chalk8.cyan(` ${attributionSummary}`));
|
|
12091
|
+
}
|
|
12092
|
+
if (options.dryRun) {
|
|
12093
|
+
console.log(chalk8.dim(`
|
|
12094
|
+
[dry-run] Would write ${results.length} sessions to tracking. No files modified.`));
|
|
12095
|
+
return;
|
|
12096
|
+
}
|
|
12097
|
+
if (!options.yes) {
|
|
12098
|
+
const proceed = await confirm3({
|
|
12099
|
+
message: `Track ${results.length} sessions and update summary? (use --yes to skip)`,
|
|
12100
|
+
default: true
|
|
12101
|
+
});
|
|
12102
|
+
if (!proceed) {
|
|
12103
|
+
console.log(chalk8.dim("\n Aborted."));
|
|
12104
|
+
return;
|
|
12105
|
+
}
|
|
12106
|
+
}
|
|
12107
|
+
mkdirSync8(trackingDir, { recursive: true });
|
|
12108
|
+
const sessionRecords = results.map(({ session, tag }) => ({
|
|
12109
|
+
sessionId: session.sessionId,
|
|
12110
|
+
featureId: tag.featureId,
|
|
12111
|
+
confidence: tag.confidence,
|
|
12112
|
+
source: tag.source,
|
|
12113
|
+
status: tag.status,
|
|
12114
|
+
inputTokens: session.inputTokens,
|
|
12115
|
+
outputTokens: session.outputTokens,
|
|
12116
|
+
cacheReadTokens: session.cacheReadTokens,
|
|
12117
|
+
model: session.model,
|
|
12118
|
+
sessionStart: session.sessionStart,
|
|
12119
|
+
sessionEnd: session.sessionEnd,
|
|
12120
|
+
activeMinutes: session.activeMinutes,
|
|
12121
|
+
tokenSource: session.tokenSource,
|
|
12122
|
+
trackedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12123
|
+
}));
|
|
12124
|
+
let existing = { sessions: [] };
|
|
12125
|
+
if (existsSync10(summaryPath)) {
|
|
12126
|
+
try {
|
|
12127
|
+
existing = JSON.parse(readFileSync11(summaryPath, "utf-8"));
|
|
12128
|
+
} catch {
|
|
12129
|
+
console.error(`Failed to parse ${summaryPath}: file contains invalid JSON`);
|
|
12130
|
+
process.exit(1);
|
|
12131
|
+
}
|
|
12132
|
+
}
|
|
12133
|
+
existing.sessions.push(...sessionRecords);
|
|
12134
|
+
writeFileSync7(
|
|
12135
|
+
summaryPath,
|
|
12136
|
+
JSON.stringify(
|
|
12137
|
+
{
|
|
12138
|
+
project: projectName,
|
|
12139
|
+
sessions: existing.sessions,
|
|
12140
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
12141
|
+
},
|
|
12142
|
+
null,
|
|
12143
|
+
2
|
|
12144
|
+
)
|
|
12145
|
+
);
|
|
12146
|
+
console.log(chalk8.dim(`
|
|
12147
|
+
Tracking saved: ${summaryPath}`));
|
|
12148
|
+
if (isConvexConnected()) {
|
|
12149
|
+
const syncCount = await withSpinner("Syncing to Convex...", async () => {
|
|
12150
|
+
let count = 0;
|
|
12151
|
+
for (const { session, tag } of results) {
|
|
12152
|
+
const synced = await syncActual({
|
|
12153
|
+
projectName,
|
|
12154
|
+
sessionId: session.sessionId,
|
|
12155
|
+
provider: "claude",
|
|
12156
|
+
model: session.model,
|
|
12157
|
+
inputTokens: session.inputTokens,
|
|
12158
|
+
outputTokens: session.outputTokens,
|
|
12159
|
+
cacheReadTokens: session.cacheReadTokens,
|
|
12160
|
+
cacheCreateTokens: 0,
|
|
12161
|
+
toolCalls: 0,
|
|
12162
|
+
estimatedCost: 0,
|
|
12163
|
+
sessionStart: session.sessionStart,
|
|
12164
|
+
sessionEnd: session.sessionEnd,
|
|
12165
|
+
activeMinutes: session.activeMinutes,
|
|
12166
|
+
featureId: tag.featureId,
|
|
12167
|
+
tagConfidence: tag.confidence,
|
|
12168
|
+
tagSource: tag.source,
|
|
12169
|
+
tagStatus: tag.status
|
|
12170
|
+
});
|
|
12171
|
+
if (synced) count++;
|
|
12172
|
+
}
|
|
12173
|
+
return count;
|
|
12174
|
+
});
|
|
12175
|
+
logConvexStatus(syncCount > 0);
|
|
12176
|
+
if (syncCount > 0) {
|
|
12177
|
+
console.log(chalk8.dim(` ${syncCount} sessions synced to Convex.`));
|
|
12178
|
+
}
|
|
12179
|
+
} else {
|
|
12180
|
+
logConvexStatus(false);
|
|
12181
|
+
}
|
|
12182
|
+
} catch (err) {
|
|
12183
|
+
if (err && typeof err === "object" && "name" in err && err.name === "ExitPromptError") {
|
|
12184
|
+
console.log(chalk8.dim("\n Exiting.\n"));
|
|
12185
|
+
return;
|
|
12186
|
+
}
|
|
12187
|
+
throw err;
|
|
11857
12188
|
}
|
|
11858
|
-
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
12189
|
+
}
|
|
12190
|
+
);
|
|
12191
|
+
|
|
12192
|
+
// src/commands/reconcile.ts
|
|
12193
|
+
import { Command as Command8 } from "commander";
|
|
12194
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync11, mkdirSync as mkdirSync9 } from "fs";
|
|
12195
|
+
import { resolve as resolve11 } from "path";
|
|
12196
|
+
import chalk9 from "chalk";
|
|
12197
|
+
import Table6 from "cli-table3";
|
|
12198
|
+
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
12199
|
+
function computeCalibrationEntries(features, actuals, sessions) {
|
|
12200
|
+
const entries = [];
|
|
12201
|
+
for (const feature of features) {
|
|
12202
|
+
const actual = actuals.get(feature.id);
|
|
12203
|
+
if (!actual) continue;
|
|
12204
|
+
const featureSessions = sessions.filter(
|
|
12205
|
+
(s) => s.featureId === feature.id && s.status !== "untagged"
|
|
11864
12206
|
);
|
|
11865
|
-
const
|
|
12207
|
+
const dominantModel = featureSessions.length > 0 ? featureSessions[0].model : "unknown";
|
|
12208
|
+
entries.push({
|
|
12209
|
+
model: dominantModel,
|
|
12210
|
+
taskType: "mixed",
|
|
12211
|
+
// features span multiple task types
|
|
12212
|
+
complexity: "M",
|
|
12213
|
+
// default; could be refined with registry metadata
|
|
12214
|
+
estimatedTokens: feature.estimated_tokens,
|
|
12215
|
+
actualTokens: actual.tokens,
|
|
12216
|
+
accuracy: feature.estimated_tokens > 0 ? actual.tokens / feature.estimated_tokens : 0,
|
|
12217
|
+
timestamp: Date.now()
|
|
12218
|
+
});
|
|
12219
|
+
}
|
|
12220
|
+
return entries;
|
|
12221
|
+
}
|
|
12222
|
+
function persistCalibration(entries, projectDir = process.cwd()) {
|
|
12223
|
+
const calPath = resolve11(projectDir, ".fathom", "store", "calibration.json");
|
|
12224
|
+
mkdirSync9(resolve11(projectDir, ".fathom", "store"), { recursive: true });
|
|
12225
|
+
let existing = { entries: [] };
|
|
12226
|
+
try {
|
|
12227
|
+
existing = JSON.parse(readFileSync12(calPath, "utf-8"));
|
|
12228
|
+
} catch {
|
|
12229
|
+
}
|
|
12230
|
+
existing.entries.push(...entries);
|
|
12231
|
+
writeFileSync8(calPath, JSON.stringify(existing, null, 2));
|
|
12232
|
+
}
|
|
12233
|
+
var reconcileCommand = new Command8("reconcile").description("Compare estimates vs actuals per feature").option("-p, --project <name>", "Project name").option("--registry <path>", "Path to registry.json").option("--dry-run", "Show reconciliation without syncing").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
|
|
12234
|
+
try {
|
|
12235
|
+
const registryPath = options.registry ?? resolve11(process.cwd(), ".claude", "te", "registry.json");
|
|
12236
|
+
const summaryPath = resolve11(
|
|
11866
12237
|
process.cwd(),
|
|
11867
12238
|
".claude",
|
|
11868
12239
|
"te",
|
|
11869
12240
|
"tracking",
|
|
11870
12241
|
"summary.json"
|
|
11871
12242
|
);
|
|
11872
|
-
|
|
11873
|
-
|
|
11874
|
-
|
|
11875
|
-
|
|
11876
|
-
|
|
11877
|
-
|
|
11878
|
-
|
|
11879
|
-
const results = [];
|
|
11880
|
-
for (const file of sessionFiles) {
|
|
11881
|
-
const session = parseSessionFile(file);
|
|
11882
|
-
if (!session) continue;
|
|
11883
|
-
if (trackedIds.has(session.sessionId)) continue;
|
|
11884
|
-
const tag = autoTag(session, registry.features);
|
|
11885
|
-
results.push({ session, tag });
|
|
12243
|
+
if (!existsSync11(registryPath)) {
|
|
12244
|
+
console.log(
|
|
12245
|
+
chalk9.yellow(
|
|
12246
|
+
"No registry found. Run `fathom analyze <spec> --project <name>` first."
|
|
12247
|
+
)
|
|
12248
|
+
);
|
|
12249
|
+
return;
|
|
11886
12250
|
}
|
|
11887
|
-
if (
|
|
12251
|
+
if (!existsSync11(summaryPath)) {
|
|
11888
12252
|
console.log(
|
|
11889
|
-
|
|
12253
|
+
chalk9.yellow(
|
|
12254
|
+
"No tracking data found. Run `fathom track --project <name>` first."
|
|
12255
|
+
)
|
|
11890
12256
|
);
|
|
11891
12257
|
return;
|
|
11892
12258
|
}
|
|
11893
|
-
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
|
|
11897
|
-
|
|
11898
|
-
|
|
11899
|
-
|
|
11900
|
-
|
|
12259
|
+
let registry;
|
|
12260
|
+
try {
|
|
12261
|
+
registry = JSON.parse(readFileSync12(registryPath, "utf-8"));
|
|
12262
|
+
} catch {
|
|
12263
|
+
console.error(`Failed to parse ${registryPath}: file contains invalid JSON`);
|
|
12264
|
+
return process.exit(1);
|
|
12265
|
+
}
|
|
12266
|
+
let summary;
|
|
12267
|
+
try {
|
|
12268
|
+
summary = JSON.parse(readFileSync12(summaryPath, "utf-8"));
|
|
12269
|
+
} catch {
|
|
12270
|
+
console.error(`Failed to parse ${summaryPath}: file contains invalid JSON`);
|
|
12271
|
+
return process.exit(1);
|
|
12272
|
+
}
|
|
12273
|
+
const projectName = options.project ?? registry.project ?? getProjectName();
|
|
12274
|
+
const data = loadData();
|
|
12275
|
+
const actuals = /* @__PURE__ */ new Map();
|
|
12276
|
+
for (const session of summary.sessions) {
|
|
12277
|
+
if (!session.featureId || session.status === "untagged") continue;
|
|
12278
|
+
const existing = actuals.get(session.featureId) ?? {
|
|
12279
|
+
tokens: 0,
|
|
12280
|
+
cost: 0,
|
|
12281
|
+
sessions: 0,
|
|
12282
|
+
minutes: 0
|
|
12283
|
+
};
|
|
12284
|
+
const totalTokens = session.inputTokens + session.outputTokens;
|
|
12285
|
+
const pricing = getModelPricing(data.models, session.model);
|
|
12286
|
+
const cost = pricing ? session.inputTokens / 1e6 * pricing.input_per_mtok + session.outputTokens / 1e6 * pricing.output_per_mtok + session.cacheReadTokens / 1e6 * pricing.cache_read_per_mtok : 0;
|
|
12287
|
+
existing.tokens += totalTokens;
|
|
12288
|
+
existing.cost += cost;
|
|
12289
|
+
existing.sessions += 1;
|
|
12290
|
+
existing.minutes += session.activeMinutes ?? 0;
|
|
12291
|
+
actuals.set(session.featureId, existing);
|
|
11901
12292
|
}
|
|
11902
12293
|
console.log(
|
|
11903
|
-
|
|
11904
|
-
Fathom \u2014
|
|
12294
|
+
chalk9.bold(`
|
|
12295
|
+
Fathom \u2014 Reconcile: ${projectName}`)
|
|
11905
12296
|
);
|
|
11906
|
-
console.log(
|
|
11907
|
-
const table = new
|
|
12297
|
+
console.log(chalk9.dim("\u2500".repeat(75)));
|
|
12298
|
+
const table = new Table6({
|
|
11908
12299
|
head: [
|
|
11909
|
-
|
|
11910
|
-
|
|
11911
|
-
|
|
11912
|
-
|
|
11913
|
-
|
|
11914
|
-
|
|
12300
|
+
chalk9.white("Feature"),
|
|
12301
|
+
chalk9.white("Est. Tokens"),
|
|
12302
|
+
chalk9.white("Act. Tokens"),
|
|
12303
|
+
chalk9.white("Drift"),
|
|
12304
|
+
chalk9.white("Act. Cost"),
|
|
12305
|
+
chalk9.white("Sessions")
|
|
11915
12306
|
],
|
|
11916
|
-
colAligns: ["left", "
|
|
12307
|
+
colAligns: ["left", "right", "right", "center", "right", "center"]
|
|
11917
12308
|
});
|
|
11918
|
-
let
|
|
11919
|
-
let
|
|
11920
|
-
let
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
const
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
|
|
11930
|
-
|
|
11931
|
-
|
|
11932
|
-
|
|
11933
|
-
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
|
|
11937
|
-
|
|
11938
|
-
|
|
12309
|
+
let totalEstimated = 0;
|
|
12310
|
+
let totalActual = 0;
|
|
12311
|
+
let totalCost = 0;
|
|
12312
|
+
for (const feature of registry.features) {
|
|
12313
|
+
const actual = actuals.get(feature.id);
|
|
12314
|
+
const estimated = feature.estimated_tokens;
|
|
12315
|
+
totalEstimated += estimated;
|
|
12316
|
+
if (actual) {
|
|
12317
|
+
totalActual += actual.tokens;
|
|
12318
|
+
totalCost += actual.cost;
|
|
12319
|
+
const drift = (actual.tokens - estimated) / estimated * 100;
|
|
12320
|
+
const driftLabel = drift > 20 ? chalk9.red(`+${drift.toFixed(0)}%`) : drift < -20 ? chalk9.green(`${drift.toFixed(0)}%`) : chalk9.white(`${drift >= 0 ? "+" : ""}${drift.toFixed(0)}%`);
|
|
12321
|
+
table.push([
|
|
12322
|
+
feature.name,
|
|
12323
|
+
estimated.toLocaleString(),
|
|
12324
|
+
actual.tokens.toLocaleString(),
|
|
12325
|
+
driftLabel,
|
|
12326
|
+
`$${actual.cost.toFixed(2)}`,
|
|
12327
|
+
actual.sessions.toString()
|
|
12328
|
+
]);
|
|
12329
|
+
} else {
|
|
12330
|
+
table.push([
|
|
12331
|
+
feature.name,
|
|
12332
|
+
estimated.toLocaleString(),
|
|
12333
|
+
chalk9.dim("\u2014"),
|
|
12334
|
+
chalk9.dim("\u2014"),
|
|
12335
|
+
chalk9.dim("\u2014"),
|
|
12336
|
+
chalk9.dim("0")
|
|
12337
|
+
]);
|
|
12338
|
+
}
|
|
11939
12339
|
}
|
|
11940
12340
|
console.log(table.toString());
|
|
12341
|
+
const overallDrift = totalEstimated > 0 ? (totalActual - totalEstimated) / totalEstimated * 100 : 0;
|
|
12342
|
+
const driftColor = Math.abs(overallDrift) > 20 ? overallDrift > 0 ? chalk9.red : chalk9.green : chalk9.white;
|
|
11941
12343
|
console.log(
|
|
11942
|
-
|
|
12344
|
+
chalk9.bold(
|
|
11943
12345
|
`
|
|
11944
|
-
|
|
12346
|
+
Overall: ${totalActual.toLocaleString()} / ${totalEstimated.toLocaleString()} tokens \u2014 ` + driftColor(
|
|
12347
|
+
`${overallDrift >= 0 ? "+" : ""}${overallDrift.toFixed(1)}% drift`
|
|
12348
|
+
)
|
|
11945
12349
|
)
|
|
11946
12350
|
);
|
|
12351
|
+
console.log(chalk9.dim(`Total actual cost: $${totalCost.toFixed(2)}`));
|
|
11947
12352
|
console.log(
|
|
11948
|
-
|
|
11949
|
-
`
|
|
12353
|
+
chalk9.dim(
|
|
12354
|
+
`Features with data: ${actuals.size} / ${registry.features.length}`
|
|
11950
12355
|
)
|
|
11951
12356
|
);
|
|
11952
|
-
|
|
11953
|
-
|
|
11954
|
-
|
|
11955
|
-
|
|
11956
|
-
|
|
11957
|
-
|
|
11958
|
-
|
|
11959
|
-
|
|
11960
|
-
|
|
11961
|
-
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
|
|
11965
|
-
|
|
11966
|
-
|
|
11967
|
-
|
|
11968
|
-
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
12357
|
+
const config = readProjectConfig();
|
|
12358
|
+
const adminKey = resolveAdminKey(config?.anthropicAdminKey ?? void 0);
|
|
12359
|
+
let adminReport = null;
|
|
12360
|
+
if (adminKey) {
|
|
12361
|
+
const sessions = summary.sessions;
|
|
12362
|
+
const starts = sessions.map((s) => s.sessionStart).filter((t) => typeof t === "number" && t > 0);
|
|
12363
|
+
if (starts.length > 0) {
|
|
12364
|
+
const earliest = new Date(Math.min(...starts));
|
|
12365
|
+
const latest = /* @__PURE__ */ new Date();
|
|
12366
|
+
earliest.setHours(0, 0, 0, 0);
|
|
12367
|
+
adminReport = await withSpinner(
|
|
12368
|
+
"Fetching admin usage data...",
|
|
12369
|
+
async () => fetchAdminUsage({
|
|
12370
|
+
adminKey,
|
|
12371
|
+
startingAt: earliest.toISOString(),
|
|
12372
|
+
endingAt: latest.toISOString(),
|
|
12373
|
+
bucketWidth: "1d",
|
|
12374
|
+
groupBy: ["model"]
|
|
12375
|
+
})
|
|
12376
|
+
);
|
|
12377
|
+
if (adminReport) {
|
|
12378
|
+
const billedTotal = adminReport.totalInputTokens + adminReport.totalOutputTokens;
|
|
12379
|
+
const gap = billedTotal - totalActual;
|
|
12380
|
+
const gapPct = totalActual > 0 ? gap / totalActual * 100 : 0;
|
|
12381
|
+
console.log(chalk9.bold("\n Billed vs Tracked (Admin API)"));
|
|
12382
|
+
console.log(chalk9.dim(" " + "\u2500".repeat(45)));
|
|
12383
|
+
console.log(
|
|
12384
|
+
chalk9.dim(
|
|
12385
|
+
` Tracked: ${totalActual.toLocaleString()} tokens`
|
|
12386
|
+
)
|
|
12387
|
+
);
|
|
12388
|
+
console.log(
|
|
12389
|
+
chalk9.dim(
|
|
12390
|
+
` Billed: ${billedTotal.toLocaleString()} tokens`
|
|
12391
|
+
)
|
|
12392
|
+
);
|
|
12393
|
+
const gapColor = Math.abs(gapPct) > 20 ? chalk9.red : chalk9.yellow;
|
|
12394
|
+
console.log(
|
|
12395
|
+
gapColor(
|
|
12396
|
+
` Gap: ${gap.toLocaleString()} tokens (${gapPct >= 0 ? "+" : ""}${gapPct.toFixed(1)}%)`
|
|
12397
|
+
)
|
|
12398
|
+
);
|
|
12399
|
+
if (adminReport.totalCacheReadTokens > 0) {
|
|
12400
|
+
console.log(
|
|
12401
|
+
chalk9.dim(
|
|
12402
|
+
` Cache: ${adminReport.totalCacheReadTokens.toLocaleString()} cache-read, ${adminReport.totalCacheCreationTokens.toLocaleString()} cache-create`
|
|
12403
|
+
)
|
|
12404
|
+
);
|
|
12405
|
+
}
|
|
12406
|
+
} else {
|
|
12407
|
+
console.log(
|
|
12408
|
+
chalk9.dim("\n Admin API: Could not fetch usage data (check key/permissions)")
|
|
12409
|
+
);
|
|
12410
|
+
}
|
|
12411
|
+
}
|
|
11975
12412
|
}
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
|
|
11979
|
-
|
|
11980
|
-
{
|
|
11981
|
-
project: projectName,
|
|
11982
|
-
sessions: existing.sessions,
|
|
11983
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
11984
|
-
},
|
|
11985
|
-
null,
|
|
11986
|
-
2
|
|
11987
|
-
)
|
|
12413
|
+
const calibrationEntries = computeCalibrationEntries(
|
|
12414
|
+
registry.features,
|
|
12415
|
+
actuals,
|
|
12416
|
+
summary.sessions
|
|
11988
12417
|
);
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
outputTokens: session.outputTokens,
|
|
12001
|
-
cacheReadTokens: session.cacheReadTokens,
|
|
12002
|
-
cacheCreateTokens: 0,
|
|
12003
|
-
toolCalls: 0,
|
|
12004
|
-
estimatedCost: 0,
|
|
12005
|
-
sessionStart: session.sessionStart,
|
|
12006
|
-
sessionEnd: session.sessionEnd,
|
|
12007
|
-
activeMinutes: session.activeMinutes,
|
|
12008
|
-
featureId: tag.featureId,
|
|
12009
|
-
tagConfidence: tag.confidence,
|
|
12010
|
-
tagSource: tag.source,
|
|
12011
|
-
tagStatus: tag.status
|
|
12012
|
-
});
|
|
12013
|
-
if (synced) syncCount++;
|
|
12014
|
-
}
|
|
12015
|
-
logConvexStatus(syncCount > 0);
|
|
12016
|
-
if (syncCount > 0) {
|
|
12017
|
-
console.log(chalk8.dim(` ${syncCount} sessions synced to Convex.`));
|
|
12018
|
-
}
|
|
12019
|
-
} else {
|
|
12020
|
-
logConvexStatus(false);
|
|
12418
|
+
if (calibrationEntries.length > 0) {
|
|
12419
|
+
const avgAccuracy = calibrationEntries.reduce((sum, e) => sum + e.accuracy, 0) / calibrationEntries.length;
|
|
12420
|
+
const pct = Math.round(
|
|
12421
|
+
Math.min(avgAccuracy, 1 / avgAccuracy) * 100
|
|
12422
|
+
);
|
|
12423
|
+
console.log(
|
|
12424
|
+
chalk9.bold(
|
|
12425
|
+
`
|
|
12426
|
+
Calibration: ${calibrationEntries.length} features reconciled. Average accuracy: ${pct}%.`
|
|
12427
|
+
)
|
|
12428
|
+
);
|
|
12021
12429
|
}
|
|
12022
|
-
|
|
12023
|
-
);
|
|
12024
|
-
|
|
12025
|
-
|
|
12026
|
-
|
|
12027
|
-
|
|
12028
|
-
|
|
12029
|
-
import chalk9 from "chalk";
|
|
12030
|
-
import Table6 from "cli-table3";
|
|
12031
|
-
var reconcileCommand = new Command8("reconcile").description("Compare estimates vs actuals per feature").option("-p, --project <name>", "Project name").option("--registry <path>", "Path to registry.json").action(async (options) => {
|
|
12032
|
-
const registryPath = options.registry ?? resolve11(process.cwd(), ".claude", "te", "registry.json");
|
|
12033
|
-
const summaryPath = resolve11(
|
|
12034
|
-
process.cwd(),
|
|
12035
|
-
".claude",
|
|
12036
|
-
"te",
|
|
12037
|
-
"tracking",
|
|
12038
|
-
"summary.json"
|
|
12039
|
-
);
|
|
12040
|
-
if (!existsSync11(registryPath)) {
|
|
12041
|
-
console.log(
|
|
12042
|
-
chalk9.yellow(
|
|
12043
|
-
"No registry found. Run `fathom analyze <spec> --project <name>` first."
|
|
12044
|
-
)
|
|
12045
|
-
);
|
|
12046
|
-
return;
|
|
12047
|
-
}
|
|
12048
|
-
if (!existsSync11(summaryPath)) {
|
|
12049
|
-
console.log(
|
|
12050
|
-
chalk9.yellow(
|
|
12051
|
-
"No tracking data found. Run `fathom track --project <name>` first."
|
|
12052
|
-
)
|
|
12053
|
-
);
|
|
12054
|
-
return;
|
|
12055
|
-
}
|
|
12056
|
-
const registry = JSON.parse(readFileSync12(registryPath, "utf-8"));
|
|
12057
|
-
const summary = JSON.parse(readFileSync12(summaryPath, "utf-8"));
|
|
12058
|
-
const projectName = options.project ?? registry.project ?? getProjectName();
|
|
12059
|
-
const data = loadData();
|
|
12060
|
-
const actuals = /* @__PURE__ */ new Map();
|
|
12061
|
-
for (const session of summary.sessions) {
|
|
12062
|
-
if (!session.featureId || session.status === "untagged") continue;
|
|
12063
|
-
const existing = actuals.get(session.featureId) ?? {
|
|
12064
|
-
tokens: 0,
|
|
12065
|
-
cost: 0,
|
|
12066
|
-
sessions: 0,
|
|
12067
|
-
minutes: 0
|
|
12068
|
-
};
|
|
12069
|
-
const totalTokens = session.inputTokens + session.outputTokens;
|
|
12070
|
-
const pricing = getModelPricing(data.models, session.model);
|
|
12071
|
-
const cost = pricing ? session.inputTokens / 1e6 * pricing.input_per_mtok + session.outputTokens / 1e6 * pricing.output_per_mtok + session.cacheReadTokens / 1e6 * pricing.cache_read_per_mtok : 0;
|
|
12072
|
-
existing.tokens += totalTokens;
|
|
12073
|
-
existing.cost += cost;
|
|
12074
|
-
existing.sessions += 1;
|
|
12075
|
-
existing.minutes += session.activeMinutes ?? 0;
|
|
12076
|
-
actuals.set(session.featureId, existing);
|
|
12077
|
-
}
|
|
12078
|
-
console.log(
|
|
12079
|
-
chalk9.bold(`
|
|
12080
|
-
Fathom \u2014 Reconcile: ${projectName}`)
|
|
12081
|
-
);
|
|
12082
|
-
console.log(chalk9.dim("\u2500".repeat(75)));
|
|
12083
|
-
const table = new Table6({
|
|
12084
|
-
head: [
|
|
12085
|
-
chalk9.white("Feature"),
|
|
12086
|
-
chalk9.white("Est. Tokens"),
|
|
12087
|
-
chalk9.white("Act. Tokens"),
|
|
12088
|
-
chalk9.white("Drift"),
|
|
12089
|
-
chalk9.white("Act. Cost"),
|
|
12090
|
-
chalk9.white("Sessions")
|
|
12091
|
-
],
|
|
12092
|
-
colAligns: ["left", "right", "right", "center", "right", "center"]
|
|
12093
|
-
});
|
|
12094
|
-
let totalEstimated = 0;
|
|
12095
|
-
let totalActual = 0;
|
|
12096
|
-
let totalCost = 0;
|
|
12097
|
-
for (const feature of registry.features) {
|
|
12098
|
-
const actual = actuals.get(feature.id);
|
|
12099
|
-
const estimated = feature.estimated_tokens;
|
|
12100
|
-
totalEstimated += estimated;
|
|
12101
|
-
if (actual) {
|
|
12102
|
-
totalActual += actual.tokens;
|
|
12103
|
-
totalCost += actual.cost;
|
|
12104
|
-
const drift = (actual.tokens - estimated) / estimated * 100;
|
|
12105
|
-
const driftLabel = drift > 20 ? chalk9.red(`+${drift.toFixed(0)}%`) : drift < -20 ? chalk9.green(`${drift.toFixed(0)}%`) : chalk9.white(`${drift >= 0 ? "+" : ""}${drift.toFixed(0)}%`);
|
|
12106
|
-
table.push([
|
|
12107
|
-
feature.name,
|
|
12108
|
-
estimated.toLocaleString(),
|
|
12109
|
-
actual.tokens.toLocaleString(),
|
|
12110
|
-
driftLabel,
|
|
12111
|
-
`$${actual.cost.toFixed(2)}`,
|
|
12112
|
-
actual.sessions.toString()
|
|
12113
|
-
]);
|
|
12114
|
-
} else {
|
|
12115
|
-
table.push([
|
|
12116
|
-
feature.name,
|
|
12117
|
-
estimated.toLocaleString(),
|
|
12118
|
-
chalk9.dim("\u2014"),
|
|
12119
|
-
chalk9.dim("\u2014"),
|
|
12120
|
-
chalk9.dim("\u2014"),
|
|
12121
|
-
chalk9.dim("0")
|
|
12122
|
-
]);
|
|
12430
|
+
if (!options.dryRun && calibrationEntries.length > 0) {
|
|
12431
|
+
persistCalibration(calibrationEntries);
|
|
12432
|
+
console.log(
|
|
12433
|
+
chalk9.dim(
|
|
12434
|
+
` Calibration data saved to .fathom/store/calibration.json`
|
|
12435
|
+
)
|
|
12436
|
+
);
|
|
12123
12437
|
}
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
)
|
|
12134
|
-
)
|
|
12135
|
-
);
|
|
12136
|
-
console.log(chalk9.dim(`Total actual cost: $${totalCost.toFixed(2)}`));
|
|
12137
|
-
console.log(
|
|
12138
|
-
chalk9.dim(
|
|
12139
|
-
`Features with data: ${actuals.size} / ${registry.features.length}`
|
|
12140
|
-
)
|
|
12141
|
-
);
|
|
12142
|
-
const config = readProjectConfig();
|
|
12143
|
-
const adminKey = resolveAdminKey(config?.anthropicAdminKey ?? void 0);
|
|
12144
|
-
let adminReport = null;
|
|
12145
|
-
if (adminKey) {
|
|
12146
|
-
const sessions = summary.sessions;
|
|
12147
|
-
const starts = sessions.map((s) => s.sessionStart).filter((t) => typeof t === "number" && t > 0);
|
|
12148
|
-
if (starts.length > 0) {
|
|
12149
|
-
const earliest = new Date(Math.min(...starts));
|
|
12150
|
-
const latest = /* @__PURE__ */ new Date();
|
|
12151
|
-
earliest.setHours(0, 0, 0, 0);
|
|
12152
|
-
adminReport = await fetchAdminUsage({
|
|
12153
|
-
adminKey,
|
|
12154
|
-
startingAt: earliest.toISOString(),
|
|
12155
|
-
endingAt: latest.toISOString(),
|
|
12156
|
-
bucketWidth: "1d",
|
|
12157
|
-
groupBy: ["model"]
|
|
12438
|
+
if (options.dryRun) {
|
|
12439
|
+
console.log(chalk9.dim(`
|
|
12440
|
+
[dry-run] Would sync reconciliation to Convex. No changes made.`));
|
|
12441
|
+
return;
|
|
12442
|
+
}
|
|
12443
|
+
if (!options.yes) {
|
|
12444
|
+
const proceed = await confirm4({
|
|
12445
|
+
message: "Sync reconciliation data? (use --yes to skip)",
|
|
12446
|
+
default: true
|
|
12158
12447
|
});
|
|
12159
|
-
if (
|
|
12160
|
-
|
|
12161
|
-
|
|
12162
|
-
const gapPct = totalActual > 0 ? gap / totalActual * 100 : 0;
|
|
12163
|
-
console.log(chalk9.bold("\n Billed vs Tracked (Admin API)"));
|
|
12164
|
-
console.log(chalk9.dim(" " + "\u2500".repeat(45)));
|
|
12165
|
-
console.log(
|
|
12166
|
-
chalk9.dim(
|
|
12167
|
-
` Tracked: ${totalActual.toLocaleString()} tokens`
|
|
12168
|
-
)
|
|
12169
|
-
);
|
|
12170
|
-
console.log(
|
|
12171
|
-
chalk9.dim(
|
|
12172
|
-
` Billed: ${billedTotal.toLocaleString()} tokens`
|
|
12173
|
-
)
|
|
12174
|
-
);
|
|
12175
|
-
const gapColor = Math.abs(gapPct) > 20 ? chalk9.red : chalk9.yellow;
|
|
12176
|
-
console.log(
|
|
12177
|
-
gapColor(
|
|
12178
|
-
` Gap: ${gap.toLocaleString()} tokens (${gapPct >= 0 ? "+" : ""}${gapPct.toFixed(1)}%)`
|
|
12179
|
-
)
|
|
12180
|
-
);
|
|
12181
|
-
if (adminReport.totalCacheReadTokens > 0) {
|
|
12182
|
-
console.log(
|
|
12183
|
-
chalk9.dim(
|
|
12184
|
-
` Cache: ${adminReport.totalCacheReadTokens.toLocaleString()} cache-read, ${adminReport.totalCacheCreationTokens.toLocaleString()} cache-create`
|
|
12185
|
-
)
|
|
12186
|
-
);
|
|
12187
|
-
}
|
|
12188
|
-
} else {
|
|
12189
|
-
console.log(
|
|
12190
|
-
chalk9.dim("\n Admin API: Could not fetch usage data (check key/permissions)")
|
|
12191
|
-
);
|
|
12448
|
+
if (!proceed) {
|
|
12449
|
+
console.log(chalk9.dim("\n Aborted."));
|
|
12450
|
+
return;
|
|
12192
12451
|
}
|
|
12193
12452
|
}
|
|
12453
|
+
const overallAccuracy = totalEstimated > 0 ? 1 - Math.abs(totalActual - totalEstimated) / totalEstimated : 0;
|
|
12454
|
+
const featureDeltas = registry.features.filter((f) => actuals.has(f.id)).map((f) => {
|
|
12455
|
+
const actual = actuals.get(f.id);
|
|
12456
|
+
const delta = actual.tokens - f.estimated_tokens;
|
|
12457
|
+
const accuracy = f.estimated_tokens > 0 ? 1 - Math.abs(delta) / f.estimated_tokens : 0;
|
|
12458
|
+
return {
|
|
12459
|
+
featureId: f.id,
|
|
12460
|
+
featureName: f.name,
|
|
12461
|
+
estimatedTokens: f.estimated_tokens,
|
|
12462
|
+
actualTokens: actual.tokens,
|
|
12463
|
+
delta,
|
|
12464
|
+
accuracy,
|
|
12465
|
+
modelRecommended: "sonnet",
|
|
12466
|
+
modelUsed: "sonnet",
|
|
12467
|
+
sessionsCount: actual.sessions,
|
|
12468
|
+
activeMinutes: actual.minutes,
|
|
12469
|
+
tagQuality: { autoTagged: actual.sessions, likelyTagged: 0, manualTagged: 0 },
|
|
12470
|
+
insights: delta > 0 ? [`Over estimate by ${(delta / f.estimated_tokens * 100).toFixed(0)}%`] : [`Under estimate by ${(Math.abs(delta) / f.estimated_tokens * 100).toFixed(0)}%`]
|
|
12471
|
+
};
|
|
12472
|
+
});
|
|
12473
|
+
const totalSessions = Array.from(actuals.values()).reduce((s, a) => s + a.sessions, 0);
|
|
12474
|
+
const autoTagged = summary.sessions.filter(
|
|
12475
|
+
(s) => s.status === "auto-tagged"
|
|
12476
|
+
).length;
|
|
12477
|
+
const autoTagAccuracy = totalSessions > 0 ? autoTagged / totalSessions : 0;
|
|
12478
|
+
const synced = await withSpinner(
|
|
12479
|
+
"Syncing reconciliation...",
|
|
12480
|
+
async () => syncReconciliation({
|
|
12481
|
+
projectName,
|
|
12482
|
+
featureDeltas,
|
|
12483
|
+
overallAccuracy,
|
|
12484
|
+
autoTagAccuracy,
|
|
12485
|
+
suggestedAdjustments: []
|
|
12486
|
+
})
|
|
12487
|
+
);
|
|
12488
|
+
logConvexStatus(synced);
|
|
12489
|
+
} catch (err) {
|
|
12490
|
+
if (err && typeof err === "object" && "name" in err && err.name === "ExitPromptError") {
|
|
12491
|
+
console.log(chalk9.dim("\n Exiting.\n"));
|
|
12492
|
+
return;
|
|
12493
|
+
}
|
|
12494
|
+
throw err;
|
|
12194
12495
|
}
|
|
12195
|
-
const overallAccuracy = totalEstimated > 0 ? 1 - Math.abs(totalActual - totalEstimated) / totalEstimated : 0;
|
|
12196
|
-
const featureDeltas = registry.features.filter((f) => actuals.has(f.id)).map((f) => {
|
|
12197
|
-
const actual = actuals.get(f.id);
|
|
12198
|
-
const delta = actual.tokens - f.estimated_tokens;
|
|
12199
|
-
const accuracy = f.estimated_tokens > 0 ? 1 - Math.abs(delta) / f.estimated_tokens : 0;
|
|
12200
|
-
return {
|
|
12201
|
-
featureId: f.id,
|
|
12202
|
-
featureName: f.name,
|
|
12203
|
-
estimatedTokens: f.estimated_tokens,
|
|
12204
|
-
actualTokens: actual.tokens,
|
|
12205
|
-
delta,
|
|
12206
|
-
accuracy,
|
|
12207
|
-
modelRecommended: "sonnet",
|
|
12208
|
-
modelUsed: "sonnet",
|
|
12209
|
-
sessionsCount: actual.sessions,
|
|
12210
|
-
activeMinutes: actual.minutes,
|
|
12211
|
-
tagQuality: { autoTagged: actual.sessions, likelyTagged: 0, manualTagged: 0 },
|
|
12212
|
-
insights: delta > 0 ? [`Over estimate by ${(delta / f.estimated_tokens * 100).toFixed(0)}%`] : [`Under estimate by ${(Math.abs(delta) / f.estimated_tokens * 100).toFixed(0)}%`]
|
|
12213
|
-
};
|
|
12214
|
-
});
|
|
12215
|
-
const totalSessions = Array.from(actuals.values()).reduce((s, a) => s + a.sessions, 0);
|
|
12216
|
-
const autoTagged = summary.sessions.filter(
|
|
12217
|
-
(s) => s.status === "auto-tagged"
|
|
12218
|
-
).length;
|
|
12219
|
-
const autoTagAccuracy = totalSessions > 0 ? autoTagged / totalSessions : 0;
|
|
12220
|
-
const synced = await syncReconciliation({
|
|
12221
|
-
projectName,
|
|
12222
|
-
featureDeltas,
|
|
12223
|
-
overallAccuracy,
|
|
12224
|
-
autoTagAccuracy,
|
|
12225
|
-
suggestedAdjustments: []
|
|
12226
|
-
});
|
|
12227
|
-
logConvexStatus(synced);
|
|
12228
12496
|
});
|
|
12229
12497
|
|
|
12230
12498
|
// src/commands/calibrate.ts
|
|
@@ -12250,8 +12518,20 @@ var calibrateCommand = new Command9("calibrate").description("Show calibration d
|
|
|
12250
12518
|
);
|
|
12251
12519
|
return;
|
|
12252
12520
|
}
|
|
12253
|
-
|
|
12254
|
-
|
|
12521
|
+
let registry;
|
|
12522
|
+
try {
|
|
12523
|
+
registry = JSON.parse(readFileSync13(registryPath, "utf-8"));
|
|
12524
|
+
} catch {
|
|
12525
|
+
console.error(`Failed to parse ${registryPath}: file contains invalid JSON`);
|
|
12526
|
+
return process.exit(1);
|
|
12527
|
+
}
|
|
12528
|
+
let summary;
|
|
12529
|
+
try {
|
|
12530
|
+
summary = JSON.parse(readFileSync13(summaryPath, "utf-8"));
|
|
12531
|
+
} catch {
|
|
12532
|
+
console.error(`Failed to parse ${summaryPath}: file contains invalid JSON`);
|
|
12533
|
+
return process.exit(1);
|
|
12534
|
+
}
|
|
12255
12535
|
const projectName = options.project ?? registry.project ?? getProjectName();
|
|
12256
12536
|
const actuals = /* @__PURE__ */ new Map();
|
|
12257
12537
|
for (const session of summary.sessions) {
|
|
@@ -12420,6 +12700,15 @@ var estimateCommand = new Command10("estimate").description("Quick single-featur
|
|
|
12420
12700
|
console.log(
|
|
12421
12701
|
chalk11.bold(` Estimated cost: $${estimate.estimatedCost.toFixed(2)}`)
|
|
12422
12702
|
);
|
|
12703
|
+
if (estimate.confidence) {
|
|
12704
|
+
const c = estimate.confidence;
|
|
12705
|
+
console.log(
|
|
12706
|
+
chalk11.dim(` Confidence: $${c.low.cost.toFixed(2)} \u2013 $${c.expected.cost.toFixed(2)} \u2013 $${c.high.cost.toFixed(2)} (low / expected / high)`)
|
|
12707
|
+
);
|
|
12708
|
+
console.log(
|
|
12709
|
+
chalk11.dim(` ${c.low.tokens.toLocaleString()} \u2013 ${c.expected.tokens.toLocaleString()} \u2013 ${c.high.tokens.toLocaleString()} tokens`)
|
|
12710
|
+
);
|
|
12711
|
+
}
|
|
12423
12712
|
console.log(chalk11.dim("\n Overhead breakdown:"));
|
|
12424
12713
|
console.log(
|
|
12425
12714
|
` Context resend: ${estimate.overheadBreakdown.contextResend.toLocaleString()} tokens`
|
|
@@ -12846,8 +13135,20 @@ var velocityCommand = new Command12("velocity").description("Show velocity metri
|
|
|
12846
13135
|
);
|
|
12847
13136
|
return;
|
|
12848
13137
|
}
|
|
12849
|
-
|
|
12850
|
-
|
|
13138
|
+
let registry;
|
|
13139
|
+
try {
|
|
13140
|
+
registry = JSON.parse(readFileSync15(registryPath, "utf-8"));
|
|
13141
|
+
} catch {
|
|
13142
|
+
console.error(`Failed to parse ${registryPath}: file contains invalid JSON`);
|
|
13143
|
+
return process.exit(1);
|
|
13144
|
+
}
|
|
13145
|
+
let summary;
|
|
13146
|
+
try {
|
|
13147
|
+
summary = JSON.parse(readFileSync15(summaryPath, "utf-8"));
|
|
13148
|
+
} catch {
|
|
13149
|
+
console.error(`Failed to parse ${summaryPath}: file contains invalid JSON`);
|
|
13150
|
+
return process.exit(1);
|
|
13151
|
+
}
|
|
12851
13152
|
const projectName = options.project ?? registry.project ?? getProjectName();
|
|
12852
13153
|
const data = loadData();
|
|
12853
13154
|
const featureDataMap = /* @__PURE__ */ new Map();
|
|
@@ -12985,8 +13286,8 @@ Fathom \u2014 Velocity: ${projectName}`));
|
|
|
12985
13286
|
// src/commands/init.ts
|
|
12986
13287
|
import { Command as Command13 } from "commander";
|
|
12987
13288
|
import {
|
|
12988
|
-
writeFileSync as
|
|
12989
|
-
mkdirSync as
|
|
13289
|
+
writeFileSync as writeFileSync9,
|
|
13290
|
+
mkdirSync as mkdirSync10,
|
|
12990
13291
|
existsSync as existsSync15,
|
|
12991
13292
|
readFileSync as readFileSync16,
|
|
12992
13293
|
readdirSync as readdirSync3
|
|
@@ -12995,7 +13296,7 @@ import { resolve as resolve15, dirname as dirname6 } from "path";
|
|
|
12995
13296
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12996
13297
|
import { execSync } from "child_process";
|
|
12997
13298
|
import chalk14 from "chalk";
|
|
12998
|
-
import { confirm as
|
|
13299
|
+
import { confirm as confirm5 } from "@inquirer/prompts";
|
|
12999
13300
|
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
13301
|
const projectDir = options.projectDir ? resolve15(options.projectDir) : process.cwd();
|
|
13001
13302
|
const projectName = options.project ?? detectProjectName(projectDir);
|
|
@@ -13007,7 +13308,7 @@ Fathom \u2014 Init: ${projectName}`));
|
|
|
13007
13308
|
".config",
|
|
13008
13309
|
"fathom"
|
|
13009
13310
|
);
|
|
13010
|
-
|
|
13311
|
+
mkdirSync10(configDir, { recursive: true });
|
|
13011
13312
|
const configPath = resolve15(configDir, "config.json");
|
|
13012
13313
|
let existing = {};
|
|
13013
13314
|
if (existsSync15(configPath)) {
|
|
@@ -13023,7 +13324,7 @@ Fathom \u2014 Init: ${projectName}`));
|
|
|
13023
13324
|
...options.adminKey && { anthropicAdminKey: options.adminKey },
|
|
13024
13325
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13025
13326
|
};
|
|
13026
|
-
|
|
13327
|
+
writeFileSync9(configPath, JSON.stringify(config, null, 2));
|
|
13027
13328
|
console.log(chalk14.green(` \u2713 Global config: ${configPath}`));
|
|
13028
13329
|
if (options.convexUrl) {
|
|
13029
13330
|
console.log(chalk14.dim(` Convex URL: ${options.convexUrl}`));
|
|
@@ -13042,7 +13343,7 @@ Fathom \u2014 Init: ${projectName}`));
|
|
|
13042
13343
|
if (claudeDetected) {
|
|
13043
13344
|
console.log("");
|
|
13044
13345
|
console.log(chalk14.cyan(" Claude Code detected!"));
|
|
13045
|
-
const installPlugin = await
|
|
13346
|
+
const installPlugin = await confirm5({
|
|
13046
13347
|
message: "Install Fathom plugin for Claude Code? (adds skills + MCP config)",
|
|
13047
13348
|
default: true
|
|
13048
13349
|
});
|
|
@@ -13065,8 +13366,8 @@ Fathom \u2014 Init: ${projectName}`));
|
|
|
13065
13366
|
"fathom",
|
|
13066
13367
|
skillDir
|
|
13067
13368
|
);
|
|
13068
|
-
|
|
13069
|
-
|
|
13369
|
+
mkdirSync10(destDir, { recursive: true });
|
|
13370
|
+
writeFileSync9(
|
|
13070
13371
|
resolve15(destDir, "SKILL.md"),
|
|
13071
13372
|
readFileSync16(src, "utf-8")
|
|
13072
13373
|
);
|
|
@@ -13080,8 +13381,8 @@ Fathom \u2014 Init: ${projectName}`));
|
|
|
13080
13381
|
);
|
|
13081
13382
|
if (existsSync15(pluginJsonSrc)) {
|
|
13082
13383
|
const destDir = resolve15(projectDir, ".claude-plugin");
|
|
13083
|
-
|
|
13084
|
-
|
|
13384
|
+
mkdirSync10(destDir, { recursive: true });
|
|
13385
|
+
writeFileSync9(
|
|
13085
13386
|
resolve15(destDir, "plugin.json"),
|
|
13086
13387
|
readFileSync16(pluginJsonSrc, "utf-8")
|
|
13087
13388
|
);
|
|
@@ -13097,63 +13398,96 @@ Fathom \u2014 Init: ${projectName}`));
|
|
|
13097
13398
|
|
|
13098
13399
|
// src/commands/rename.ts
|
|
13099
13400
|
import { Command as Command14 } from "commander";
|
|
13100
|
-
import { readFileSync as readFileSync17, writeFileSync as
|
|
13401
|
+
import { readFileSync as readFileSync17, writeFileSync as writeFileSync10, existsSync as existsSync16 } from "fs";
|
|
13101
13402
|
import { resolve as resolve16 } from "path";
|
|
13102
13403
|
import chalk15 from "chalk";
|
|
13103
|
-
|
|
13104
|
-
|
|
13105
|
-
|
|
13106
|
-
|
|
13107
|
-
|
|
13108
|
-
|
|
13109
|
-
|
|
13110
|
-
|
|
13111
|
-
|
|
13112
|
-
|
|
13113
|
-
resolve16(teDir, "config.json"),
|
|
13114
|
-
resolve16(teDir, "registry.json"),
|
|
13115
|
-
resolve16(teDir, "tracking", "summary.json")
|
|
13116
|
-
];
|
|
13117
|
-
let oldName = "unnamed";
|
|
13118
|
-
let updated = 0;
|
|
13119
|
-
for (const filePath of jsonFiles) {
|
|
13120
|
-
if (!existsSync16(filePath)) continue;
|
|
13121
|
-
const data = JSON.parse(readFileSync17(filePath, "utf-8"));
|
|
13122
|
-
if (data.project) {
|
|
13123
|
-
oldName = data.project;
|
|
13124
|
-
data.project = name;
|
|
13125
|
-
writeFileSync9(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
13126
|
-
updated++;
|
|
13127
|
-
}
|
|
13128
|
-
}
|
|
13129
|
-
const skillPath = resolve16(baseDir, ".claude", "skills", "fathom", "SKILL.md");
|
|
13130
|
-
if (existsSync16(skillPath)) {
|
|
13131
|
-
const content3 = readFileSync17(skillPath, "utf-8");
|
|
13132
|
-
const newContent = content3.replaceAll(oldName, name);
|
|
13133
|
-
if (newContent !== content3) {
|
|
13134
|
-
writeFileSync9(skillPath, newContent);
|
|
13135
|
-
updated++;
|
|
13136
|
-
}
|
|
13137
|
-
}
|
|
13138
|
-
const hookPath = resolve16(baseDir, ".claude", "hooks", "te-session-sync.sh");
|
|
13139
|
-
if (existsSync16(hookPath)) {
|
|
13140
|
-
const content3 = readFileSync17(hookPath, "utf-8");
|
|
13141
|
-
const newContent = content3.replaceAll(oldName, name);
|
|
13142
|
-
if (newContent !== content3) {
|
|
13143
|
-
writeFileSync9(hookPath, newContent);
|
|
13144
|
-
updated++;
|
|
13404
|
+
import { confirm as confirm6 } from "@inquirer/prompts";
|
|
13405
|
+
var renameCommand = new Command14("rename").description("Rename the project in all .claude/te/ config files").argument("<name>", "New project name").option("-y, --yes", "Skip confirmation prompt").action(async (name, options) => {
|
|
13406
|
+
try {
|
|
13407
|
+
const baseDir = process.cwd();
|
|
13408
|
+
const teDir = resolve16(baseDir, ".claude", "te");
|
|
13409
|
+
if (!existsSync16(teDir)) {
|
|
13410
|
+
console.error(
|
|
13411
|
+
chalk15.red("\n Error: .claude/te/ not found. Run `fathom setup` first.\n")
|
|
13412
|
+
);
|
|
13413
|
+
process.exit(1);
|
|
13145
13414
|
}
|
|
13146
|
-
|
|
13147
|
-
|
|
13148
|
-
|
|
13415
|
+
const jsonFiles = [
|
|
13416
|
+
resolve16(teDir, "config.json"),
|
|
13417
|
+
resolve16(teDir, "registry.json"),
|
|
13418
|
+
resolve16(teDir, "tracking", "summary.json")
|
|
13419
|
+
];
|
|
13420
|
+
let oldName = "unnamed";
|
|
13421
|
+
const configPath = resolve16(teDir, "config.json");
|
|
13422
|
+
if (existsSync16(configPath)) {
|
|
13423
|
+
try {
|
|
13424
|
+
const configData = JSON.parse(readFileSync17(configPath, "utf-8"));
|
|
13425
|
+
if (configData.project) oldName = configData.project;
|
|
13426
|
+
} catch {
|
|
13427
|
+
}
|
|
13428
|
+
}
|
|
13429
|
+
if (!options.yes) {
|
|
13430
|
+
const proceed = await confirm6({
|
|
13431
|
+
message: `Rename project "${oldName}" to "${name}"?`,
|
|
13432
|
+
default: true
|
|
13433
|
+
});
|
|
13434
|
+
if (!proceed) {
|
|
13435
|
+
console.log(chalk15.dim("\n Aborted."));
|
|
13436
|
+
return;
|
|
13437
|
+
}
|
|
13438
|
+
}
|
|
13439
|
+
let updated = 0;
|
|
13440
|
+
for (const filePath of jsonFiles) {
|
|
13441
|
+
if (!existsSync16(filePath)) continue;
|
|
13442
|
+
let data;
|
|
13443
|
+
try {
|
|
13444
|
+
data = JSON.parse(readFileSync17(filePath, "utf-8"));
|
|
13445
|
+
} catch {
|
|
13446
|
+
console.error(`Failed to parse ${filePath}: file contains invalid JSON`);
|
|
13447
|
+
return process.exit(1);
|
|
13448
|
+
}
|
|
13449
|
+
if (data.project) {
|
|
13450
|
+
oldName = data.project;
|
|
13451
|
+
data.project = name;
|
|
13452
|
+
writeFileSync10(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
13453
|
+
updated++;
|
|
13454
|
+
}
|
|
13455
|
+
}
|
|
13456
|
+
const skillPath = resolve16(baseDir, ".claude", "skills", "fathom", "SKILL.md");
|
|
13457
|
+
if (existsSync16(skillPath)) {
|
|
13458
|
+
const content3 = readFileSync17(skillPath, "utf-8");
|
|
13459
|
+
const newContent = content3.replaceAll(oldName, name);
|
|
13460
|
+
if (newContent !== content3) {
|
|
13461
|
+
writeFileSync10(skillPath, newContent);
|
|
13462
|
+
updated++;
|
|
13463
|
+
}
|
|
13464
|
+
}
|
|
13465
|
+
const hookPath = resolve16(baseDir, ".claude", "hooks", "te-session-sync.sh");
|
|
13466
|
+
if (existsSync16(hookPath)) {
|
|
13467
|
+
const content3 = readFileSync17(hookPath, "utf-8");
|
|
13468
|
+
const newContent = content3.replaceAll(oldName, name);
|
|
13469
|
+
if (newContent !== content3) {
|
|
13470
|
+
writeFileSync10(hookPath, newContent);
|
|
13471
|
+
updated++;
|
|
13472
|
+
}
|
|
13473
|
+
}
|
|
13474
|
+
console.log(
|
|
13475
|
+
chalk15.bold(`
|
|
13149
13476
|
Renamed "${oldName}" \u2192 "${name}" (${updated} files updated)
|
|
13150
13477
|
`)
|
|
13151
|
-
|
|
13478
|
+
);
|
|
13479
|
+
} catch (err) {
|
|
13480
|
+
if (err && typeof err === "object" && "name" in err && err.name === "ExitPromptError") {
|
|
13481
|
+
console.log(chalk15.dim("\n Exiting.\n"));
|
|
13482
|
+
return;
|
|
13483
|
+
}
|
|
13484
|
+
throw err;
|
|
13485
|
+
}
|
|
13152
13486
|
});
|
|
13153
13487
|
|
|
13154
13488
|
// src/commands/research.ts
|
|
13155
13489
|
import { Command as Command15 } from "commander";
|
|
13156
|
-
import { writeFileSync as
|
|
13490
|
+
import { writeFileSync as writeFileSync11, existsSync as existsSync17 } from "fs";
|
|
13157
13491
|
import { resolve as resolve17 } from "path";
|
|
13158
13492
|
import chalk16 from "chalk";
|
|
13159
13493
|
|
|
@@ -13235,6 +13569,10 @@ function loadLocalPricing(fetchedAt, error) {
|
|
|
13235
13569
|
}
|
|
13236
13570
|
|
|
13237
13571
|
// src/fetchers/intelligence-fetcher.ts
|
|
13572
|
+
function maskKey(key) {
|
|
13573
|
+
if (key.length <= 6) return "***";
|
|
13574
|
+
return key.slice(0, 3) + "***" + key.slice(-3);
|
|
13575
|
+
}
|
|
13238
13576
|
async function getAnthropicClient3(apiKey) {
|
|
13239
13577
|
const { default: AnthropicSDK } = await import("@anthropic-ai/sdk");
|
|
13240
13578
|
return new AnthropicSDK({ apiKey: apiKey ?? process.env.ANTHROPIC_API_KEY });
|
|
@@ -13307,7 +13645,10 @@ ${recsDesc}`;
|
|
|
13307
13645
|
fetchedAt: now
|
|
13308
13646
|
};
|
|
13309
13647
|
} catch (err) {
|
|
13310
|
-
|
|
13648
|
+
let errorMsg = err instanceof Error ? err.message : String(err);
|
|
13649
|
+
if (apiKey) {
|
|
13650
|
+
errorMsg = errorMsg.replaceAll(apiKey, maskKey(apiKey));
|
|
13651
|
+
}
|
|
13311
13652
|
if (options.verbose) {
|
|
13312
13653
|
console.log(` Intelligence fetch failed: ${errorMsg}`);
|
|
13313
13654
|
}
|
|
@@ -13470,13 +13811,16 @@ var researchCommand = new Command15("research").description("Check and update mo
|
|
|
13470
13811
|
console.log(chalk16.yellow(" Warning: could not load local data files"));
|
|
13471
13812
|
localData = { models: [], recommendations: {} };
|
|
13472
13813
|
}
|
|
13473
|
-
const [pricingResult, intelligenceResult] = await
|
|
13474
|
-
|
|
13475
|
-
|
|
13476
|
-
offline: options.offline,
|
|
13477
|
-
|
|
13478
|
-
|
|
13479
|
-
|
|
13814
|
+
const [pricingResult, intelligenceResult] = await withSpinner(
|
|
13815
|
+
"Researching pricing intelligence...",
|
|
13816
|
+
async () => Promise.all([
|
|
13817
|
+
fetchPricing({ offline: options.offline, verbose: options.verbose }),
|
|
13818
|
+
fetchIntelligence(localData.models, localData.recommendations, {
|
|
13819
|
+
offline: options.offline,
|
|
13820
|
+
verbose: options.verbose
|
|
13821
|
+
})
|
|
13822
|
+
])
|
|
13823
|
+
);
|
|
13480
13824
|
if (options.verbose) {
|
|
13481
13825
|
const elapsed = Date.now() - startTime;
|
|
13482
13826
|
console.log(chalk16.dim(`
|
|
@@ -13509,10 +13853,10 @@ var researchCommand = new Command15("research").description("Check and update mo
|
|
|
13509
13853
|
const dataDir = findDataDir2();
|
|
13510
13854
|
const modelsPath = resolve17(dataDir, "models.yaml");
|
|
13511
13855
|
const agentsPath = resolve17(dataDir, "agents.yaml");
|
|
13512
|
-
|
|
13856
|
+
writeFileSync11(modelsPath, generateModelsYaml(mergeResult.models.merged));
|
|
13513
13857
|
console.log(chalk16.green(`
|
|
13514
13858
|
Updated: ${modelsPath}`));
|
|
13515
|
-
|
|
13859
|
+
writeFileSync11(agentsPath, generateAgentsYaml(mergeResult.recommendations.merged));
|
|
13516
13860
|
console.log(chalk16.green(` Updated: ${agentsPath}`));
|
|
13517
13861
|
}
|
|
13518
13862
|
if (options.sync) {
|
|
@@ -13636,10 +13980,10 @@ var projectCommand = new Command16("project").description("View or update projec
|
|
|
13636
13980
|
console.log(chalk17.dim(" Run `fathom` to set up a project with intent capture.\n"));
|
|
13637
13981
|
return;
|
|
13638
13982
|
}
|
|
13639
|
-
const { loadIntent } = await import("./dist-
|
|
13983
|
+
const { loadIntent } = await import("./dist-VMLJKWUO.js");
|
|
13640
13984
|
const intent = await loadIntent(cwd);
|
|
13641
13985
|
if (options.regenerate) {
|
|
13642
|
-
const { writeProjections } = await import("./dist-
|
|
13986
|
+
const { writeProjections } = await import("./dist-HPXMBCZX.js");
|
|
13643
13987
|
const files = await writeProjections(cwd, intent);
|
|
13644
13988
|
console.log(chalk17.green(`
|
|
13645
13989
|
\u2713 Regenerated ${files.length} projection files from intent`));
|
|
@@ -13673,7 +14017,7 @@ var projectCommand = new Command16("project").description("View or update projec
|
|
|
13673
14017
|
console.log(` Alert at ${Math.round(intent.budget.alert_threshold * 100)}%`);
|
|
13674
14018
|
}
|
|
13675
14019
|
if (intent.guardrails) {
|
|
13676
|
-
const { resolveGuardrails } = await import("./dist-
|
|
14020
|
+
const { resolveGuardrails } = await import("./dist-VMLJKWUO.js");
|
|
13677
14021
|
const resolved = resolveGuardrails(intent);
|
|
13678
14022
|
const total = resolved.security.length + resolved.quality.length + resolved.process.length;
|
|
13679
14023
|
console.log(chalk17.bold(`
|
|
@@ -13713,16 +14057,16 @@ var projectCommand = new Command16("project").description("View or update projec
|
|
|
13713
14057
|
|
|
13714
14058
|
// src/commands/report.ts
|
|
13715
14059
|
import { Command as Command17 } from "commander";
|
|
13716
|
-
import { writeFileSync as
|
|
14060
|
+
import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync11, existsSync as existsSync19 } from "fs";
|
|
13717
14061
|
import { resolve as resolve19, join as join4 } from "path";
|
|
13718
14062
|
import chalk18 from "chalk";
|
|
13719
14063
|
|
|
13720
14064
|
// ../store/dist/index.js
|
|
13721
14065
|
import { randomUUID } from "crypto";
|
|
13722
|
-
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
14066
|
+
import { mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
|
|
13723
14067
|
import { dirname as dirname7, join as join3 } from "path";
|
|
13724
14068
|
import { readFile as readFile2 } from "fs/promises";
|
|
13725
|
-
import { join as join22 } from "path";
|
|
14069
|
+
import { isAbsolute, join as join22, normalize } from "path";
|
|
13726
14070
|
var OverheadBreakdownSchema = external_exports.object({
|
|
13727
14071
|
contextResend: external_exports.number(),
|
|
13728
14072
|
toolCalls: external_exports.number(),
|
|
@@ -13926,9 +14270,15 @@ async function readJsonFile(path) {
|
|
|
13926
14270
|
}
|
|
13927
14271
|
async function writeJsonFile(path, data) {
|
|
13928
14272
|
await ensureDir(path);
|
|
13929
|
-
const tmp = `${path}.tmp`;
|
|
14273
|
+
const tmp = `${path}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
13930
14274
|
await writeFile(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
13931
|
-
|
|
14275
|
+
try {
|
|
14276
|
+
await rename(tmp, path);
|
|
14277
|
+
} catch (err) {
|
|
14278
|
+
await unlink(tmp).catch(() => {
|
|
14279
|
+
});
|
|
14280
|
+
throw err;
|
|
14281
|
+
}
|
|
13932
14282
|
}
|
|
13933
14283
|
var FileStore = class {
|
|
13934
14284
|
baseDir;
|
|
@@ -14147,7 +14497,8 @@ var ConvexStore = class {
|
|
|
14147
14497
|
this.client = new convex.ConvexHttpClient(this.url);
|
|
14148
14498
|
this.api = convex.anyApi;
|
|
14149
14499
|
return true;
|
|
14150
|
-
} catch {
|
|
14500
|
+
} catch (err) {
|
|
14501
|
+
console.error(`[FathomStore] Convex client init failed:`, err);
|
|
14151
14502
|
return false;
|
|
14152
14503
|
}
|
|
14153
14504
|
})();
|
|
@@ -14162,7 +14513,8 @@ var ConvexStore = class {
|
|
|
14162
14513
|
if (!await this.init()) return null;
|
|
14163
14514
|
try {
|
|
14164
14515
|
return await this.client.query(fn, args);
|
|
14165
|
-
} catch {
|
|
14516
|
+
} catch (err) {
|
|
14517
|
+
console.error(`[FathomStore] query failed:`, err);
|
|
14166
14518
|
return null;
|
|
14167
14519
|
}
|
|
14168
14520
|
}
|
|
@@ -14174,7 +14526,8 @@ var ConvexStore = class {
|
|
|
14174
14526
|
if (!await this.init()) return null;
|
|
14175
14527
|
try {
|
|
14176
14528
|
return await this.client.mutation(fn, args);
|
|
14177
|
-
} catch {
|
|
14529
|
+
} catch (err) {
|
|
14530
|
+
console.error(`[FathomStore] mutation failed:`, err);
|
|
14178
14531
|
return null;
|
|
14179
14532
|
}
|
|
14180
14533
|
}
|
|
@@ -14409,6 +14762,15 @@ var ConvexStore = class {
|
|
|
14409
14762
|
);
|
|
14410
14763
|
}
|
|
14411
14764
|
};
|
|
14765
|
+
function validateStorePath(storePath) {
|
|
14766
|
+
if (isAbsolute(storePath)) {
|
|
14767
|
+
throw new Error(`Store path must be relative, got absolute path: "${storePath}"`);
|
|
14768
|
+
}
|
|
14769
|
+
const normalized = normalize(storePath);
|
|
14770
|
+
if (normalized.startsWith("..")) {
|
|
14771
|
+
throw new Error(`Store path must not escape project directory: "${storePath}"`);
|
|
14772
|
+
}
|
|
14773
|
+
}
|
|
14412
14774
|
async function loadStoreConfig(dir) {
|
|
14413
14775
|
try {
|
|
14414
14776
|
const raw = await readFile2(join22(dir, ".fathom", "config.json"), "utf-8");
|
|
@@ -14416,6 +14778,7 @@ async function loadStoreConfig(dir) {
|
|
|
14416
14778
|
if (config.store && typeof config.store === "object") {
|
|
14417
14779
|
const store = config.store;
|
|
14418
14780
|
if (store.type === "file" && typeof store.path === "string") {
|
|
14781
|
+
validateStorePath(store.path);
|
|
14419
14782
|
return { type: "file", path: store.path };
|
|
14420
14783
|
}
|
|
14421
14784
|
if (store.type === "convex" && typeof store.url === "string") {
|
|
@@ -14433,7 +14796,8 @@ function createStore(config) {
|
|
|
14433
14796
|
}
|
|
14434
14797
|
switch (config.type) {
|
|
14435
14798
|
case "file":
|
|
14436
|
-
|
|
14799
|
+
validateStorePath(config.path);
|
|
14800
|
+
return new FileStore(join22(process.cwd(), config.path));
|
|
14437
14801
|
case "convex":
|
|
14438
14802
|
return new ConvexStore({ url: config.url });
|
|
14439
14803
|
default:
|
|
@@ -15347,9 +15711,9 @@ Fathom \u2014 Project Report: ${projectName}`));
|
|
|
15347
15711
|
const outputPath = options.output ? resolve19(options.output) : defaultPath;
|
|
15348
15712
|
const dir = resolve19(outputPath, "..");
|
|
15349
15713
|
if (!existsSync19(dir)) {
|
|
15350
|
-
|
|
15714
|
+
mkdirSync11(dir, { recursive: true });
|
|
15351
15715
|
}
|
|
15352
|
-
|
|
15716
|
+
writeFileSync12(outputPath, report, "utf-8");
|
|
15353
15717
|
console.log(
|
|
15354
15718
|
chalk18.green(`
|
|
15355
15719
|
\u2713 Report generated: ${outputPath}`)
|
|
@@ -15365,17 +15729,29 @@ Fathom \u2014 Project Report: ${projectName}`));
|
|
|
15365
15729
|
|
|
15366
15730
|
// src/commands/contribute.ts
|
|
15367
15731
|
import { Command as Command18 } from "commander";
|
|
15368
|
-
import { readFileSync as readFileSync18, writeFileSync as
|
|
15732
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, mkdirSync as mkdirSync12 } from "fs";
|
|
15369
15733
|
import { resolve as resolve20 } from "path";
|
|
15370
15734
|
import chalk19 from "chalk";
|
|
15371
|
-
import { confirm as
|
|
15735
|
+
import { confirm as confirm7 } from "@inquirer/prompts";
|
|
15372
15736
|
function loadCalibratedProfiles() {
|
|
15373
15737
|
const profiles = [];
|
|
15374
15738
|
const registryPath = resolve20(process.cwd(), ".claude", "te", "registry.json");
|
|
15375
15739
|
const summaryPath = resolve20(process.cwd(), ".claude", "te", "tracking", "summary.json");
|
|
15376
15740
|
if (existsSync20(registryPath) && existsSync20(summaryPath)) {
|
|
15377
|
-
|
|
15378
|
-
|
|
15741
|
+
let registry;
|
|
15742
|
+
try {
|
|
15743
|
+
registry = JSON.parse(readFileSync18(registryPath, "utf-8"));
|
|
15744
|
+
} catch {
|
|
15745
|
+
console.error(`Failed to parse ${registryPath}: file contains invalid JSON`);
|
|
15746
|
+
return process.exit(1);
|
|
15747
|
+
}
|
|
15748
|
+
let summary;
|
|
15749
|
+
try {
|
|
15750
|
+
summary = JSON.parse(readFileSync18(summaryPath, "utf-8"));
|
|
15751
|
+
} catch {
|
|
15752
|
+
console.error(`Failed to parse ${summaryPath}: file contains invalid JSON`);
|
|
15753
|
+
return process.exit(1);
|
|
15754
|
+
}
|
|
15379
15755
|
const actuals = /* @__PURE__ */ new Map();
|
|
15380
15756
|
for (const session of summary.sessions ?? []) {
|
|
15381
15757
|
if (!session.featureId || session.status === "untagged") continue;
|
|
@@ -15452,7 +15828,7 @@ var contributeCommand = new Command18("contribute").description("Share anonymize
|
|
|
15452
15828
|
}
|
|
15453
15829
|
if (!options.yes) {
|
|
15454
15830
|
try {
|
|
15455
|
-
const confirmed = await
|
|
15831
|
+
const confirmed = await confirm7({
|
|
15456
15832
|
message: "Share this data?",
|
|
15457
15833
|
default: false
|
|
15458
15834
|
});
|
|
@@ -15466,11 +15842,11 @@ var contributeCommand = new Command18("contribute").description("Share anonymize
|
|
|
15466
15842
|
}
|
|
15467
15843
|
}
|
|
15468
15844
|
const contributionsDir = resolve20(process.cwd(), ".fathom", "contributions");
|
|
15469
|
-
|
|
15845
|
+
mkdirSync12(contributionsDir, { recursive: true });
|
|
15470
15846
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
15471
15847
|
const outputPath = resolve20(contributionsDir, `${today}.json`);
|
|
15472
15848
|
const json = exportContribution(dataPoints);
|
|
15473
|
-
|
|
15849
|
+
writeFileSync13(outputPath, json, "utf-8");
|
|
15474
15850
|
console.log(chalk19.green(`
|
|
15475
15851
|
\u2713 Contribution saved to .fathom/contributions/${today}.json`));
|
|
15476
15852
|
console.log(chalk19.green(" \u2713 Thank you! This helps make Fathom better for everyone."));
|
|
@@ -15479,7 +15855,7 @@ var contributeCommand = new Command18("contribute").description("Share anonymize
|
|
|
15479
15855
|
|
|
15480
15856
|
// src/commands/feedback.ts
|
|
15481
15857
|
import { Command as Command19 } from "commander";
|
|
15482
|
-
import { writeFileSync as
|
|
15858
|
+
import { writeFileSync as writeFileSync14, mkdirSync as mkdirSync13, readFileSync as readFileSync19, existsSync as existsSync21 } from "fs";
|
|
15483
15859
|
import { resolve as resolve21, join as join5 } from "path";
|
|
15484
15860
|
import chalk20 from "chalk";
|
|
15485
15861
|
function getIntentSummary() {
|
|
@@ -15507,7 +15883,7 @@ function getSystemInfo() {
|
|
|
15507
15883
|
}
|
|
15508
15884
|
function saveFeedback(type, description) {
|
|
15509
15885
|
const feedbackDir = resolve21(process.cwd(), ".fathom", "feedback");
|
|
15510
|
-
|
|
15886
|
+
mkdirSync13(feedbackDir, { recursive: true });
|
|
15511
15887
|
const sys = getSystemInfo();
|
|
15512
15888
|
const intent = getIntentSummary();
|
|
15513
15889
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
@@ -15544,7 +15920,7 @@ function saveFeedback(type, description) {
|
|
|
15544
15920
|
"Review this file, add any additional details, then email to feedback@fathom.dev"
|
|
15545
15921
|
);
|
|
15546
15922
|
lines.push("");
|
|
15547
|
-
|
|
15923
|
+
writeFileSync14(filepath, lines.join("\n"));
|
|
15548
15924
|
return filepath;
|
|
15549
15925
|
}
|
|
15550
15926
|
var feedbackCommand = new Command19("feedback").description("How to report bugs and request features").option("--bug <description>", "Save a bug report to .fathom/feedback/").option(
|
|
@@ -15605,9 +15981,10 @@ here's how to reach us:
|
|
|
15605
15981
|
// src/commands/sync.ts
|
|
15606
15982
|
import { Command as Command20 } from "commander";
|
|
15607
15983
|
import chalk21 from "chalk";
|
|
15984
|
+
import { confirm as confirm8 } from "@inquirer/prompts";
|
|
15608
15985
|
|
|
15609
15986
|
// ../sync/dist/index.js
|
|
15610
|
-
import { readFileSync as readFileSync20, writeFileSync as
|
|
15987
|
+
import { readFileSync as readFileSync20, writeFileSync as writeFileSync15, mkdirSync as mkdirSync14, existsSync as existsSync23 } from "fs";
|
|
15611
15988
|
import { resolve as resolve22, dirname as dirname8 } from "path";
|
|
15612
15989
|
var SyncStatusSchema = external_exports.enum([
|
|
15613
15990
|
"backlog",
|
|
@@ -16300,8 +16677,8 @@ function loadLedger(ledgerPath) {
|
|
|
16300
16677
|
return { provider: "", projectKey: "", items: [], lastSync: 0 };
|
|
16301
16678
|
}
|
|
16302
16679
|
function saveLedger(ledgerPath, ledger) {
|
|
16303
|
-
|
|
16304
|
-
|
|
16680
|
+
mkdirSync14(dirname8(ledgerPath), { recursive: true });
|
|
16681
|
+
writeFileSync15(ledgerPath, JSON.stringify(ledger, null, 2) + "\n");
|
|
16305
16682
|
}
|
|
16306
16683
|
function createSyncManager(config, options) {
|
|
16307
16684
|
const apiKey = resolveApiKey(config);
|
|
@@ -16412,7 +16789,7 @@ function formatStatus2(status) {
|
|
|
16412
16789
|
};
|
|
16413
16790
|
return (colors[status] ?? chalk21.white)(status);
|
|
16414
16791
|
}
|
|
16415
|
-
var syncCommand = new Command20("sync").description("Sync spec slices to external PM tools (Linear, GitHub, Notion, Jira)").requiredOption("--provider <provider>", "PM tool provider (linear, github, notion, jira)").requiredOption("--project-key <key>", "Project key (Linear team key, GitHub owner/repo, Notion database ID, Jira host/project)").option("--pull", "Only pull status updates, don't push new slices").option("--dry-run", "Show what would be synced without making changes").action(async (options) => {
|
|
16792
|
+
var syncCommand = new Command20("sync").description("Sync spec slices to external PM tools (Linear, GitHub, Notion, Jira)").requiredOption("--provider <provider>", "PM tool provider (linear, github, notion, jira)").requiredOption("--project-key <key>", "Project key (Linear team key, GitHub owner/repo, Notion database ID, Jira host/project)").option("--pull", "Only pull status updates, don't push new slices").option("--dry-run", "Show what would be synced without making changes").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
|
|
16416
16793
|
try {
|
|
16417
16794
|
const config = loadSyncConfig(options);
|
|
16418
16795
|
if (!config.projectKey) {
|
|
@@ -16435,10 +16812,23 @@ Fathom \u2014 Sync to ${config.provider.charAt(0).toUpperCase() + config.provide
|
|
|
16435
16812
|
console.log(chalk21.dim(` - ${s.sliceId}: ${s.title}`));
|
|
16436
16813
|
}
|
|
16437
16814
|
} else {
|
|
16815
|
+
if (!options.yes) {
|
|
16816
|
+
const proceed = await confirm8({
|
|
16817
|
+
message: `Sync ${slices.length} slices to ${config.provider}?`,
|
|
16818
|
+
default: true
|
|
16819
|
+
});
|
|
16820
|
+
if (!proceed) {
|
|
16821
|
+
console.log(chalk21.dim("\n Aborted."));
|
|
16822
|
+
return;
|
|
16823
|
+
}
|
|
16824
|
+
}
|
|
16438
16825
|
console.log(chalk21.dim(`
|
|
16439
16826
|
Syncing ${slices.length} pending slice(s) to ${config.provider} project ${config.projectKey}...
|
|
16440
16827
|
`));
|
|
16441
|
-
const results = await
|
|
16828
|
+
const results = await withSpinner(
|
|
16829
|
+
`Syncing slices to ${config.provider}...`,
|
|
16830
|
+
async () => manager.syncSlices(slices)
|
|
16831
|
+
);
|
|
16442
16832
|
if (results.length === 0) {
|
|
16443
16833
|
console.log(chalk21.dim(" All slices already synced."));
|
|
16444
16834
|
} else {
|
|
@@ -16450,7 +16840,10 @@ Fathom \u2014 Sync to ${config.provider.charAt(0).toUpperCase() + config.provide
|
|
|
16450
16840
|
}
|
|
16451
16841
|
if (!options.dryRun) {
|
|
16452
16842
|
console.log(chalk21.dim("\n Pulling status updates..."));
|
|
16453
|
-
const updates = await
|
|
16843
|
+
const updates = await withSpinner(
|
|
16844
|
+
"Pulling status updates...",
|
|
16845
|
+
async () => manager.pullStatus()
|
|
16846
|
+
);
|
|
16454
16847
|
if (updates.length === 0) {
|
|
16455
16848
|
console.log(chalk21.dim(" No status changes."));
|
|
16456
16849
|
} else {
|
|
@@ -16481,9 +16874,9 @@ Fathom \u2014 Sync to ${config.provider.charAt(0).toUpperCase() + config.provide
|
|
|
16481
16874
|
// src/commands/install-plugin.ts
|
|
16482
16875
|
import { Command as Command21 } from "commander";
|
|
16483
16876
|
import {
|
|
16484
|
-
writeFileSync as
|
|
16877
|
+
writeFileSync as writeFileSync16,
|
|
16485
16878
|
readFileSync as readFileSync23,
|
|
16486
|
-
mkdirSync as
|
|
16879
|
+
mkdirSync as mkdirSync15,
|
|
16487
16880
|
existsSync as existsSync25,
|
|
16488
16881
|
readdirSync as readdirSync4
|
|
16489
16882
|
} from "fs";
|
|
@@ -16523,8 +16916,8 @@ var installPluginCommand = new Command21("install-plugin").description(
|
|
|
16523
16916
|
"fathom",
|
|
16524
16917
|
skillDir
|
|
16525
16918
|
);
|
|
16526
|
-
|
|
16527
|
-
|
|
16919
|
+
mkdirSync15(destDir, { recursive: true });
|
|
16920
|
+
writeFileSync16(resolve24(destDir, "SKILL.md"), readFileSync23(src, "utf-8"));
|
|
16528
16921
|
console.log(chalk22.green(` \u2713 .claude/skills/fathom/${skillDir}/SKILL.md`));
|
|
16529
16922
|
}
|
|
16530
16923
|
}
|
|
@@ -16535,8 +16928,8 @@ var installPluginCommand = new Command21("install-plugin").description(
|
|
|
16535
16928
|
);
|
|
16536
16929
|
if (existsSync25(pluginJsonSrc)) {
|
|
16537
16930
|
const destDir = resolve24(projectDir, ".claude-plugin");
|
|
16538
|
-
|
|
16539
|
-
|
|
16931
|
+
mkdirSync15(destDir, { recursive: true });
|
|
16932
|
+
writeFileSync16(
|
|
16540
16933
|
resolve24(destDir, "plugin.json"),
|
|
16541
16934
|
readFileSync23(pluginJsonSrc, "utf-8")
|
|
16542
16935
|
);
|
|
@@ -16556,7 +16949,7 @@ var installPluginCommand = new Command21("install-plugin").description(
|
|
|
16556
16949
|
command: "npx",
|
|
16557
16950
|
args: ["-y", "fathom-token-mcp@latest", "--project-dir", "."]
|
|
16558
16951
|
};
|
|
16559
|
-
|
|
16952
|
+
writeFileSync16(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n");
|
|
16560
16953
|
console.log(chalk22.green(" \u2713 .mcp.json (MCP auto-discovery)"));
|
|
16561
16954
|
console.log(chalk22.bold("\n\u2713 Fathom plugin installed"));
|
|
16562
16955
|
console.log(
|
|
@@ -16564,8 +16957,261 @@ var installPluginCommand = new Command21("install-plugin").description(
|
|
|
16564
16957
|
);
|
|
16565
16958
|
});
|
|
16566
16959
|
|
|
16960
|
+
// src/commands/completions.ts
|
|
16961
|
+
import { Command as Command22 } from "commander";
|
|
16962
|
+
var COMMANDS = [
|
|
16963
|
+
"go",
|
|
16964
|
+
"intake",
|
|
16965
|
+
"analyze",
|
|
16966
|
+
"estimate",
|
|
16967
|
+
"setup",
|
|
16968
|
+
"init",
|
|
16969
|
+
"track",
|
|
16970
|
+
"reconcile",
|
|
16971
|
+
"calibrate",
|
|
16972
|
+
"velocity",
|
|
16973
|
+
"report",
|
|
16974
|
+
"project",
|
|
16975
|
+
"contribute",
|
|
16976
|
+
"feedback",
|
|
16977
|
+
"sync",
|
|
16978
|
+
"install-plugin",
|
|
16979
|
+
"status",
|
|
16980
|
+
"pricing",
|
|
16981
|
+
"validate",
|
|
16982
|
+
"rename",
|
|
16983
|
+
"research",
|
|
16984
|
+
"doctor",
|
|
16985
|
+
"completions"
|
|
16986
|
+
];
|
|
16987
|
+
var GLOBAL_FLAGS = ["--help", "--version"];
|
|
16988
|
+
function generateBashCompletion() {
|
|
16989
|
+
const words = [...COMMANDS, ...GLOBAL_FLAGS].join(" ");
|
|
16990
|
+
return `#!/bin/bash
|
|
16991
|
+
# bash completion for fathom
|
|
16992
|
+
|
|
16993
|
+
_fathom() {
|
|
16994
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
16995
|
+
local commands="${words}"
|
|
16996
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
16997
|
+
return 0
|
|
16998
|
+
}
|
|
16999
|
+
|
|
17000
|
+
complete -F _fathom fathom
|
|
17001
|
+
`;
|
|
17002
|
+
}
|
|
17003
|
+
function generateZshCompletion() {
|
|
17004
|
+
const words = [...COMMANDS, ...GLOBAL_FLAGS].join(" ");
|
|
17005
|
+
return `#compdef fathom
|
|
17006
|
+
# zsh completion for fathom
|
|
17007
|
+
|
|
17008
|
+
_fathom() {
|
|
17009
|
+
local -a commands
|
|
17010
|
+
commands=(${words})
|
|
17011
|
+
compadd -a commands
|
|
17012
|
+
}
|
|
17013
|
+
|
|
17014
|
+
_fathom "$@"
|
|
17015
|
+
`;
|
|
17016
|
+
}
|
|
17017
|
+
var completionsCommand = new Command22("completions").description("Generate shell completion scripts").argument("<shell>", "Shell type: bash or zsh").action((shell) => {
|
|
17018
|
+
switch (shell) {
|
|
17019
|
+
case "bash":
|
|
17020
|
+
process.stdout.write(generateBashCompletion());
|
|
17021
|
+
break;
|
|
17022
|
+
case "zsh":
|
|
17023
|
+
process.stdout.write(generateZshCompletion());
|
|
17024
|
+
break;
|
|
17025
|
+
default:
|
|
17026
|
+
console.error(
|
|
17027
|
+
"Unsupported shell. Use: fathom completions bash|zsh"
|
|
17028
|
+
);
|
|
17029
|
+
process.exitCode = 1;
|
|
17030
|
+
}
|
|
17031
|
+
});
|
|
17032
|
+
|
|
17033
|
+
// src/commands/doctor.ts
|
|
17034
|
+
import { Command as Command23 } from "commander";
|
|
17035
|
+
import { existsSync as existsSync26, readFileSync as readFileSync24 } from "fs";
|
|
17036
|
+
import { resolve as resolve25 } from "path";
|
|
17037
|
+
import chalk23 from "chalk";
|
|
17038
|
+
async function runChecks(cwd) {
|
|
17039
|
+
const results = [];
|
|
17040
|
+
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
17041
|
+
results.push({
|
|
17042
|
+
name: "Node version >= 20",
|
|
17043
|
+
ok: nodeMajor >= 20,
|
|
17044
|
+
message: nodeMajor >= 20 ? `Node.js v${process.versions.node}` : "Upgrade to Node.js 20 or later"
|
|
17045
|
+
});
|
|
17046
|
+
const fathomDir = resolve25(cwd, ".fathom");
|
|
17047
|
+
const hasFathomDir = existsSync26(fathomDir);
|
|
17048
|
+
results.push({
|
|
17049
|
+
name: ".fathom/ directory",
|
|
17050
|
+
ok: hasFathomDir,
|
|
17051
|
+
message: hasFathomDir ? ".fathom/ found" : "Run `fathom init` to create project config"
|
|
17052
|
+
});
|
|
17053
|
+
const configPath = resolve25(fathomDir, "config.json");
|
|
17054
|
+
let configOk = false;
|
|
17055
|
+
let configMsg = "Run `fathom init` to create project config";
|
|
17056
|
+
if (hasFathomDir && existsSync26(configPath)) {
|
|
17057
|
+
try {
|
|
17058
|
+
JSON.parse(readFileSync24(configPath, "utf-8"));
|
|
17059
|
+
configOk = true;
|
|
17060
|
+
configMsg = "config.json valid";
|
|
17061
|
+
} catch {
|
|
17062
|
+
configMsg = "Delete .fathom/config.json and run `fathom init`";
|
|
17063
|
+
}
|
|
17064
|
+
} else if (hasFathomDir) {
|
|
17065
|
+
configMsg = "Run `fathom init` to create config.json";
|
|
17066
|
+
}
|
|
17067
|
+
results.push({
|
|
17068
|
+
name: "config.json valid",
|
|
17069
|
+
ok: configOk,
|
|
17070
|
+
message: configMsg
|
|
17071
|
+
});
|
|
17072
|
+
const intentPath = resolve25(fathomDir, "intent.yaml");
|
|
17073
|
+
let intentOk = false;
|
|
17074
|
+
let intentMsg = "Run `fathom init` to create intent.yaml";
|
|
17075
|
+
if (hasFathomDir && existsSync26(intentPath)) {
|
|
17076
|
+
try {
|
|
17077
|
+
jsYaml.load(readFileSync24(intentPath, "utf-8"), { schema: jsYaml.JSON_SCHEMA });
|
|
17078
|
+
intentOk = true;
|
|
17079
|
+
intentMsg = "intent.yaml parseable";
|
|
17080
|
+
} catch {
|
|
17081
|
+
intentMsg = "Check intent.yaml syntax \u2014 run `fathom validate`";
|
|
17082
|
+
}
|
|
17083
|
+
}
|
|
17084
|
+
results.push({
|
|
17085
|
+
name: "intent.yaml parseable",
|
|
17086
|
+
ok: intentOk,
|
|
17087
|
+
message: intentMsg
|
|
17088
|
+
});
|
|
17089
|
+
const legacyDir = resolve25(cwd, ".claude", "te");
|
|
17090
|
+
const legacyRegistry = resolve25(legacyDir, "registry.json");
|
|
17091
|
+
const legacyTracking = resolve25(legacyDir, "tracking", "summary.json");
|
|
17092
|
+
const hasLegacyRegistry = existsSync26(legacyRegistry);
|
|
17093
|
+
const hasLegacyTracking = existsSync26(legacyTracking);
|
|
17094
|
+
if (existsSync26(legacyDir)) {
|
|
17095
|
+
results.push({
|
|
17096
|
+
name: "Legacy .claude/te/ data",
|
|
17097
|
+
ok: hasLegacyRegistry && hasLegacyTracking,
|
|
17098
|
+
message: hasLegacyRegistry && hasLegacyTracking ? "Legacy registry and tracking data found" : `Missing ${!hasLegacyRegistry ? "registry.json" : ""}${!hasLegacyRegistry && !hasLegacyTracking ? " and " : ""}${!hasLegacyTracking ? "tracking/summary.json" : ""} \u2014 run \`fathom analyze\` and \`fathom track\` to populate`
|
|
17099
|
+
});
|
|
17100
|
+
}
|
|
17101
|
+
let dataOk = false;
|
|
17102
|
+
let dataMsg = "Reinstall fathom \u2014 `npm i -g fathom`";
|
|
17103
|
+
try {
|
|
17104
|
+
loadData();
|
|
17105
|
+
dataOk = true;
|
|
17106
|
+
dataMsg = "Core data files loaded";
|
|
17107
|
+
} catch {
|
|
17108
|
+
}
|
|
17109
|
+
results.push({
|
|
17110
|
+
name: "Data files loadable",
|
|
17111
|
+
ok: dataOk,
|
|
17112
|
+
message: dataMsg
|
|
17113
|
+
});
|
|
17114
|
+
return results;
|
|
17115
|
+
}
|
|
17116
|
+
var doctorCommand = new Command23("doctor").description("Check project health and configuration").action(async () => {
|
|
17117
|
+
console.log(chalk23.bold("\nFathom \u2014 Doctor"));
|
|
17118
|
+
console.log(chalk23.dim("\u2500".repeat(50)));
|
|
17119
|
+
const results = await runChecks(process.cwd());
|
|
17120
|
+
let passed = 0;
|
|
17121
|
+
for (const r of results) {
|
|
17122
|
+
if (r.ok) {
|
|
17123
|
+
console.log(chalk23.green(` \u2713 ${r.name}`));
|
|
17124
|
+
passed++;
|
|
17125
|
+
} else {
|
|
17126
|
+
console.log(chalk23.red(` \u2717 ${r.name} \u2014 ${r.message}`));
|
|
17127
|
+
}
|
|
17128
|
+
}
|
|
17129
|
+
console.log(
|
|
17130
|
+
chalk23.dim(`
|
|
17131
|
+
${passed}/${results.length} checks passed
|
|
17132
|
+
`)
|
|
17133
|
+
);
|
|
17134
|
+
if (passed < results.length) {
|
|
17135
|
+
process.exit(1);
|
|
17136
|
+
}
|
|
17137
|
+
});
|
|
17138
|
+
|
|
17139
|
+
// src/index.ts
|
|
17140
|
+
import chalk24 from "chalk";
|
|
17141
|
+
|
|
17142
|
+
// src/ux/update-checker.ts
|
|
17143
|
+
import { readFileSync as readFileSync25, writeFileSync as writeFileSync17, mkdirSync as mkdirSync16, existsSync as existsSync27 } from "fs";
|
|
17144
|
+
import { join as join6 } from "path";
|
|
17145
|
+
import { homedir as homedir3 } from "os";
|
|
17146
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
17147
|
+
function getCachePath() {
|
|
17148
|
+
return join6(homedir3(), ".fathom", "update-check.json");
|
|
17149
|
+
}
|
|
17150
|
+
function readCache() {
|
|
17151
|
+
try {
|
|
17152
|
+
const cachePath = getCachePath();
|
|
17153
|
+
if (!existsSync27(cachePath)) return null;
|
|
17154
|
+
const raw = readFileSync25(cachePath, "utf-8");
|
|
17155
|
+
return JSON.parse(raw);
|
|
17156
|
+
} catch {
|
|
17157
|
+
return null;
|
|
17158
|
+
}
|
|
17159
|
+
}
|
|
17160
|
+
function writeCache(latest) {
|
|
17161
|
+
try {
|
|
17162
|
+
const cachePath = getCachePath();
|
|
17163
|
+
const dir = join6(homedir3(), ".fathom");
|
|
17164
|
+
if (!existsSync27(dir)) {
|
|
17165
|
+
mkdirSync16(dir, { recursive: true });
|
|
17166
|
+
}
|
|
17167
|
+
writeFileSync17(
|
|
17168
|
+
cachePath,
|
|
17169
|
+
JSON.stringify({ latest, checkedAt: (/* @__PURE__ */ new Date()).toISOString() })
|
|
17170
|
+
);
|
|
17171
|
+
} catch {
|
|
17172
|
+
}
|
|
17173
|
+
}
|
|
17174
|
+
function isCacheFresh(cache) {
|
|
17175
|
+
const checkedAt = new Date(cache.checkedAt).getTime();
|
|
17176
|
+
return Date.now() - checkedAt < CACHE_TTL_MS;
|
|
17177
|
+
}
|
|
17178
|
+
async function checkForUpdate() {
|
|
17179
|
+
try {
|
|
17180
|
+
const current = VERSION;
|
|
17181
|
+
const cache = readCache();
|
|
17182
|
+
if (cache && isCacheFresh(cache)) {
|
|
17183
|
+
return {
|
|
17184
|
+
current,
|
|
17185
|
+
latest: cache.latest,
|
|
17186
|
+
updateAvailable: cache.latest !== current
|
|
17187
|
+
};
|
|
17188
|
+
}
|
|
17189
|
+
const controller = new AbortController();
|
|
17190
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
17191
|
+
try {
|
|
17192
|
+
const response = await fetch(
|
|
17193
|
+
"https://registry.npmjs.org/fathom-cli/latest",
|
|
17194
|
+
{ signal: controller.signal }
|
|
17195
|
+
);
|
|
17196
|
+
if (!response.ok) return null;
|
|
17197
|
+
const data = await response.json();
|
|
17198
|
+
const latest = data.version;
|
|
17199
|
+
writeCache(latest);
|
|
17200
|
+
return {
|
|
17201
|
+
current,
|
|
17202
|
+
latest,
|
|
17203
|
+
updateAvailable: latest !== current
|
|
17204
|
+
};
|
|
17205
|
+
} finally {
|
|
17206
|
+
clearTimeout(timeout);
|
|
17207
|
+
}
|
|
17208
|
+
} catch {
|
|
17209
|
+
return null;
|
|
17210
|
+
}
|
|
17211
|
+
}
|
|
17212
|
+
|
|
16567
17213
|
// src/index.ts
|
|
16568
|
-
var program = new
|
|
17214
|
+
var program = new Command24();
|
|
16569
17215
|
program.name("fathom").description("Workflow intelligence platform for AI-augmented development").version(VERSION);
|
|
16570
17216
|
program.addCommand(goCommand, { isDefault: true });
|
|
16571
17217
|
program.addCommand(intakeCommand);
|
|
@@ -16588,5 +17234,17 @@ program.addCommand(pricingCommand);
|
|
|
16588
17234
|
program.addCommand(validateCommand);
|
|
16589
17235
|
program.addCommand(renameCommand);
|
|
16590
17236
|
program.addCommand(researchCommand);
|
|
17237
|
+
program.addCommand(completionsCommand);
|
|
17238
|
+
program.addCommand(doctorCommand);
|
|
16591
17239
|
program.parse();
|
|
17240
|
+
checkForUpdate().then((result) => {
|
|
17241
|
+
if (result?.updateAvailable) {
|
|
17242
|
+
console.error(
|
|
17243
|
+
chalk24.yellow(`
|
|
17244
|
+
Update available: ${result.current} \u2192 ${result.latest}`)
|
|
17245
|
+
);
|
|
17246
|
+
console.error(chalk24.yellow(`Run: npm i -g fathom-cli`));
|
|
17247
|
+
}
|
|
17248
|
+
}).catch(() => {
|
|
17249
|
+
});
|
|
16592
17250
|
//# sourceMappingURL=index.js.map
|