fapony 0.3.3 → 0.3.4

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 (81) hide show
  1. package/fapony.ts +3 -116
  2. package/package.json +2 -2
  3. package/skill/move-to-done/SKILL.md +3 -2
  4. package/src/adapters/cli.ts +123 -0
  5. package/src/adapters/hooks/compute-hint-impact.ts +107 -0
  6. package/src/adapters/hooks/context-data.ts +102 -0
  7. package/src/adapters/hooks/edit-hint.ts +195 -0
  8. package/src/adapters/hooks/git-autonomy.ts +178 -0
  9. package/src/adapters/hooks/index.ts +79 -0
  10. package/src/adapters/hooks/mv-guard.ts +52 -0
  11. package/src/adapters/hooks/read-hint.ts +383 -0
  12. package/src/adapters/hooks/session-start.ts +101 -0
  13. package/src/adapters/hooks/stop.ts +302 -0
  14. package/src/{mcp → adapters/mcp}/evidence.ts +2 -2
  15. package/src/{mcp → adapters/mcp}/primitives.ts +2 -2
  16. package/src/{mcp → adapters/mcp}/tools/check.ts +1 -1
  17. package/src/{mcp → adapters/mcp}/tools/collect.ts +1 -1
  18. package/src/{mcp → adapters/mcp}/tools/mem.ts +3 -3
  19. package/src/{mcp → adapters/mcp}/tools/report.ts +4 -4
  20. package/src/adapters/mcp/types.ts +18 -0
  21. package/src/{mcp → adapters/mcp}/worktree.ts +1 -1
  22. package/src/analyze.ts +1 -1
  23. package/src/conventions-seed.ts +1 -1
  24. package/src/core/config.ts +199 -0
  25. package/src/core/debt-format.ts +107 -0
  26. package/src/core/debt-types.ts +79 -0
  27. package/src/core/defaults.ts +8 -0
  28. package/src/core/enums.ts +34 -0
  29. package/src/core/format.ts +33 -0
  30. package/src/core/hint-log.ts +68 -0
  31. package/src/core/hook-helpers.ts +31 -0
  32. package/src/core/mem-log.ts +357 -0
  33. package/src/core/parse.ts +71 -0
  34. package/src/core/pricing.ts +217 -0
  35. package/src/core/safety.ts +18 -0
  36. package/src/core/types.ts +142 -0
  37. package/src/core/util.ts +105 -0
  38. package/src/db/store.ts +2 -2
  39. package/src/debt/cli.ts +1 -1
  40. package/src/debt/format.ts +2 -107
  41. package/src/debt/load.ts +1 -1
  42. package/src/debt/promotion.ts +1 -1
  43. package/src/debt/types.ts +14 -79
  44. package/src/digest/collect.ts +4 -3
  45. package/src/gate.ts +2 -2
  46. package/src/gates.ts +1 -1
  47. package/src/hook.ts +69 -1424
  48. package/src/init-mem.ts +1 -1
  49. package/src/init.ts +1 -1
  50. package/src/install/claude.ts +17 -0
  51. package/src/install/opencode.ts +114 -0
  52. package/src/install.ts +12 -5
  53. package/src/lint-baseline.ts +1 -2
  54. package/src/map.ts +29 -8
  55. package/src/mem/commands/plan.ts +70 -42
  56. package/src/mem/store.ts +5 -1
  57. package/src/memory.ts +24 -387
  58. package/src/parse.ts +9 -71
  59. package/src/price/fetch.ts +4 -16
  60. package/src/price/resolve.ts +12 -213
  61. package/src/report/cli.ts +3 -3
  62. package/src/safety.ts +2 -18
  63. package/src/seed/plan-seed.ts +8 -4
  64. package/src/seed/primitives.ts +1 -7
  65. package/src/session/types.ts +14 -128
  66. package/src/setup.ts +1 -1
  67. package/src/stats/data.ts +3 -3
  68. package/src/telemetry.ts +2 -2
  69. package/src/usage/cache.ts +1 -2
  70. package/src/usage/cli.ts +1 -1
  71. package/src/usage/scan.ts +2 -1
  72. package/src/util.ts +10 -93
  73. package/src/web/html.ts +2 -33
  74. package/src/db/defaults.ts +0 -34
  75. package/src/db/getters.ts +0 -35
  76. package/src/db/index.ts +0 -7
  77. package/src/db/load.ts +0 -57
  78. package/src/db/types.ts +0 -77
  79. package/src/mcp/types.ts +0 -54
  80. /package/src/{mcp → adapters/mcp}/tools/index.ts +0 -0
  81. /package/src/{mcp → adapters/mcp}/transport.ts +0 -0
