fapony 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -15
- package/fapony.ts +12 -3
- package/package.json +1 -1
- package/skill/define-convention/SKILL.md +77 -0
- package/skill/lookup-before-edit/SKILL.md +48 -0
- package/skill/review-pony/SKILL.md +11 -3
- package/src/analyze.ts +162 -4
- package/src/conventions-seed.ts +3 -2
- package/src/db/defaults.ts +13 -5
- package/src/db/getters.ts +8 -6
- package/src/db/load.ts +2 -2
- package/src/db/types.ts +1 -3
- package/src/debt/cli.ts +193 -0
- package/src/debt/format.ts +107 -0
- package/src/debt/index.ts +19 -0
- package/src/debt/load.ts +92 -0
- package/src/debt/promotion.ts +152 -0
- package/src/debt/scan.ts +214 -0
- package/src/debt/types.ts +79 -0
- package/src/detect.ts +92 -0
- package/src/digest/collect.ts +3 -2
- package/src/hook.ts +412 -17
- package/src/init-mem.ts +5 -5
- package/src/init.ts +6 -5
- package/src/install/claude.ts +51 -3
- package/src/install/codex.ts +144 -13
- package/src/install/opencode.ts +209 -1
- package/src/install.ts +2 -1
- package/src/lint-baseline.ts +4 -3
- package/src/mcp/evidence.ts +14 -2
- package/src/mcp/tools/index.ts +13 -28
- package/src/mcp/tools/mem.ts +71 -0
- package/src/mcp/transport.ts +12 -63
- package/src/mem/commands/read.ts +18 -1
- package/src/mem/store.ts +6 -4
- package/src/memory.ts +23 -13
- package/src/plan-seed.ts +7 -6
- package/src/session/helpers.ts +1 -1
- package/src/session/registry.ts +3 -6
- package/src/setup.ts +4 -3
- package/src/stats/data.ts +6 -18
- package/src/debt.ts +0 -806
- package/src/mcp/tools/usage.ts +0 -211
package/src/mcp/tools/usage.ts
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
// src/mcp/tools/usage.ts — fapony_usage tool
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
type ImputeSummary,
|
|
5
|
-
imputeResult,
|
|
6
|
-
loadPrices,
|
|
7
|
-
type PriceTable,
|
|
8
|
-
} from "../../price/index.js";
|
|
9
|
-
import {
|
|
10
|
-
CLIENTS,
|
|
11
|
-
mergeBytesByTool,
|
|
12
|
-
type PassiveUsageResult,
|
|
13
|
-
} from "../../session/index.js";
|
|
14
|
-
import { jsonResult, type ToolResult } from "../types.js";
|
|
15
|
-
|
|
16
|
-
/** list-price summary per client — additive, leaves existing lines untouched */
|
|
17
|
-
function imputationOf(
|
|
18
|
-
result: PassiveUsageResult,
|
|
19
|
-
prices: PriceTable | null,
|
|
20
|
-
): (ImputeSummary & { prices_fetched_at: string }) | null {
|
|
21
|
-
if (result.session_count === 0 || !prices) return null;
|
|
22
|
-
return {
|
|
23
|
-
...imputeResult(result, prices),
|
|
24
|
-
prices_fetched_at: prices.fetched_at,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function imputedTextLines(
|
|
29
|
-
label: string,
|
|
30
|
-
result: PassiveUsageResult,
|
|
31
|
-
prices: PriceTable | null,
|
|
32
|
-
): string[] {
|
|
33
|
-
if (result.session_count === 0) return [];
|
|
34
|
-
const imp = imputationOf(result, prices);
|
|
35
|
-
if (!imp)
|
|
36
|
-
return [` ${label}list-price equivalent: — (run \`fapony price-scan\`)`];
|
|
37
|
-
const parts = [
|
|
38
|
-
`~$${imp.total_imputed.toFixed(4)} over ${imp.priced_sessions} priced sessions`,
|
|
39
|
-
];
|
|
40
|
-
if (imp.free_sessions > 0) parts.push(`${imp.free_sessions} free`);
|
|
41
|
-
if (imp.unpriced_sessions > 0)
|
|
42
|
-
parts.push(
|
|
43
|
-
`unpriced: ${imp.unpriced_sessions} sessions / ${imp.unpriced_tokens.toLocaleString()} tokens`,
|
|
44
|
-
);
|
|
45
|
-
return [` ${label}list-price equivalent: ${parts.join(" · ")}`];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** list price per model appended to the model name — only rows where the client records no cost */
|
|
49
|
-
function imputedSuffix(
|
|
50
|
-
provider: string,
|
|
51
|
-
model: string,
|
|
52
|
-
imp: (ImputeSummary & { prices_fetched_at: string }) | null,
|
|
53
|
-
): string {
|
|
54
|
-
if (!imp) return "";
|
|
55
|
-
const m = imp.by_model.find(
|
|
56
|
-
(b) => b.provider === provider && b.model === model,
|
|
57
|
-
);
|
|
58
|
-
if (m && m.status === "priced")
|
|
59
|
-
return ` (~$${m.imputed_cost.toFixed(4)} list-price)`;
|
|
60
|
-
return "";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function toolPassiveUsage(args: Record<string, unknown>): ToolResult {
|
|
64
|
-
const worktree =
|
|
65
|
-
typeof args.worktree === "string" ? args.worktree : undefined;
|
|
66
|
-
const since = typeof args.since === "number" ? args.since : undefined;
|
|
67
|
-
const until = typeof args.until === "number" ? args.until : undefined;
|
|
68
|
-
const detail = args.detail === true;
|
|
69
|
-
|
|
70
|
-
const primary = CLIENTS.find((c) => c.primary)!;
|
|
71
|
-
const others = CLIENTS.filter((c) => !c.primary);
|
|
72
|
-
|
|
73
|
-
const data: PassiveUsageResult = primary.read(worktree, since, until, detail);
|
|
74
|
-
|
|
75
|
-
// Every non-primary client is always fetched when its own log exists.
|
|
76
|
-
const otherResults = others.map((client) => ({
|
|
77
|
-
client,
|
|
78
|
-
result: client.read(worktree, since, until, detail),
|
|
79
|
-
}));
|
|
80
|
-
|
|
81
|
-
// list prices from the cache only — read once per call, not per section
|
|
82
|
-
const prices = loadPrices();
|
|
83
|
-
|
|
84
|
-
if (args.json === true) {
|
|
85
|
-
const json: Record<string, unknown> = {
|
|
86
|
-
...data,
|
|
87
|
-
imputation: imputationOf(data, prices),
|
|
88
|
-
};
|
|
89
|
-
for (const { client, result } of otherResults) {
|
|
90
|
-
json[client.key] =
|
|
91
|
-
result.session_count > 0
|
|
92
|
-
? { ...result, imputation: imputationOf(result, prices) }
|
|
93
|
-
: null;
|
|
94
|
-
}
|
|
95
|
-
return jsonResult(json);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const lines: string[] = [];
|
|
99
|
-
lines.push("Passive Usage Report");
|
|
100
|
-
lines.push("====================");
|
|
101
|
-
lines.push(`Sessions: ${data.session_count}`);
|
|
102
|
-
lines.push(`Total Input Tokens: ${data.total_tokens_input.toLocaleString()}`);
|
|
103
|
-
lines.push(
|
|
104
|
-
`Total Output Tokens: ${data.total_tokens_output.toLocaleString()}`,
|
|
105
|
-
);
|
|
106
|
-
lines.push(
|
|
107
|
-
`Total Reasoning Tokens: ${data.total_tokens_reasoning.toLocaleString()}`,
|
|
108
|
-
);
|
|
109
|
-
lines.push(`Cache Read: ${data.total_tokens_cache_read.toLocaleString()}`);
|
|
110
|
-
lines.push(`Cache Write: ${data.total_tokens_cache_write.toLocaleString()}`);
|
|
111
|
-
lines.push(`Total Cost: $${data.total_cost.toFixed(4)}`);
|
|
112
|
-
lines.push(...imputedTextLines("", data, prices));
|
|
113
|
-
|
|
114
|
-
if (data.by_model.length > 0) {
|
|
115
|
-
lines.push("");
|
|
116
|
-
lines.push("By Model:");
|
|
117
|
-
lines.push("--------");
|
|
118
|
-
for (const m of data.by_model) {
|
|
119
|
-
const prefix = m.provider ? `${m.provider}/` : "";
|
|
120
|
-
lines.push(
|
|
121
|
-
` ${prefix}${m.model}: ${m.session_count} sessions, ` +
|
|
122
|
-
`${m.tokens_input.toLocaleString()} in / ${m.tokens_output.toLocaleString()} out, ` +
|
|
123
|
-
`$${m.cost.toFixed(4)}`,
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// detail:true is opt-in — default text above is byte-identical to before.
|
|
129
|
-
if (detail && data.detail) {
|
|
130
|
-
lines.push("");
|
|
131
|
-
lines.push("Detail (tool activity — signal, not quality):");
|
|
132
|
-
lines.push(` steps: ${data.detail.steps.toLocaleString()}`);
|
|
133
|
-
const tools = Object.entries(data.detail.tool_breakdown).sort(
|
|
134
|
-
(a, b) => b[1] - a[1],
|
|
135
|
-
);
|
|
136
|
-
if (tools.length > 0) {
|
|
137
|
-
lines.push(" by tool:");
|
|
138
|
-
for (const [tool, count] of tools.slice(0, 20)) {
|
|
139
|
-
lines.push(` ${tool}: ${count.toLocaleString()}`);
|
|
140
|
-
}
|
|
141
|
-
if (tools.length > 20) {
|
|
142
|
-
lines.push(` ... +${tools.length - 20} more (see json:true)`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
// Context bytes by tool — proportion of context window consumed per tool.
|
|
146
|
-
// Bytes are a proxy for tokens; shown as % of total, never as "tokens".
|
|
147
|
-
// Merged across all clients: OpenCode/ZCode (state.output), Claude Code
|
|
148
|
-
// and Codex (tool_result) each populate bytes_by_tool, so reading the
|
|
149
|
-
// top-level (opencode) detail alone would miss the other clients.
|
|
150
|
-
const bytes = mergeBytesByTool(
|
|
151
|
-
data.detail,
|
|
152
|
-
...otherResults.map((r) => r.result.detail),
|
|
153
|
-
);
|
|
154
|
-
if (bytes) {
|
|
155
|
-
const entries = Object.entries(bytes).sort((a, b) => b[1] - a[1]);
|
|
156
|
-
const total = entries.reduce((s, e) => s + e[1], 0);
|
|
157
|
-
if (entries.length > 0 && total > 0) {
|
|
158
|
-
lines.push(" context bytes by tool (% of total):");
|
|
159
|
-
for (const [tool, b] of entries.slice(0, 10)) {
|
|
160
|
-
const pct = ((b / total) * 100).toFixed(1);
|
|
161
|
-
lines.push(` ${tool}: ${pct}%`);
|
|
162
|
-
}
|
|
163
|
-
if (entries.length > 10) {
|
|
164
|
-
lines.push(` ... +${entries.length - 10} more (see json:true)`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
lines.push(` sessions with activity: ${data.detail.by_session.length}`);
|
|
169
|
-
const top = data.detail.by_session.slice(0, 10);
|
|
170
|
-
for (const s of top) {
|
|
171
|
-
const topTool = Object.entries(s.tools).sort((a, b) => b[1] - a[1])[0];
|
|
172
|
-
lines.push(
|
|
173
|
-
` ${s.session_id}: ${s.steps} steps` +
|
|
174
|
-
(topTool ? `, top tool ${topTool[0]}×${topTool[1]}` : ""),
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
if (data.detail.by_session.length > 10) {
|
|
178
|
-
lines.push(
|
|
179
|
-
` ... +${data.detail.by_session.length - 10} more (see json:true)`,
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// One text section per non-primary client — same shape for every client,
|
|
185
|
-
// new ones need no new formatting code, only a registry.ts entry.
|
|
186
|
-
for (const { client, result } of otherResults) {
|
|
187
|
-
if (result.session_count === 0) continue;
|
|
188
|
-
const imp = imputationOf(result, prices);
|
|
189
|
-
const label = client.reportLabel ?? client.key;
|
|
190
|
-
lines.push("");
|
|
191
|
-
lines.push(`${label} usage:`);
|
|
192
|
-
lines.push(
|
|
193
|
-
` total: ${result.total_tokens_input.toLocaleString()} in / ${result.total_tokens_output.toLocaleString()} out / ${result.total_tokens_reasoning.toLocaleString()} reasoning tokens over ${result.session_count} sessions`,
|
|
194
|
-
);
|
|
195
|
-
lines.push(
|
|
196
|
-
` cache: ${result.total_tokens_cache_read.toLocaleString()} read / ${result.total_tokens_cache_write.toLocaleString()} write`,
|
|
197
|
-
);
|
|
198
|
-
if (result.by_model.length > 0) {
|
|
199
|
-
lines.push(" by model:");
|
|
200
|
-
for (const m of result.by_model) {
|
|
201
|
-
const prefix = m.provider ? `${m.provider}/` : "";
|
|
202
|
-
lines.push(
|
|
203
|
-
` ${prefix}${m.model}: ${m.tokens_input.toLocaleString()} in / ${m.tokens_output.toLocaleString()} out (cache r/w: ${(m.tokens_cache_read ?? 0).toLocaleString()} / ${(m.tokens_cache_write ?? 0).toLocaleString()})${imputedSuffix(m.provider, m.model, imp)}`,
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
lines.push(...imputedTextLines("", result, prices));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
211
|
-
}
|