fapony 0.1.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/LICENSE +21 -0
- package/README.md +473 -0
- package/fapony.ts +78 -0
- package/package.json +42 -0
- package/skill/git-commit-conventional/SKILL.md +68 -0
- package/skill/git-ship/SKILL.md +144 -0
- package/skill/move-to-done/SKILL.md +126 -0
- package/skill/plan-with-pony/SKILL.md +263 -0
- package/skill/review-pony/SKILL.md +254 -0
- package/src/analyze.ts +517 -0
- package/src/context/index.ts +11 -0
- package/src/context/projectHealth.ts +359 -0
- package/src/conventions-seed.ts +420 -0
- package/src/db/defaults.ts +26 -0
- package/src/db/getters.ts +33 -0
- package/src/db/index.ts +7 -0
- package/src/db/load.ts +57 -0
- package/src/db/store.ts +286 -0
- package/src/db/types.ts +79 -0
- package/src/debt.ts +667 -0
- package/src/digest/cli.ts +75 -0
- package/src/digest/collect.ts +625 -0
- package/src/digest/html.ts +208 -0
- package/src/digest/text.ts +191 -0
- package/src/gate.ts +153 -0
- package/src/gates.ts +194 -0
- package/src/hook.ts +436 -0
- package/src/init-mem.ts +71 -0
- package/src/init.ts +237 -0
- package/src/install/claude.ts +361 -0
- package/src/install/codex.ts +61 -0
- package/src/install/cursor.ts +167 -0
- package/src/install/detect.ts +78 -0
- package/src/install/opencode.ts +234 -0
- package/src/install/skills.ts +106 -0
- package/src/install/types.ts +69 -0
- package/src/install/utils.ts +29 -0
- package/src/install/zcode.ts +120 -0
- package/src/install.ts +176 -0
- package/src/lint-baseline.ts +260 -0
- package/src/map.ts +320 -0
- package/src/math.ts +13 -0
- package/src/mcp/evidence.ts +332 -0
- package/src/mcp/primitives.ts +316 -0
- package/src/mcp/tools/check.ts +243 -0
- package/src/mcp/tools/collect.ts +157 -0
- package/src/mcp/tools/context.ts +66 -0
- package/src/mcp/tools/index.ts +309 -0
- package/src/mcp/tools/mem.ts +95 -0
- package/src/mcp/tools/plans.ts +255 -0
- package/src/mcp/tools/report.ts +285 -0
- package/src/mcp/tools/stats.ts +96 -0
- package/src/mcp/tools/usage.ts +211 -0
- package/src/mcp/tools/verdict.ts +148 -0
- package/src/mcp/transport.ts +241 -0
- package/src/mcp/types.ts +54 -0
- package/src/mcp/worktree.ts +27 -0
- package/src/memory.ts +264 -0
- package/src/parse.ts +71 -0
- package/src/plan-seed.ts +599 -0
- package/src/price/fetch.ts +146 -0
- package/src/price/index.ts +8 -0
- package/src/price/resolve.ts +213 -0
- package/src/report/cli.ts +92 -0
- package/src/report/format.ts +37 -0
- package/src/report/index.ts +4 -0
- package/src/report/render.ts +206 -0
- package/src/review-seed.ts +932 -0
- package/src/safety.ts +18 -0
- package/src/session/activeSession.ts +153 -0
- package/src/session/claude-code.ts +412 -0
- package/src/session/codex.ts +347 -0
- package/src/session/findModel.ts +376 -0
- package/src/session/helpers.ts +640 -0
- package/src/session/index.ts +31 -0
- package/src/session/opencode.ts +167 -0
- package/src/session/registry.ts +45 -0
- package/src/session/types.ts +128 -0
- package/src/session/zcode.ts +151 -0
- package/src/setup.ts +242 -0
- package/src/stats/cli.ts +44 -0
- package/src/stats/data.ts +1019 -0
- package/src/stats/format.ts +584 -0
- package/src/stats/index.ts +19 -0
- package/src/telemetry.ts +364 -0
- package/src/test.ts +2 -0
- package/src/update.ts +212 -0
- package/src/usage/cache.ts +125 -0
- package/src/usage/cli.ts +120 -0
- package/src/usage/format.ts +29 -0
- package/src/usage/index.ts +4 -0
- package/src/usage/render.ts +523 -0
- package/src/usage/scan.ts +161 -0
- package/src/util.ts +32 -0
- package/src/web/html.ts +33 -0
- package/templates/PLAN.md +90 -0
- package/templates/SPEC.md +30 -0
- package/templates/mem/commands/plan.ts +360 -0
- package/templates/mem/commands/read.ts +194 -0
- package/templates/mem/commands/rotate.ts +59 -0
- package/templates/mem/commands/selftest.ts +450 -0
- package/templates/mem/commands/write.ts +214 -0
- package/templates/mem/mem.ts +68 -0
- package/templates/mem/render.ts +63 -0
- package/templates/mem/selectors.ts +144 -0
- package/templates/mem/store.ts +285 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// src/session/codex.ts — Codex passive usage reader
|
|
2
|
+
//
|
|
3
|
+
// Reads ~/.codex/sessions/**/*.jsonl (one file per rollout session).
|
|
4
|
+
// Each line is a JSON object; usage data lives in type: "token_usage_record"
|
|
5
|
+
// payloads. No cost field — total_cost is always 0.
|
|
6
|
+
//
|
|
7
|
+
// detail:true adds tool_breakdown/bytes_by_tool from response_item lines of
|
|
8
|
+
// type custom_tool_call / custom_tool_call_output, matched by call_id — the
|
|
9
|
+
// Codex equivalent of Claude Code's tool_use/tool_result pair.
|
|
10
|
+
|
|
11
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
EMPTY_RESULT,
|
|
16
|
+
type ModelBreakdown,
|
|
17
|
+
type PassiveUsageResult,
|
|
18
|
+
type UsageDetail,
|
|
19
|
+
} from "./types.js";
|
|
20
|
+
|
|
21
|
+
const SESSIONS_DIR = join(homedir(), ".codex", "sessions");
|
|
22
|
+
|
|
23
|
+
function resolveSessionsDir(optDir?: string): string {
|
|
24
|
+
return optDir ?? process.env.FAPONY_CODEX_SESSIONS_DIR ?? SESSIONS_DIR;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface TokenUsageRecord {
|
|
28
|
+
session_id: string;
|
|
29
|
+
thread_id?: string;
|
|
30
|
+
turn_id?: string;
|
|
31
|
+
root_turn_id?: string;
|
|
32
|
+
usage?: {
|
|
33
|
+
input_tokens?: number;
|
|
34
|
+
output_tokens?: number;
|
|
35
|
+
reasoning_output_tokens?: number;
|
|
36
|
+
cached_input_tokens?: number;
|
|
37
|
+
cache_write_input_tokens?: number;
|
|
38
|
+
};
|
|
39
|
+
turn_token_usage?: {
|
|
40
|
+
input_tokens?: number;
|
|
41
|
+
output_tokens?: number;
|
|
42
|
+
reasoning_output_tokens?: number;
|
|
43
|
+
cached_input_tokens?: number;
|
|
44
|
+
cache_write_input_tokens?: number;
|
|
45
|
+
};
|
|
46
|
+
thread_token_usage?: {
|
|
47
|
+
input_tokens?: number;
|
|
48
|
+
output_tokens?: number;
|
|
49
|
+
reasoning_output_tokens?: number;
|
|
50
|
+
cached_input_tokens?: number;
|
|
51
|
+
cache_write_input_tokens?: number;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface CodexLine {
|
|
56
|
+
timestamp?: string;
|
|
57
|
+
type?: string;
|
|
58
|
+
payload?: {
|
|
59
|
+
type?: string; // "custom_tool_call" | "custom_tool_call_output" for response_item lines
|
|
60
|
+
session_id?: string;
|
|
61
|
+
cwd?: string;
|
|
62
|
+
usage?: TokenUsageRecord["usage"];
|
|
63
|
+
turn_token_usage?: TokenUsageRecord["turn_token_usage"];
|
|
64
|
+
thread_token_usage?: TokenUsageRecord["thread_token_usage"];
|
|
65
|
+
// session_meta fields
|
|
66
|
+
model_provider?: string;
|
|
67
|
+
model?: string;
|
|
68
|
+
base_instructions?: {
|
|
69
|
+
provenance?: {
|
|
70
|
+
model?: string;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
// custom_tool_call / custom_tool_call_output fields
|
|
74
|
+
call_id?: string;
|
|
75
|
+
name?: string;
|
|
76
|
+
output?: unknown;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ModelAcc {
|
|
81
|
+
provider: string;
|
|
82
|
+
session_count: number;
|
|
83
|
+
tokens_input: number;
|
|
84
|
+
tokens_output: number;
|
|
85
|
+
tokens_reasoning: number;
|
|
86
|
+
tokens_cache_read: number;
|
|
87
|
+
tokens_cache_write: number;
|
|
88
|
+
cost: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function walkJsonl(dir: string): string[] {
|
|
92
|
+
const files: string[] = [];
|
|
93
|
+
let entries: string[];
|
|
94
|
+
try {
|
|
95
|
+
entries = readdirSync(dir);
|
|
96
|
+
} catch {
|
|
97
|
+
return files;
|
|
98
|
+
}
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
const full = join(dir, entry);
|
|
101
|
+
try {
|
|
102
|
+
const st = statSync(full);
|
|
103
|
+
if (st.isDirectory()) {
|
|
104
|
+
files.push(...walkJsonl(full));
|
|
105
|
+
} else if (entry.endsWith(".jsonl")) {
|
|
106
|
+
files.push(full);
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// skip inaccessible entries
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return files;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Read passive usage from Codex JSONL session files.
|
|
117
|
+
* Returns EMPTY_RESULT when the sessions dir is missing, inaccessible,
|
|
118
|
+
* or has no qualifying token records.
|
|
119
|
+
*
|
|
120
|
+
* detail:true adds a UsageDetail — tool_breakdown/bytes_by_tool come from
|
|
121
|
+
* custom_tool_call/custom_tool_call_output pairs (matched by call_id); no
|
|
122
|
+
* per-session or timing breakdown yet. Steps are counted from
|
|
123
|
+
* token_usage_record lines.
|
|
124
|
+
*/
|
|
125
|
+
export function readCodexUsage(
|
|
126
|
+
worktree?: string,
|
|
127
|
+
since?: number,
|
|
128
|
+
until?: number,
|
|
129
|
+
detail?: boolean,
|
|
130
|
+
): PassiveUsageResult {
|
|
131
|
+
const sessionsDir = resolveSessionsDir();
|
|
132
|
+
try {
|
|
133
|
+
if (!statSync(sessionsDir).isDirectory()) return EMPTY_RESULT;
|
|
134
|
+
} catch {
|
|
135
|
+
return EMPTY_RESULT;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const files = walkJsonl(sessionsDir);
|
|
139
|
+
if (files.length === 0) return EMPTY_RESULT;
|
|
140
|
+
|
|
141
|
+
const models = new Map<string, ModelAcc>();
|
|
142
|
+
let totalSessions = 0;
|
|
143
|
+
let totalInput = 0;
|
|
144
|
+
let totalOutput = 0;
|
|
145
|
+
let totalReasoning = 0;
|
|
146
|
+
let totalCacheRead = 0;
|
|
147
|
+
let totalCacheWrite = 0;
|
|
148
|
+
let totalSteps = 0;
|
|
149
|
+
|
|
150
|
+
// Detail accumulators (only filled when detail:true).
|
|
151
|
+
const toolBreakdown: Record<string, number> = {};
|
|
152
|
+
const bytesByTool: Record<string, number> = {};
|
|
153
|
+
|
|
154
|
+
for (const filePath of files) {
|
|
155
|
+
// ponytail: skip files untouched since `since` before reading them —
|
|
156
|
+
// see claude-code.ts for the rationale (append-only JSONL, mtime gate
|
|
157
|
+
// avoids full-file read+parse on old history).
|
|
158
|
+
if (since !== undefined) {
|
|
159
|
+
try {
|
|
160
|
+
if (statSync(filePath).mtimeMs / 1000 < since) continue;
|
|
161
|
+
} catch {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
let content: string;
|
|
166
|
+
try {
|
|
167
|
+
content = readFileSync(filePath, "utf-8");
|
|
168
|
+
} catch {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Track per-file state for session attribution.
|
|
173
|
+
const fileSessions = new Set<string>();
|
|
174
|
+
const callNames = new Map<string, string>(); // call_id -> tool name, for this file
|
|
175
|
+
let fileCwd: string | undefined;
|
|
176
|
+
let fileTimestamp: string | undefined;
|
|
177
|
+
let fileModel: string | undefined;
|
|
178
|
+
let fileProvider: string = "";
|
|
179
|
+
let hasTokenUsage = false;
|
|
180
|
+
|
|
181
|
+
const lines = content.split("\n");
|
|
182
|
+
for (const line of lines) {
|
|
183
|
+
if (!line) continue;
|
|
184
|
+
|
|
185
|
+
let parsed: CodexLine;
|
|
186
|
+
try {
|
|
187
|
+
parsed = JSON.parse(line) as CodexLine;
|
|
188
|
+
} catch {
|
|
189
|
+
continue; // malformed line — skip, don't abort
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (parsed.type === "session_meta" && parsed.payload) {
|
|
193
|
+
fileCwd = parsed.payload.cwd;
|
|
194
|
+
fileTimestamp = parsed.timestamp;
|
|
195
|
+
// Model can be in payload.model or payload.base_instructions.provenance.model
|
|
196
|
+
fileModel =
|
|
197
|
+
parsed.payload.model ??
|
|
198
|
+
parsed.payload.base_instructions?.provenance?.model;
|
|
199
|
+
fileProvider = parsed.payload.model_provider ?? "";
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Detail pass: custom_tool_call → custom_tool_call_output, matched by
|
|
204
|
+
// call_id (Codex's equivalent of tool_use/tool_result).
|
|
205
|
+
if (detail && parsed.type === "response_item" && parsed.payload) {
|
|
206
|
+
const p = parsed.payload;
|
|
207
|
+
if (
|
|
208
|
+
p.type === "custom_tool_call" &&
|
|
209
|
+
typeof p.call_id === "string" &&
|
|
210
|
+
typeof p.name === "string"
|
|
211
|
+
) {
|
|
212
|
+
callNames.set(p.call_id, p.name);
|
|
213
|
+
toolBreakdown[p.name] = (toolBreakdown[p.name] ?? 0) + 1;
|
|
214
|
+
} else if (
|
|
215
|
+
p.type === "custom_tool_call_output" &&
|
|
216
|
+
typeof p.call_id === "string"
|
|
217
|
+
) {
|
|
218
|
+
const name = callNames.get(p.call_id);
|
|
219
|
+
if (name) {
|
|
220
|
+
const bytes = JSON.stringify(p.output ?? "").length;
|
|
221
|
+
bytesByTool[name] = (bytesByTool[name] ?? 0) + bytes;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (parsed.type === "token_usage_record" && parsed.payload) {
|
|
228
|
+
const rec = parsed.payload;
|
|
229
|
+
const usage = rec.usage ?? rec.turn_token_usage;
|
|
230
|
+
if (!usage) continue;
|
|
231
|
+
|
|
232
|
+
// Worktree filtering.
|
|
233
|
+
if (worktree && fileCwd && fileCwd !== worktree) continue;
|
|
234
|
+
|
|
235
|
+
// Timestamp filtering — use the line's own timestamp as fallback.
|
|
236
|
+
const ts = parsed.timestamp ?? fileTimestamp;
|
|
237
|
+
if (ts) {
|
|
238
|
+
const epoch = new Date(ts).getTime() / 1000;
|
|
239
|
+
if (since !== undefined && epoch < since) continue;
|
|
240
|
+
if (until !== undefined && epoch > until) continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const sessionId = rec.session_id ?? "unknown";
|
|
244
|
+
const model = fileModel ?? "(unknown)";
|
|
245
|
+
const input = usage.input_tokens ?? 0;
|
|
246
|
+
const output = usage.output_tokens ?? 0;
|
|
247
|
+
const reasoning = usage.reasoning_output_tokens ?? 0;
|
|
248
|
+
const cacheRead = usage.cached_input_tokens ?? 0;
|
|
249
|
+
const cacheWrite = usage.cache_write_input_tokens ?? 0;
|
|
250
|
+
|
|
251
|
+
// Only count each session id once per file.
|
|
252
|
+
fileSessions.add(sessionId);
|
|
253
|
+
hasTokenUsage = true;
|
|
254
|
+
totalSteps++;
|
|
255
|
+
|
|
256
|
+
let acc = models.get(model);
|
|
257
|
+
if (!acc) {
|
|
258
|
+
acc = {
|
|
259
|
+
provider: fileProvider,
|
|
260
|
+
session_count: 0,
|
|
261
|
+
tokens_input: 0,
|
|
262
|
+
tokens_output: 0,
|
|
263
|
+
tokens_reasoning: 0,
|
|
264
|
+
tokens_cache_read: 0,
|
|
265
|
+
tokens_cache_write: 0,
|
|
266
|
+
cost: 0,
|
|
267
|
+
};
|
|
268
|
+
models.set(model, acc);
|
|
269
|
+
}
|
|
270
|
+
acc.tokens_input += input;
|
|
271
|
+
acc.tokens_output += output;
|
|
272
|
+
acc.tokens_reasoning += reasoning;
|
|
273
|
+
acc.tokens_cache_read += cacheRead;
|
|
274
|
+
acc.tokens_cache_write += cacheWrite;
|
|
275
|
+
|
|
276
|
+
totalInput += input;
|
|
277
|
+
totalOutput += output;
|
|
278
|
+
totalReasoning += reasoning;
|
|
279
|
+
totalCacheRead += cacheRead;
|
|
280
|
+
totalCacheWrite += cacheWrite;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (hasTokenUsage && fileSessions.size > 0) {
|
|
285
|
+
totalSessions += fileSessions.size;
|
|
286
|
+
// Attribute 1 session to each model that appeared in this file.
|
|
287
|
+
const seenModels = new Set<string>();
|
|
288
|
+
for (const line of lines) {
|
|
289
|
+
if (!line) continue;
|
|
290
|
+
try {
|
|
291
|
+
const parsed = JSON.parse(line) as CodexLine;
|
|
292
|
+
if (parsed.type === "session_meta" && parsed.payload) {
|
|
293
|
+
const m =
|
|
294
|
+
parsed.payload.model ??
|
|
295
|
+
parsed.payload.base_instructions?.provenance?.model;
|
|
296
|
+
if (m && !seenModels.has(m)) {
|
|
297
|
+
seenModels.add(m);
|
|
298
|
+
const acc = models.get(m);
|
|
299
|
+
if (acc) acc.session_count++;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
/* skip malformed */
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (totalSessions === 0) return EMPTY_RESULT;
|
|
310
|
+
|
|
311
|
+
const by_model: ModelBreakdown[] = [...models.entries()]
|
|
312
|
+
.map(([model, acc]) => ({
|
|
313
|
+
provider: acc.provider,
|
|
314
|
+
model,
|
|
315
|
+
session_count: acc.session_count,
|
|
316
|
+
tokens_input: acc.tokens_input,
|
|
317
|
+
tokens_output: acc.tokens_output,
|
|
318
|
+
tokens_reasoning: acc.tokens_reasoning,
|
|
319
|
+
tokens_cache_read: acc.tokens_cache_read,
|
|
320
|
+
tokens_cache_write: acc.tokens_cache_write,
|
|
321
|
+
cost: acc.cost,
|
|
322
|
+
}))
|
|
323
|
+
.sort((a, b) => b.cost - a.cost || b.tokens_input - a.tokens_input);
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
total_tokens_input: totalInput,
|
|
327
|
+
total_tokens_output: totalOutput,
|
|
328
|
+
total_tokens_reasoning: totalReasoning,
|
|
329
|
+
total_tokens_cache_read: totalCacheRead,
|
|
330
|
+
total_tokens_cache_write: totalCacheWrite,
|
|
331
|
+
total_cost: 0, // Codex JSONL has no cost field
|
|
332
|
+
session_count: totalSessions,
|
|
333
|
+
by_model,
|
|
334
|
+
...(detail
|
|
335
|
+
? {
|
|
336
|
+
detail: {
|
|
337
|
+
tool_breakdown: toolBreakdown,
|
|
338
|
+
bytes_by_tool:
|
|
339
|
+
Object.keys(bytesByTool).length > 0 ? bytesByTool : undefined,
|
|
340
|
+
steps: totalSteps,
|
|
341
|
+
by_session: [],
|
|
342
|
+
note: "Codex has no per-session/timing breakdown — tool_breakdown and bytes_by_tool come from custom_tool_call pairs",
|
|
343
|
+
} satisfies UsageDetail,
|
|
344
|
+
}
|
|
345
|
+
: {}),
|
|
346
|
+
};
|
|
347
|
+
}
|