@@ -1,213 +1,12 @@
1
- // src/price/resolve.ts — normalize model id + compute list-price equivalent
2
- //
3
- // Hard rule: an unmappable model must be unpriced, never silently counted as 0 (the main failure mode
4
- // of this feature) · free only applies to things that truly cost 0 (local / :free rows in the table)
5
-
6
- import type { ModelBreakdown, PassiveUsageResult } from "../session/types.js";
7
- import type { ModelRates, PriceTable } from "./fetch.js";
8
-
9
- export type PriceStatus = "priced" | "free" | "unpriced";
10
-
11
- export interface PriceResolution {
12
- status: PriceStatus;
13
- rates: ModelRates | null;
14
- }
15
-
16
- /** client prefix prepended to every model id — strip it, then look up again */
17
- const CLIENT_PREFIXES = ["openrouter/", "opencode-go/", "opencode/"];
18
-
19
- /** OpenRouter's trailing tier — :free is a genuinely free endpoint, :batch is a discount */
20
- function stripTier(id: string): string {
21
- return id.endsWith(":free") || id.endsWith(":batch")
22
- ? id.slice(0, id.lastIndexOf(":"))
23
- : id;
24
- }
25
-
26
- /**
27
- * Candidate ids ordered by specificity: exact first, broad later
28
- * (an exact match always wins — the bare slug is the broadest match and goes last)
29
- */
30
- export function candidateIds(provider: string, model: string): string[] {
31
- const full = provider ? `${provider}/${model}` : model;
32
- const out: string[] = [full];
33
- let rest = full;
34
- for (const p of CLIENT_PREFIXES) {
35
- if (rest.startsWith(p)) {
36
- rest = rest.slice(p.length);
37
- out.push(rest);
38
- break;
39
- }
40
- }
41
- // opencode appends -free to the slug of a free model (deepseek-v4-flash-free)
42
- if (rest.endsWith("-free")) out.push(rest.slice(0, -"-free".length));
43
- return out;
44
- }
45
-
46
- /**
47
- * Find rates for one model — null only for local, which skips the table
48
- * (local never had a price to begin with, it is not "not found")
49
- */
50
- function isLocalProvider(provider: string): boolean {
51
- return provider === "lmstudio_local";
52
- }
53
-
54
- export function resolvePrice(
55
- provider: string,
56
- model: string,
57
- table: PriceTable,
58
- ): PriceResolution {
59
- if (!model || model === "(no model id)" || model === "(unknown)")
60
- return { status: "unpriced", rates: null };
61
- if (isLocalProvider(provider)) return { status: "free", rates: null };
62
- // ends with :free or -free = used a free endpoint / the client's free model
63
- // the real price is 0 (not list price), whether or not OpenRouter maps it
64
- if (model.endsWith(":free") || model.endsWith("-free"))
65
- return { status: "free", rates: null };
66
- for (const id of candidateIds(provider, model)) {
67
- const rates = table.models[id] ?? table.models[stripTier(id)];
68
- if (rates) {
69
- // an all-zero rate row (:free / free model) = genuinely free, not unpriced
70
- if (
71
- rates.input === 0 &&
72
- rates.output === 0 &&
73
- rates.cacheRead === 0 &&
74
- (rates.cacheWrite ?? 0) === 0
75
- )
76
- return { status: "free", rates };
77
- return { status: "priced", rates };
78
- }
79
- }
80
- // zcode stores only the slug with no vendor (GLM-5.3-Flash) — compare the suffix / exact
81
- // case-insensitive match (exact, not fuzzy: the whole string must be equal in length)
82
- // the table side strips the tier (:free/:batch) before comparing — so a bare slug hits the :free
83
- // row that is truly 0 (e.g. ling-3.0-flash-fin) and becomes free, not unpriced
84
- const slug = stripTier(
85
- candidateIds(provider, model).at(-1) ?? "",
86
- ).toLowerCase();
87
- if (slug) {
88
- const ids = Object.keys(table.models).sort();
89
- for (const id of ids) {
90
- const rawSuffix = id.includes("/")
91
- ? id.slice(id.lastIndexOf("/") + 1)
92
- : id;
93
- if (stripTier(rawSuffix).toLowerCase() === slug) {
94
- const rates = table.models[id];
95
- if (
96
- rates.input === 0 &&
97
- rates.output === 0 &&
98
- rates.cacheRead === 0 &&
99
- (rates.cacheWrite ?? 0) === 0
100
- )
101
- return { status: "free", rates };
102
- return { status: "priced", rates };
103
- }
104
- }
105
- }
106
- return { status: "unpriced", rates: null };
107
- }
108
-
109
- export interface TokenCounts {
110
- input: number;
111
- cacheRead: number;
112
- cacheWrite: number;
113
- output: number;
114
- }
115
-
116
- /**
117
- * Pure costing: separate rates for input / cache-read / cache-write — never one rate for all
118
- *
119
- * reasoning is not multiplied separately: Anthropic-family thinking is already included in output
120
- * (the reader keeps it separately just for visibility); multiplying separately = double counting · cache_write absent from the table
121
- * → fall back to the input rate (writing cache costs more than or equal to a fresh read, never less)
122
- */
123
- export function calcCost(t: TokenCounts, rates: ModelRates): number {
124
- return (
125
- t.input * rates.input +
126
- t.cacheRead * rates.cacheRead +
127
- t.cacheWrite * (rates.cacheWrite ?? rates.input) +
128
- t.output * rates.output
129
- );
130
- }
131
-
132
- export interface ImputedModel {
133
- provider: string;
134
- model: string;
135
- session_count: number;
136
- tokens_input: number;
137
- tokens_output: number;
138
- tokens_cache_read: number;
139
- tokens_cache_write: number;
140
- status: PriceStatus;
141
- /** dollars at list-price — 0 when free/unpriced (check status, do not read the number alone) */
142
- imputed_cost: number;
143
- }
144
-
145
- export interface ImputeSummary {
146
- total_imputed: number;
147
- priced_sessions: number;
148
- free_sessions: number;
149
- unpriced_sessions: number;
150
- unpriced_tokens: number;
151
- by_model: ImputedModel[];
152
- }
153
-
154
- /**
155
- * Price an entire PassiveUsageResult — works on live results (stats/usage) or cache rows
156
- * (usage-web) alike, because it only takes tokens per model
157
- */
158
- export function imputeResult(
159
- result: PassiveUsageResult,
160
- table: PriceTable,
161
- ): ImputeSummary {
162
- const by_model: ImputedModel[] = [];
163
- let total_imputed = 0;
164
- let priced_sessions = 0;
165
- let free_sessions = 0;
166
- let unpriced_sessions = 0;
167
- let unpriced_tokens = 0;
168
- for (const m of result.by_model as ModelBreakdown[]) {
169
- const r = resolvePrice(m.provider, m.model, table);
170
- let cost = 0;
171
- if (r.status === "priced" && r.rates) {
172
- cost = calcCost(
173
- {
174
- input: m.tokens_input,
175
- cacheRead: m.tokens_cache_read,
176
- cacheWrite: m.tokens_cache_write,
177
- output: m.tokens_output,
178
- },
179
- r.rates,
180
- );
181
- total_imputed += cost;
182
- priced_sessions += m.session_count;
183
- } else if (r.status === "free") {
184
- free_sessions += m.session_count;
185
- } else {
186
- unpriced_sessions += m.session_count;
187
- unpriced_tokens +=
188
- m.tokens_input +
189
- m.tokens_output +
190
- m.tokens_cache_read +
191
- m.tokens_cache_write;
192
- }
193
- by_model.push({
194
- provider: m.provider,
195
- model: m.model,
196
- session_count: m.session_count,
197
- tokens_input: m.tokens_input,
198
- tokens_output: m.tokens_output,
199
- tokens_cache_read: m.tokens_cache_read,
200
- tokens_cache_write: m.tokens_cache_write,
201
- status: r.status,
202
- imputed_cost: cost,
203
- });
204
- }
205
- return {
206
- total_imputed,
207
- priced_sessions,
208
- free_sessions,
209
- unpriced_sessions,
210
- unpriced_tokens,
211
- by_model,
212
- };
213
- }
1
+ // shim — moved to src/core/pricing.ts
2
+ export {
3
+ calcCost,
4
+ candidateIds,
5
+ type ImputedModel,
6
+ type ImputeSummary,
7
+ imputeResult,
8
+ type PriceResolution,
9
+ type PriceStatus,
10
+ resolvePrice,
11
+ type TokenCounts,
12
+ } from "../core/pricing.js";
package/src/report/cli.ts CHANGED
@@ -3,9 +3,9 @@
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { writeFileSync } from "node:fs";
5
5
  import { dirname, resolve } from "node:path";
