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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +473 -0
  3. package/fapony.ts +78 -0
  4. package/package.json +42 -0
  5. package/skill/git-commit-conventional/SKILL.md +68 -0
  6. package/skill/git-ship/SKILL.md +144 -0
  7. package/skill/move-to-done/SKILL.md +126 -0
  8. package/skill/plan-with-pony/SKILL.md +263 -0
  9. package/skill/review-pony/SKILL.md +254 -0
  10. package/src/analyze.ts +517 -0
  11. package/src/context/index.ts +11 -0
  12. package/src/context/projectHealth.ts +359 -0
  13. package/src/conventions-seed.ts +420 -0
  14. package/src/db/defaults.ts +26 -0
  15. package/src/db/getters.ts +33 -0
  16. package/src/db/index.ts +7 -0
  17. package/src/db/load.ts +57 -0
  18. package/src/db/store.ts +286 -0
  19. package/src/db/types.ts +79 -0
  20. package/src/debt.ts +667 -0
  21. package/src/digest/cli.ts +75 -0
  22. package/src/digest/collect.ts +625 -0
  23. package/src/digest/html.ts +208 -0
  24. package/src/digest/text.ts +191 -0
  25. package/src/gate.ts +153 -0
  26. package/src/gates.ts +194 -0
  27. package/src/hook.ts +436 -0
  28. package/src/init-mem.ts +71 -0
  29. package/src/init.ts +237 -0
  30. package/src/install/claude.ts +361 -0
  31. package/src/install/codex.ts +61 -0
  32. package/src/install/cursor.ts +167 -0
  33. package/src/install/detect.ts +78 -0
  34. package/src/install/opencode.ts +234 -0
  35. package/src/install/skills.ts +106 -0
  36. package/src/install/types.ts +69 -0
  37. package/src/install/utils.ts +29 -0
  38. package/src/install/zcode.ts +120 -0
  39. package/src/install.ts +176 -0
  40. package/src/lint-baseline.ts +260 -0
  41. package/src/map.ts +320 -0
  42. package/src/math.ts +13 -0
  43. package/src/mcp/evidence.ts +332 -0
  44. package/src/mcp/primitives.ts +316 -0
  45. package/src/mcp/tools/check.ts +243 -0
  46. package/src/mcp/tools/collect.ts +157 -0
  47. package/src/mcp/tools/context.ts +66 -0
  48. package/src/mcp/tools/index.ts +309 -0
  49. package/src/mcp/tools/mem.ts +95 -0
  50. package/src/mcp/tools/plans.ts +255 -0
  51. package/src/mcp/tools/report.ts +285 -0
  52. package/src/mcp/tools/stats.ts +96 -0
  53. package/src/mcp/tools/usage.ts +211 -0
  54. package/src/mcp/tools/verdict.ts +148 -0
  55. package/src/mcp/transport.ts +241 -0
  56. package/src/mcp/types.ts +54 -0
  57. package/src/mcp/worktree.ts +27 -0
  58. package/src/memory.ts +264 -0
  59. package/src/parse.ts +71 -0
  60. package/src/plan-seed.ts +599 -0
  61. package/src/price/fetch.ts +146 -0
  62. package/src/price/index.ts +8 -0
  63. package/src/price/resolve.ts +213 -0
  64. package/src/report/cli.ts +92 -0
  65. package/src/report/format.ts +37 -0
  66. package/src/report/index.ts +4 -0
  67. package/src/report/render.ts +206 -0
  68. package/src/review-seed.ts +932 -0
  69. package/src/safety.ts +18 -0
  70. package/src/session/activeSession.ts +153 -0
  71. package/src/session/claude-code.ts +412 -0
  72. package/src/session/codex.ts +347 -0
  73. package/src/session/findModel.ts +376 -0
  74. package/src/session/helpers.ts +640 -0
  75. package/src/session/index.ts +31 -0
  76. package/src/session/opencode.ts +167 -0
  77. package/src/session/registry.ts +45 -0
  78. package/src/session/types.ts +128 -0
  79. package/src/session/zcode.ts +151 -0
  80. package/src/setup.ts +242 -0
  81. package/src/stats/cli.ts +44 -0
  82. package/src/stats/data.ts +1019 -0
  83. package/src/stats/format.ts +584 -0
  84. package/src/stats/index.ts +19 -0
  85. package/src/telemetry.ts +364 -0
  86. package/src/test.ts +2 -0
  87. package/src/update.ts +212 -0
  88. package/src/usage/cache.ts +125 -0
  89. package/src/usage/cli.ts +120 -0
  90. package/src/usage/format.ts +29 -0
  91. package/src/usage/index.ts +4 -0
  92. package/src/usage/render.ts +523 -0
  93. package/src/usage/scan.ts +161 -0
  94. package/src/util.ts +32 -0
  95. package/src/web/html.ts +33 -0
  96. package/templates/PLAN.md +90 -0
  97. package/templates/SPEC.md +30 -0
  98. package/templates/mem/commands/plan.ts +360 -0
  99. package/templates/mem/commands/read.ts +194 -0
  100. package/templates/mem/commands/rotate.ts +59 -0
  101. package/templates/mem/commands/selftest.ts +450 -0
  102. package/templates/mem/commands/write.ts +214 -0
  103. package/templates/mem/mem.ts +68 -0
  104. package/templates/mem/render.ts +63 -0
  105. package/templates/mem/selectors.ts +144 -0
  106. package/templates/mem/store.ts +285 -0