6
- import { loadConfig } from "../db/index.js";
7
- import { toolVerificationReport } from "../mcp/tools/report.js";
8
- import { parseToolResult } from "../mcp/types.js";
6
+ import { toolVerificationReport } from "../adapters/mcp/tools/report.js";
7
+ import { loadConfig } from "../core/config.js";
8
+ import { parseToolResult } from "../core/types.js";
9
9
  import { getStatsData } from "../stats/data.js";
10
10
  import { renderReportHtml } from "./render.js";
11
11
 
package/src/safety.ts CHANGED
@@ -1,18 +1,2 @@
1
- // src/safety.ts — deny-list for dangerous git commands.
2
- // Rule: assertSafe() must be called on every command before spawn, including
3
- // ones built from config templates (memory claim/close/add, evidence
4
- // collector commands, install's `claude mcp add`).
5
- // Patterns come from config.safety.deny (regex sources); built-in default
6
- // covers the 4 known-destructive git invocations.
7
-
8
- import { DEFAULT_SAFETY_DENY } from "./db/index.js";
9
-
10
- export function assertSafe(argv: string[], denySources?: string[]): void {
11
- const sources = denySources ?? DEFAULT_SAFETY_DENY;
12
- const joined = argv.join(" ");
13
- for (const src of sources) {
14
- if (new RegExp(src).test(joined)) {
15
- throw new Error(`refusing to run dangerous command: ${joined}`);
16
- }
17
- }
18
- }
1
+ // shim — moved to src/core/safety.ts
2
+ export { assertSafe } from "./core/safety.js";
@@ -36,10 +36,14 @@ import {
36
36
  import { basename, join, relative, resolve, sep } from "node:path";
37
37
  import { collectSourceFiles, isSkippedDir, SCAN_EXTS } from "../analyze.js";
38
38
  import { computeModelFit } from "../context/projectHealth.js";
39
- import { doneDir, planDir, specDir } from "../db/getters.js";
40
- import { CONFIG_FILENAME } from "../db/index.js";
41
- import { loadConfig } from "../db/load.js";
42
- import type { Config } from "../db/types.js";
39
+ import {
40
+ CONFIG_FILENAME,
41
+ type Config,
42
+ doneDir,
43
+ loadConfig,
44
+ planDir,
45
+ specDir,
46
+ } from "../core/config.js";
43
47
  import { extractExports } from "../map.js";
44
48
  import { readRecentMemDecisions } from "../memory.js";
45
49
  import { getStatsData } from "../stats/data.js";
@@ -53,13 +53,7 @@ export function gitValue(kind: string, value: string): string {
53
53
 
54
54
  // Keep the head, always say how much was cut — a silent cut is
55
55
  // indistinguishable from "that was everything".
56
- export function capLines(lines: string[], cap: number, what: string): string[] {
57
- if (lines.length <= cap) return lines;
58
- const rest = lines.length - (cap - 1);
59
- const kept = lines.slice(0, cap - 1);
60
- kept.push(`… +${rest} more ${what}`);
61
- return kept;
62
- }
56
+ export { capLines } from "../core/util.js";
63
57
 
64
58
  // --- Signature formatting ---
65
59
 
@@ -1,128 +1,14 @@
1
- // src/session/types.ts — shared types + constants for session usage providers
2
-
3
- export interface ModelBreakdown {
4
- provider: string;
5
- model: string;
6
- session_count: number;
7
- tokens_input: number;
8
- tokens_output: number;
9
- tokens_reasoning: number;
10
- tokens_cache_read: number;
11
- tokens_cache_write: number;
12
- cost: number;
13
- }
14
-
15
- export interface SessionDetail {
16
- session_id: string;
17
- model: string;
18
- steps: number;
19
- tools: Record<string, number>;
20
- }
21
-
22
- export interface ToolLatencyStat {
23
- count: number;
24
- avgMs: number;
25
- }
26
-
27
- export interface StepTimingSummary {
28
- /** step-finish row count (mirrors UsageDetail.steps for SQLite providers). */
29
- steps: number;
30
- /** Mean per-part duration from embedded data.time (row-timestamp fallback when absent). */
31
- avgStepMs: number | null;
32
- /** How many durations were actually measured (vs. steps with no time signal). */
33
- stepSamples: number;
34
- /** Mean per-step tokens — averages only, never summed (sums overlap). */
35
- avgStepInput: number | null;
36
- avgStepOutput: number | null;
37
- avgStepCost: number | null;
38
- /** Mean tool latency grouped by tool name (from data.state.time). */
39
- toolLatencyMsByType: Record<string, ToolLatencyStat>;
40
- note: string;
41
- }
42
-
43
- export interface StaleReadFile {
44
- file: string;
45
- /** How many distinct sessions read this file (diversity, not just count). */
46
- sessions: number;
47
- /** Total Read calls across those sessions. */
48
- reads: number;
49
- }
50
-
51
- export interface UsageDetail {
52
- /** Global tool-call counts across the filtered sessions (activity signal, not quality). */
53
- tool_breakdown: Record<string, number>;
54
- /** Context bytes per tool — size of tool_result content blocks. */
55
- bytes_by_tool?: Record<string, number>;
56
- /** Total step-finish parts across the filtered sessions. */
57
- steps: number;
58
- /** Per-session breakdown (SQL-aggregated, never raw part rows). */
59
- by_session: SessionDetail[];
60
- /**
61
- * Files read across 2+ sessions in this window and never edited/written —
62
- * candidates for pasting into CLAUDE.md instead of re-reading every
63
- * session. Top 20 by session count. Needs a wide `since` window (many
64
- * sessions) before the pattern means anything — noise below that.
65
- */
66
- stale_reads?: StaleReadFile[];
67
- /**
68
- * Step-token sums are NOT reported: per-step tokens overlap (each step
69
- * carries the full context window), so SUM(step tokens) >> session tokens.
70
- * Verified on real data — see testSessionDetailStepTokensNotSummed.
71
- */
72
- note: string;
73
- /** Per-step timing/token/latency signal — present only when detail:true was requested. */
74
- timing?: StepTimingSummary | null;
75
- }
76
-
77
- export interface PassiveUsageResult {
78
- total_tokens_input: number;
79
- total_tokens_output: number;
80
- total_tokens_reasoning: number;
81
- total_tokens_cache_read: number;
82
- total_tokens_cache_write: number;
83
- total_cost: number;
84
- session_count: number;
85
- by_model: ModelBreakdown[];
86
- /** Present only when detail:true was requested (additive, default absent). */
87
- detail?: UsageDetail | null;
88
- /** ZCode sessions, when available. Absent when ZCode DB not found. */
89
- zcode?: PassiveUsageResult | null;
90
- /** Claude Code sessions, when available. Absent when projects dir not found. */
91
- claude_code?: PassiveUsageResult | null;
92
- /**
93
- * Why this read came back short, when it did. Absent on success.
94
- *
95
- * Zeros with no `error` mean "no sessions matched"; zeros WITH an error mean
96
- * "could not read the client's log" — most often schema drift after the
97
- * client updated. The two used to be indistinguishable: a renamed column
98
- * made the query throw, the catch-all swallowed it, and the numbers just
99
- * quietly went missing.
100
- */
101
- error?: string;
102
- }
103
-
104
- export const EMPTY_RESULT: PassiveUsageResult = {
105
- total_tokens_input: 0,
106
- total_tokens_output: 0,
107
- total_tokens_reasoning: 0,
108
- total_tokens_cache_read: 0,
109
- total_tokens_cache_write: 0,
110
- total_cost: 0,
111
- session_count: 0,
112
- by_model: [],
113
- };
114
-
115
- /** Common shape every client reader implements — see registry.ts. */
116
- export type PassiveUsageReader = (
117
- worktree?: string,
118
- since?: number,
119
- until?: number,
120
- detail?: boolean,
121
- full?: boolean,
122
- ) => PassiveUsageResult;
123
-
124
- export const STEP_TOKENS_NOTE =
125
- "step tokens overlap (per-step context window) — SUM(step tokens) != session tokens; steps is a count only";
126
-
127
- export const TIMING_NOTE =
128
- "timing from embedded part fields only (data.time/data.state.time/step-finish tokens) with row-timestamp fallback; averages, never raw I/O";
1
+ // shim — moved to src/core/types.ts
2
+ export {
3
+ EMPTY_RESULT,
4
+ type ModelBreakdown,
5
+ type PassiveUsageReader,
6
+ type PassiveUsageResult,
7
+ type SessionDetail,
8
+ STEP_TOKENS_NOTE,
9
+ type StaleReadFile,
10
+ type StepTimingSummary,
11
+ TIMING_NOTE,
12
+ type ToolLatencyStat,
13
+ type UsageDetail,
14
+ } from "../core/types.js";
package/src/setup.ts CHANGED
@@ -6,7 +6,7 @@ import { existsSync, statSync, writeFileSync } from "node:fs";
6
6
  import { join, resolve } from "node:path";
7
7
  import { createInterface } from "node:readline";
8
8
  import { seedConventionsFile } from "./conventions-seed.js";
9
- import { CONFIG_FILENAME, FAPONY_DIR } from "./db/index.js";
9
+ import { CONFIG_FILENAME, FAPONY_DIR } from "./core/config.js";
10
10
  import { initProject } from "./init.js";
11
11
  import { isAffirmative } from "./util.js";
12
12
 
package/src/stats/data.ts CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  import { readdirSync } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { type Event, openDb, PLAN_DIR, type Run } from "../db/index.js";
6
- import { loadConfig } from "../db/load.js";
5
+ import { type Event, loadConfig, PLAN_DIR, type Run } from "../core/config.js";
6
+ import { REASON_CODES, REGIME_CODES } from "../core/enums.js";
7
+ import { openDb } from "../db/store.js";
7
8
  import { enrichGateWindows } from "../gates.js";
8
- import { REASON_CODES, REGIME_CODES } from "../mcp/types.js";
9
9
  import {
10
10
  isPassFamily,
11
11
  qualityScore,
package/src/telemetry.ts CHANGED
@@ -13,9 +13,9 @@ import {
13
13
  type Config,
14
14
  type Event,
15
15
  loadConfig,
16
- openDb,
17
16
  type Run,
18
- } from "./db/index.js";
17
+ } from "./core/config.js";
18
+ import { openDb } from "./db/store.js";
19
19
  import { enrichGateWindows } from "./gates.js";
20
20
  import { readPassiveUsage } from "./session/index.js";
21
21
  import { avg, minutesBetween } from "./util.js";
@@ -12,8 +12,7 @@ import {
12
12
  writeFileSync,
13
13
  } from "node:fs";
14
14
  import { dirname, join } from "node:path";
15
- import { faponyDir } from "../db/load.js";
16
- import type { Config } from "../db/types.js";
15
+ import { type Config, faponyDir } from "../core/config.js";
17
16
 
18
17
  const CACHE_FILENAME = "usage-cache.jsonl";
19
18
 
package/src/usage/cli.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Reads from usage-cache.jsonl (written by `fapony usage-scan`).
4
4
  // No live session scanning — serves static HTML.
5
5
 
6
- import { loadConfig } from "../db/index.js";
6
+ import { loadConfig } from "../core/config.js";
7
7
  import type { PassiveUsageResult } from "../session/types.js";
8
8
  import { type CacheEntry, cacheMeta, readCache } from "./cache.js";
9
9
  import { renderUsageHtml } from "./render.js";
package/src/usage/scan.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  // Per-worktree + global aggregate — the cache now carries a worktree dimension.
6
6
  // Progress bar on TTY, plain lines on pipe/CI.
7
7
 
8
- import { loadConfig, openDb } from "../db/index.js";
8
+ import { loadConfig } from "../core/config.js";
9
+ import { openDb } from "../db/store.js";
9
10
  import { CLIENTS } from "../session/index.js";
10
11
  import type { PassiveUsageResult } from "../session/types.js";
11
12
  import {
package/src/util.ts CHANGED
@@ -1,93 +1,10 @@
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
- /** Minutes between two ISO-like timestamps (space separator, UTC assumed). */
29
- export function minutesBetween(a: string, b: string): number {
30
- const t0 = new Date(`${a.replace(" ", "T")}Z`).getTime();
31
- const t1 = new Date(`${b.replace(" ", "T")}Z`).getTime();
32
- return (t1 - t0) / 60000;
33
- }
34
-
35
- /** Arithmetic mean of a numeric array. 0 for empty arrays. */
36
- export function avg(xs: number[]): number {
37
- return xs.length ? xs.reduce((s, x) => s + x, 0) / xs.length : 0;
38
- }
39
-
40
- /** True for "y"/"yes" (case-insensitive, trimmed) — the only affirmative answers. */
41
- export function isAffirmative(answer: string): boolean {
42
- const normalized = answer.trim().toLowerCase();
43
- return normalized === "y" || normalized === "yes";
44
- }
45
-
46
- // --- Recursive directory walker ---
47
-
48
- import { readdirSync } from "node:fs";
49
- import { join } from "node:path";
50
-
51
- export interface WalkDirOpts {
52
- /** Max recursion depth (default 4). */
53
- depth?: number;
54
- /** Skip node_modules (default true). */
55
- skipNodeModules?: boolean;
56
- /** Dot-directories to descend into (default []). Others starting with . are skipped. */
57
- includeDotDirs?: string[];
58
- /** Extra filter: return true to collect this dir. Omit to collect all reached dirs. */
59
- predicate?: (dirPath: string) => boolean;
60
- }
61
-
62
- /**
63
- * Recursively walk directories under `root`.
64
- * Returns absolute paths of dirs for which `predicate` returned true (or all
65
- * reached dirs when no predicate is given). Skips node_modules by default.
66
- */
67
- export function walkDir(root: string, opts?: WalkDirOpts): string[] {
68
- const maxDepth = opts?.depth ?? 4;
69
- const skipNm = opts?.skipNodeModules ?? true;
70
- const includeDot = new Set(opts?.includeDotDirs ?? []);
71
- const pred = opts?.predicate;
72
- const result: string[] = [];
73
-
74
- const walk = (dir: string, depth: number): void => {
75
- if (depth > maxDepth) return;
76
- if (!pred || pred(dir)) result.push(dir);
77
- try {
78
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
79
- if (!entry.isDirectory()) continue;
80
- if (entry.isSymbolicLink()) continue;
81
- if (skipNm && entry.name === "node_modules") continue;
82
- if (entry.name.startsWith(".") && !includeDot.has(entry.name)) {
83
- continue;
84
- }
85
- walk(join(dir, entry.name), depth + 1);
86
- }
87
- } catch {
88
- // ignore unreadable dirs
89
- }
90
- };
91
- walk(root, 0);
92
- return result;
93
- }
1
+ // shim — moved to src/core/util.ts
2
+ export {
3
+ avg,
4
+ fillPrompt,
5
+ isAffirmative,
6
+ minutesBetween,
7
+ templateArgs,
8
+ type WalkDirOpts,
9
+ walkDir,
10
+ } from "./core/util.js";
package/src/web/html.ts CHANGED
@@ -1,33 +1,2 @@
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
- `;
1
+ // shim — moved to src/core/format.ts
2
+ export { DARK_THEME_CSS, esc, TABLE_CSS } from "../core/format.js";