@@ -0,0 +1,161 @@
1
+ // src/usage/scan.ts — fapony usage-scan command
2
+ //
3
+ // Scans session logs for all four clients, writes usage-cache.jsonl.
4
+ // Windowed replace (30d default, all-time with --full) — never incremental.
5
+ // Per-worktree + global aggregate — the cache now carries a worktree dimension.
6
+ // Progress bar on TTY, plain lines on pipe/CI.
7
+
8
+ import { loadConfig, openDb } from "../db/index.js";
9
+ import { CLIENTS } from "../session/index.js";
10
+ import type { PassiveUsageResult } from "../session/types.js";
11
+ import {
12
+ type CacheEntry,
13
+ cacheMeta,
14
+ mergeEntries,
15
+ readCache,
16
+ writeCache,
17
+ } from "./cache.js";
18
+
19
+ const DEFAULT_JSONL_LOOKBACK_DAYS = 30;
20
+
21
+ function toCacheEntry(
22
+ client: string,
23
+ result: PassiveUsageResult,
24
+ worktree?: string,
25
+ ): CacheEntry {
26
+ return {
27
+ client,
28
+ worktree: worktree ?? null,
29
+ scanned_at: new Date().toISOString(),
30
+ session_count: result.session_count,
31
+ total_tokens_input: result.total_tokens_input,
32
+ total_tokens_output: result.total_tokens_output,
33
+ total_tokens_reasoning: result.total_tokens_reasoning,
34
+ total_tokens_cache_read: result.total_tokens_cache_read,
35
+ total_tokens_cache_write: result.total_tokens_cache_write,
36
+ total_cost: result.total_cost,
37
+ by_model: result.by_model.map((m) => ({
38
+ model: m.model,
39
+ provider: m.provider,
40
+ session_count: m.session_count,
41
+ tokens_input: m.tokens_input,
42
+ tokens_output: m.tokens_output,
43
+ tokens_reasoning: m.tokens_reasoning,
44
+ tokens_cache_read: m.tokens_cache_read,
45
+ tokens_cache_write: m.tokens_cache_write,
46
+ cost: m.cost,
47
+ })),
48
+ ...(result.error ? { error: result.error } : {}),
49
+ };
50
+ }
51
+
52
+ /** Surface a per-client problem without aborting the scan of the others. */
53
+ function warn(scope: string, detail: string, isTTY: boolean): void {
54
+ process.stderr.write(
55
+ isTTY ? `\n ⚠ ${scope}: ${detail}\n` : ` warn: ${scope}: ${detail}\n`,
56
+ );
57
+ }
58
+
59
+ function progress(msg: string, isTTY: boolean): void {
60
+ if (isTTY) {
61
+ process.stderr.write(`\r\x1B[K${msg}`);
62
+ } else {
63
+ process.stderr.write(`${msg}\n`);
64
+ }
65
+ }
66
+
67
+ export function cmdUsageScan(rawArgs: string[]): void {
68
+ const full = rawArgs.includes("--full");
69
+ const args = rawArgs.filter((a) => a !== "--full");
70
+ const config = loadConfig();
71
+ const isTTY = !!process.stderr.isTTY;
72
+
73
+ if (args[0] && args[0] !== "--full") {
74
+ console.error("usage: fapony usage-scan [--full]");
75
+ process.exit(1);
76
+ }
77
+
78
+ const existing = readCache(config);
79
+
80
+ const windowSince = full
81
+ ? undefined
82
+ : Date.now() / 1000 - DEFAULT_JSONL_LOOKBACK_DAYS * 86400;
83
+
84
+ // Collect distinct worktrees from runs table (the source of truth for projects).
85
+ const db = openDb();
86
+ let worktrees: string[];
87
+ try {
88
+ worktrees = (
89
+ db
90
+ .prepare("SELECT DISTINCT worktree FROM runs WHERE worktree != ''")
91
+ .all() as { worktree: string }[]
92
+ )
93
+ .map((r) => r.worktree)
94
+ .sort();
95
+ } finally {
96
+ db.close();
97
+ }
98
+
99
+ // Scan targets: per worktree + global (null = aggregate).
100
+ const scanTargets: Array<{ label: string; worktree?: string }> = [
101
+ ...worktrees.map((wt) => ({ label: wt, worktree: wt })),
102
+ { label: "all projects" },
103
+ ];
104
+
105
+ const totalSteps = scanTargets.length * CLIENTS.length;
106
+ let step = 0;
107
+
108
+ const entries: CacheEntry[] = [];
109
+
110
+ for (const target of scanTargets) {
111
+ const wt = target.worktree;
112
+
113
+ for (const client of CLIENTS) {
114
+ step++;
115
+ const pct = Math.round((step / totalSteps) * 100);
116
+ const bar = `[${"█".repeat(Math.round(pct / 5))}${"░".repeat(20 - Math.round(pct / 5))}]`;
117
+ const label = client.scanLabel ?? client.key;
118
+ const scopeLabel = wt ? `${target.label}/${label}` : label;
119
+ progress(
120
+ `${scopeLabel} ${bar} ${step}/${totalSteps} (${pct}%)`,
121
+ isTTY,
122
+ );
123
+
124
+ try {
125
+ const result = client.read(wt, windowSince, undefined, false, full);
126
+ // A failed read returns zeros WITH a code — the same shape as "no
127
+ // sessions", so it gets dropped below and would otherwise leave the
128
+ // cache silently short one client. Say it out loud instead.
129
+ if (result.error) warn(scopeLabel, result.error, isTTY);
130
+ // A broken client is cached too, precisely because it has no sessions
131
+ // to speak for it — otherwise it just vanishes from usage-web.
132
+ if (result.session_count > 0 || result.error) {
133
+ entries.push(toCacheEntry(client.key, result, wt));
134
+ }
135
+ } catch (err) {
136
+ warn(scopeLabel, String(err), isTTY);
137
+ }
138
+ }
139
+ }
140
+
141
+ // Merge with existing and write.
142
+ const merged = mergeEntries(existing, entries);
143
+ writeCache(merged, config);
144
+
145
+ const finalMeta = cacheMeta(merged);
146
+ if (isTTY) {
147
+ process.stderr.write(
148
+ `\r\x1B[Kscan done — ${finalMeta?.total_sessions ?? 0} sessions cached (${entries.length} entries written, ${worktrees.length} projects)\n`,
149
+ );
150
+ } else {
151
+ process.stderr.write(
152
+ `scan done — ${finalMeta?.total_sessions ?? 0} sessions cached (${entries.length} entries written, ${worktrees.length} projects)\n`,
153
+ );
154
+ }
155
+
156
+ console.log(`cache written to ${cachePathHint(config)}`);
157
+ }
158
+
159
+ function cachePathHint(_config?: ReturnType<typeof loadConfig>): string {
160
+ return "usage-cache.jsonl";
161
+ }
package/src/util.ts ADDED
@@ -0,0 +1,32 @@
1
+ // Replacement values are inserted via a function so `$&`, `$'` etc. in the
2
+ // value are treated as literal text, not replace() special patterns.
3
+ export function templateArgs(
4
+ arr: string[],
5
+ vars: Record<string, string>,
6
+ ): string[] {
7
+ return arr.map((s) => {
8
+ let out = s;
9
+ for (const [k, v] of Object.entries(vars)) {
10
+ out = out.replaceAll(`{${k}}`, () => v);
11
+ }
12
+ return out;
13
+ });
14
+ }
15
+
16
+ /** Fill a prompt template with {{VARS}} (all occurrences). Missing vars → "". */
17
+ export function fillPrompt(
18
+ template: string,
19
+ vars: Record<string, string>,
20
+ ): string {
21
+ let out = template;
22
+ for (const [k, v] of Object.entries(vars)) {
23
+ out = out.replaceAll(`{{${k}}}`, () => v);
24
+ }
25
+ return out;
26
+ }
27
+
28
+ /** True for "y"/"yes" (case-insensitive, trimmed) — the only affirmative answers. */
29
+ export function isAffirmative(answer: string): boolean {
30
+ const normalized = answer.trim().toLowerCase();
31
+ return normalized === "y" || normalized === "yes";
32
+ }
@@ -0,0 +1,33 @@
1
+ // src/web/html.ts — shared HTML helpers for report-web and usage-web
2
+
3
+ /** HTML-escape interpolated strings. */
4
+ export function esc(s: string): string {
5
+ return s
6
+ .replaceAll("&", "&amp;")
7
+ .replaceAll("<", "&lt;")
8
+ .replaceAll(">", "&gt;")
9
+ .replaceAll('"', "&quot;");
10
+ }
11
+
12
+ /** Common dark-theme CSS variables + base reset used by all HTML pages. */
13
+ export const DARK_THEME_CSS = `
14
+ :root { --bg: #0d1117; --fg: #c9d1d9; --border: #30363d; --accent: #58a6ff; --green: #3fb950; --red: #f85149; --yellow: #d29922; --muted: #8b949e; }
15
+ * { box-sizing: border-box; margin: 0; padding: 0; }
16
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: var(--bg); color: var(--fg); line-height: 1.6; padding: 2rem; max-width: 960px; margin: 0 auto; }
17
+ `;
18
+
19
+ /** Shared table + typography CSS used by both report and usage pages. */
20
+ export const TABLE_CSS = `
21
+ h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
22
+ h2 { font-size: 1.1rem; color: var(--accent); margin: 1.5rem 0 0.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; }
23
+ .meta { color: var(--muted); font-size: 0.85rem; margin-bottom: 1.5rem; }
24
+ table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; font-size: 0.9rem; }
25
+ th, td { padding: 0.5rem 0.8rem; text-align: left; border-bottom: 1px solid var(--border); }
26
+ th { color: var(--muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; }
27
+ tr:hover { background: #161b22; }
28
+ .pass { color: var(--green); }
29
+ .fail { color: var(--red); }
30
+ .warn { color: var(--yellow); }
31
+ .muted { color: var(--muted); }
32
+ .sample { font-size: 0.8rem; color: var(--muted); }
33
+ `;
@@ -0,0 +1,90 @@
1
+ # Plan Core — template for every plan file
2
+
3
+ > Use this template for every plan file (not just fapony) — see [CLAUDE.md](../CLAUDE.md) § Rules
4
+ > for AI Agents for the rules each plan must pass before an agent may execute it.
5
+
6
+ ```markdown
7
+ ---
8
+ kind: unit # `tracker` for a checklist that never finishes; omit = unit of work
9
+ status: active # active | blocked | superseded · omit = not started
10
+ blocked_by: <plan or sentence> # required when status: blocked
11
+ blocks: PLAN-<other>.md # plans that cannot start until this one lands (comma-separated)
12
+ superseded_by: PLAN-<other>.md # required when status: superseded
13
+ spec: SPEC-<feature>.md # if any
14
+ ---
15
+
16
+ # PLAN-<feature>.md — <short name>
17
+
18
+ > **Status:** 🚧 in-progress · **Owner:** <dev> · **Created:** <YYYY-MM-DD>
19
+ > **Source spec:** [spec/<feature>.md](../spec/<feature>.md) — if any
20
+
21
+ ## TL;DR
22
+ - **What:** one line
23
+ - **Why:** one line — the decision or the pain, not the implementation
24
+ - **Done when:** one line, testable
25
+ - **Order:** what this waits on / what it unblocks (mirrors the frontmatter)
26
+ - **Progress:**
27
+ - [x] chunk 1 — <what landed> `<short sha>` <YYYY-MM-DD>
28
+ - [ ] chunk 2 — <what is next>
29
+
30
+ ---
31
+
32
+ ## 1. Goal (why)
33
+ 1–3 sentences — if a reader can't answer "so what" after reading = not clear yet
34
+
35
+ ## 2. Scope (do / don't do)
36
+ **Do:** 3–7 bullets, outcomes not tasks
37
+ **Don't do:** 2–5 bullets + 1-line reason per item
38
+
39
+ ## 3. Done criteria (how we know it's finished)
40
+ 3–6 bullets — testable (tests pass / command runs / user can reproduce)
41
+ Never write bare "done" — must be measurable
42
+
43
+ ## 4. Constraints / Hard rules (must not violate)
44
+ 3–8 bullets — violations that break things (not "good practices")
45
+
46
+ ## 5. Risks & Escape hatches (if it fails)
47
+ Table with 3–5 rows: risk | likelihood | impact | escape hatch
48
+
49
+ ## 6. Steps (what in which order)
50
+ 1. **<Step 1>** — has a clear deliverable
51
+ 2. **<Step 2>** — ...
52
+ Each step must be verifiable before moving to the next
53
+
54
+ ## 7. Examples (make it concrete)
55
+ bash examples: before / after — **link into spec/, don't paste it.**
56
+ If Source spec exists, this section stays ≤5 lines (1-2 examples or a pointer);
57
+ anything longer belongs in the spec file, not here.
58
+
59
+ ## 8. References
60
+ - link back to related files
61
+ ```
62
+
63
+ **Two rules that keep the file cheap to read:**
64
+ - **The TL;DR is 15 lines, hard cap.** It is the only part that changes while the work is in
65
+ flight (tick a box, stamp a sha); everything below it is the agreement and changes only when
66
+ scope changes. That contract is what lets a reader trust the first 40 lines instead of pulling
67
+ 100KB into context.
68
+ - **`plan_list` counts the checkboxes in the first `##` section only** — whatever that section is
69
+ called, so the tally works in any language, and a step list deeper in the file stays detail
70
+ instead of becoming status.
71
+
72
+ **Where files live:** `.fapony/plan/` live · `.fapony/done/` shipped (a sibling, so archiving is a
73
+ plain `git mv` that keeps every relative link working) · `.fapony/spec/` every spec, never archived.
74
+ See [examples/](../examples/) for the whole layout with one file per shape.
75
+
76
+ **Language:** frontmatter keys and values are English always (they are an enum a tool reads);
77
+ headings stay as this template has them; everything else is written in whatever language the dev
78
+ reads, because the plan is for them.
79
+
80
+ **4 iron rules:**
81
+ - Sections 1–4 are mandatory — if missing = plan is immature, agent must not execute
82
+ - Section 6 each step must be verifiable — if you can't tell it passed = not clear yet
83
+ - Section 8 must link back — prevents drift and gives context on reopen
84
+ - **Plan is "what/why/order", spec is "how in detail"** — API shapes, schemas,
85
+ wireframes, edge-case tables go in [spec/](../spec/), the plan only links to
86
+ them. A plan that keeps growing past ~200 lines is spec content leaking in,
87
+ not a plan getting more thorough — split it out.
88
+
89
+ Use [templates/SPEC.md](SPEC.md) for the spec file itself — it links back to
90
+ every plan that uses it, so the relationship reads both ways.
@@ -0,0 +1,30 @@
1
+ # Spec Core — template for every spec file
2
+
3
+ > Use with [PLAN.md](PLAN.md). A spec holds the **detail** a plan should only
4
+ > link to: API/data shapes, schemas, wireframes, edge cases, examples. Put it
5
+ > in `.fapony/spec/<feature>.md`.
6
+
7
+ ```markdown
8
+ # SPEC-<feature>.md — <short name>
9
+
10
+ > **Used by:** [PLAN-<feature>.md](../plan/PLAN-<feature>.md)
11
+ > (add every plan that references this spec — keeps the link bidirectional so
12
+ > either file leads you to the other, and `fapony plan-mv` finds this file
13
+ > when it scans inbound links.)
14
+
15
+ ---
16
+
17
+ ## Shape (data / API / schema)
18
+ Concrete types, request/response bodies, DB columns — whatever the code needs.
19
+
20
+ ## Edge cases
21
+ Table or bullets: input → expected behavior.
22
+
23
+ ## Examples
24
+ Before / after, request / response, sample payloads — as long as it needs to be.
25
+ ```
26
+
27
+ **Rule:** a plan's section 7 (Examples) links here instead of pasting content.
28
+ If a plan keeps growing, the fix is usually "move it into the spec", not
29
+ "trim the plan" — the detail is still needed, just not in the file an agent
30
+ re-reads every round.