fapony 0.1.1 → 0.1.3
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 +22 -36
- package/images/logo.png +0 -0
- package/images/logo.webp +0 -0
- package/images/logo@400.webp +0 -0
- package/images/sample.webp +0 -0
- package/images/summary.webp +0 -0
- package/package.json +10 -8
- package/skill/plan-with-pony/SKILL.md +1 -1
- package/src/analyze.ts +9 -8
- package/src/conventions-seed.ts +10 -10
- package/src/db/index.ts +1 -1
- package/src/db/store.ts +4 -0
- package/src/debt.ts +26 -22
- package/src/digest/collect.ts +7 -7
- package/src/digest/html.ts +2 -2
- package/src/digest/text.ts +1 -1
- package/src/gate.ts +3 -3
- package/src/hook.ts +119 -21
- package/src/init-mem.ts +3 -3
- package/src/install/opencode.ts +95 -8
- package/src/install/types.ts +1 -1
- package/src/install.ts +5 -1
- package/src/lint-baseline.ts +9 -9
- package/src/mcp/tools/mem.ts +1 -1
- package/src/mcp/tools/stats.ts +2 -2
- package/src/mcp/tools/usage.ts +3 -3
- package/src/mcp/tools/verdict.ts +29 -16
- package/src/mcp/transport.ts +1 -1
- package/src/plan-seed.ts +23 -21
- package/src/price/fetch.ts +19 -19
- package/src/price/resolve.ts +24 -24
- package/src/review-seed.ts +1 -1
- package/src/stats/data.ts +8 -7
- package/src/stats/format.ts +6 -5
- package/src/usage/render.ts +6 -5
- package/templates/PLAN.md +1 -1
- package/templates/mem/commands/plan.ts +44 -44
- package/templates/mem/commands/read.ts +5 -5
- package/templates/mem/commands/rotate.ts +6 -6
- package/templates/mem/commands/selftest.ts +14 -14
- package/templates/mem/commands/write.ts +13 -13
- package/templates/mem/mem.ts +5 -5
- package/templates/mem/selectors.ts +18 -18
- package/templates/mem/store.ts +50 -50
package/src/plan-seed.ts
CHANGED
|
@@ -88,7 +88,7 @@ function scopeSourceFiles(root: string): string[] {
|
|
|
88
88
|
// is about to plan. Headers only (title + shipped date) — pulling the bodies
|
|
89
89
|
// in would recreate the reading task the plan exists to avoid.
|
|
90
90
|
function renderPriorArt(cwd: string, config: Config, roots: string[]): string {
|
|
91
|
-
const placeholder = "- _(agent
|
|
91
|
+
const placeholder = "- _(agent fills in)_";
|
|
92
92
|
const keys = roots
|
|
93
93
|
.map((r) => relative(cwd, r))
|
|
94
94
|
.filter((r) => r !== "" && r !== ".");
|
|
@@ -124,7 +124,7 @@ function renderPriorArt(cwd: string, config: Config, roots: string[]): string {
|
|
|
124
124
|
const shipped = content.match(/shipped\s+(\d{4}-\d{2}-\d{2})/)?.[1] ?? "";
|
|
125
125
|
hits.push({
|
|
126
126
|
shipped,
|
|
127
|
-
line: `- ✅
|
|
127
|
+
line: `- ✅ Already decided: [${n}](../${label}/${n}) — ${title}${shipped ? ` (shipped ${shipped})` : ""} \`(fapony plan-seed)\``,
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
130
|
}
|
|
@@ -137,7 +137,9 @@ function renderPriorArt(cwd: string, config: Config, roots: string[]): string {
|
|
|
137
137
|
);
|
|
138
138
|
const shown = hits.slice(0, MAX_PRIOR_ART).map((h) => h.line);
|
|
139
139
|
if (hits.length > MAX_PRIOR_ART) {
|
|
140
|
-
shown.push(
|
|
140
|
+
shown.push(
|
|
141
|
+
`- … +${hits.length - MAX_PRIOR_ART} more touching the same scope`,
|
|
142
|
+
);
|
|
141
143
|
}
|
|
142
144
|
shown.push(placeholder);
|
|
143
145
|
return shown.join("\n");
|
|
@@ -185,49 +187,49 @@ kind: unit
|
|
|
185
187
|
status: active
|
|
186
188
|
---
|
|
187
189
|
|
|
188
|
-
# PLAN-${name} — (agent
|
|
190
|
+
# PLAN-${name} — (agent fills in a title)
|
|
189
191
|
|
|
190
|
-
> **Status:** 🚧 in-progress · **Created:** (agent
|
|
192
|
+
> **Status:** 🚧 in-progress · **Created:** (agent fills in the date)
|
|
191
193
|
|
|
192
194
|
## TL;DR
|
|
193
|
-
- **What:** (agent
|
|
194
|
-
- **Order:** (agent
|
|
195
|
+
- **What:** (agent fills in) · **Why:** (agent fills in) · **Done when:** (agent fills in)
|
|
196
|
+
- **Order:** (agent fills in)
|
|
195
197
|
- **Progress:**
|
|
196
|
-
- [ ] chunk 1 — (agent
|
|
198
|
+
- [ ] chunk 1 — (agent fills in)
|
|
197
199
|
|
|
198
200
|
## Context (fapony)
|
|
199
201
|
${contextFapony}
|
|
200
202
|
|
|
201
203
|
## 1. Goal (why)
|
|
202
|
-
_(agent
|
|
204
|
+
_(agent fills in)_
|
|
203
205
|
|
|
204
206
|
## 2. Scope (do / don't do)
|
|
205
|
-
_(agent
|
|
207
|
+
_(agent fills in)_
|
|
206
208
|
|
|
207
209
|
## 3. Done criteria (how we know it's finished)
|
|
208
|
-
_(agent
|
|
210
|
+
_(agent fills in — must be verifiable)_
|
|
209
211
|
|
|
210
212
|
## 4. Constraints / Hard rules (must not violate)
|
|
211
|
-
_(agent
|
|
213
|
+
_(agent fills in)_
|
|
212
214
|
|
|
213
215
|
## 5. Risks & Escape hatches (if it fails)
|
|
214
|
-
_(agent
|
|
216
|
+
_(agent fills in)_
|
|
215
217
|
|
|
216
218
|
## 6. Steps (what in which order)
|
|
217
|
-
1. _(agent
|
|
219
|
+
1. _(agent fills in — each step must be verifiable)_
|
|
218
220
|
|
|
219
221
|
## 7. Examples
|
|
220
222
|
${
|
|
221
223
|
specLink
|
|
222
|
-
? `→ ${specLink} (
|
|
223
|
-
: "_(agent
|
|
224
|
+
? `→ ${specLink} (signatures live in the spec, not the plan)`
|
|
225
|
+
: "_(agent fills in — or add a SPEC with plan-seed --spec)_"
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
## 8. References
|
|
227
229
|
${priorArt}
|
|
228
230
|
|
|
229
231
|
## Context (agent)
|
|
230
|
-
_(slot
|
|
232
|
+
_(empty slot — the agent dumps its own graph/code-summary)_
|
|
231
233
|
`;
|
|
232
234
|
}
|
|
233
235
|
|
|
@@ -437,7 +439,7 @@ function specTemplate(
|
|
|
437
439
|
// undershot the cap by that many lines (the 18k-SPEC failure mode). Cap the
|
|
438
440
|
// index separately so the body's capLines has a bounded head to work with.
|
|
439
441
|
const fixedHead = [
|
|
440
|
-
`# SPEC-${name} — (agent
|
|
442
|
+
`# SPEC-${name} — (agent fills in a title)`,
|
|
441
443
|
"",
|
|
442
444
|
`> **Used by:** PLAN-${name} — signatures below come from a live source scan — re-seed after structural changes.`,
|
|
443
445
|
...(scopeEcho ? [`> **Scope:** ${scopeEcho}`] : []),
|
|
@@ -456,7 +458,7 @@ function specTemplate(
|
|
|
456
458
|
);
|
|
457
459
|
const head = [...fixedHead, ...cappedIndex, ""];
|
|
458
460
|
const tail = [
|
|
459
|
-
"## (agent
|
|
461
|
+
"## (agent fills in — wireframes / edge cases / API shapes the plan references)",
|
|
460
462
|
];
|
|
461
463
|
const bodyLines = chunks.flatMap((c) => [
|
|
462
464
|
`## <a id="${c.slug}"></a>${c.title}`,
|
|
@@ -557,7 +559,7 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
557
559
|
const planPath = join(planDirAbs, `PLAN-${name}.md`);
|
|
558
560
|
if (existsSync(planPath)) {
|
|
559
561
|
console.error(
|
|
560
|
-
`${planPath} already exists — not overwriting.
|
|
562
|
+
`${planPath} already exists — not overwriting. Use a new name, e.g. PLAN-${name}-v2`,
|
|
561
563
|
);
|
|
562
564
|
process.exit(1);
|
|
563
565
|
}
|
|
@@ -571,7 +573,7 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
571
573
|
const specPath = join(specDirAbs, `SPEC-${name}.md`);
|
|
572
574
|
if (existsSync(specPath)) {
|
|
573
575
|
console.error(
|
|
574
|
-
`${specPath} already exists — not overwriting.
|
|
576
|
+
`${specPath} already exists — not overwriting. Use a new name, e.g. SPEC-${name}-v2`,
|
|
575
577
|
);
|
|
576
578
|
process.exit(1);
|
|
577
579
|
}
|
package/src/price/fetch.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// src/price/fetch.ts —
|
|
1
|
+
// src/price/fetch.ts — fetch OpenRouter's price table + cache it as prices.json
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// FAPONY_STATE_DIR)
|
|
5
|
-
// refresh
|
|
6
|
-
// (
|
|
3
|
+
// prices are cache, not state: stored at ~/.config/fapony/prices.json (honors
|
|
4
|
+
// FAPONY_STATE_DIR) no new SQLite tables (DB Schema rule: 2 tables only)
|
|
5
|
+
// refresh happens only when someone runs `fapony price-scan` — a query never fetches on its own
|
|
6
|
+
// (same discipline as usage-scan) offline, read the existing cache
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
existsSync,
|
|
@@ -19,12 +19,12 @@ import type { Config } from "../db/types.js";
|
|
|
19
19
|
const PRICES_FILENAME = "prices.json";
|
|
20
20
|
const MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
21
21
|
|
|
22
|
-
/**
|
|
22
|
+
/** per-token rates (dollars) — every rate comes straight from OpenRouter, no guessing */
|
|
23
23
|
export interface ModelRates {
|
|
24
24
|
input: number;
|
|
25
25
|
output: number;
|
|
26
26
|
cacheRead: number;
|
|
27
|
-
/** null =
|
|
27
|
+
/** null = the table does not provide it → use the input rate instead (see calcCost) */
|
|
28
28
|
cacheWrite: number | null;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -37,7 +37,7 @@ export function pricesPath(config?: Config): string {
|
|
|
37
37
|
return join(faponyDir(config), PRICES_FILENAME);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/**
|
|
40
|
+
/** Read the cache — null when the file is missing or broken (caller shows "—" + hint) */
|
|
41
41
|
export function loadPrices(config?: Config): PriceTable | null {
|
|
42
42
|
const p = pricesPath(config);
|
|
43
43
|
if (!existsSync(p)) return null;
|
|
@@ -53,7 +53,7 @@ export function loadPrices(config?: Config): PriceTable | null {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/**
|
|
56
|
+
/** Write atomically (tmp + rename), create the state dir when absent */
|
|
57
57
|
export function writePrices(table: PriceTable, config?: Config): void {
|
|
58
58
|
const p = pricesPath(config);
|
|
59
59
|
mkdirSync(dirname(p), { recursive: true });
|
|
@@ -72,14 +72,14 @@ function toRate(v: unknown): number | null {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
75
|
+
* Turn OpenRouter's raw response into a rate table (pure — testable without hitting the network)
|
|
76
76
|
*
|
|
77
|
-
*
|
|
77
|
+
* Real fields (confirmed 2026-09-14, 445 models): pricing.prompt / .completion /
|
|
78
78
|
* .input_cache_read / .input_cache_write (optional) / .input_cache_write_1h
|
|
79
|
-
* (
|
|
80
|
-
*
|
|
81
|
-
* utc_days —
|
|
82
|
-
*
|
|
79
|
+
* (the 1h TTL rate of some models — the standard 5m rate suffices because logs do not report TTL)
|
|
80
|
+
* ignore pricing.web_search / pricing.overrides (tiers based on min_prompt_tokens /
|
|
81
|
+
* utc_days — use the base rate and state the limitation) · ids starting with ~ (alias) are dropped
|
|
82
|
+
* ids ending :free / :batch are kept as-is (they are their own price rows)
|
|
83
83
|
*/
|
|
84
84
|
export function parsePricesResponse(json: unknown): Record<string, ModelRates> {
|
|
85
85
|
const out: Record<string, ModelRates> = {};
|
|
@@ -102,8 +102,8 @@ export function parsePricesResponse(json: unknown): Record<string, ModelRates> {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
*
|
|
106
|
-
* response
|
|
105
|
+
* Merge the new table into the old cache — merge, not replace: an id missing from
|
|
106
|
+
* this response (an old model dropped from the table) must still be priced with its old rate
|
|
107
107
|
*/
|
|
108
108
|
export function mergePriceTables(
|
|
109
109
|
old: PriceTable | null,
|
|
@@ -115,7 +115,7 @@ export function mergePriceTables(
|
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
/**
|
|
118
|
+
/** Fetch the table from OpenRouter (public, no auth) — fetcher is injectable for tests */
|
|
119
119
|
export async function fetchPriceTable(
|
|
120
120
|
fetcher: typeof fetch = fetch,
|
|
121
121
|
): Promise<Record<string, ModelRates>> {
|
|
@@ -134,7 +134,7 @@ export async function cmdPriceScan(rawArgs: string[]): Promise<void> {
|
|
|
134
134
|
fresh = await fetchPriceTable();
|
|
135
135
|
} catch (err) {
|
|
136
136
|
console.error(
|
|
137
|
-
`fapony price-scan: fetch failed (${String(err)}) — cache
|
|
137
|
+
`fapony price-scan: fetch failed (${String(err)}) — the existing cache is still here, use the prices you have`,
|
|
138
138
|
);
|
|
139
139
|
process.exit(1);
|
|
140
140
|
}
|
package/src/price/resolve.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// src/price/resolve.ts — normalize model id +
|
|
1
|
+
// src/price/resolve.ts — normalize model id + compute list-price equivalent
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
5
|
|
|
6
6
|
import type { ModelBreakdown, PassiveUsageResult } from "../session/types.js";
|
|
7
7
|
import type { ModelRates, PriceTable } from "./fetch.js";
|
|
@@ -13,10 +13,10 @@ export interface PriceResolution {
|
|
|
13
13
|
rates: ModelRates | null;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/** prefix
|
|
16
|
+
/** client prefix prepended to every model id — strip it, then look up again */
|
|
17
17
|
const CLIENT_PREFIXES = ["openrouter/", "opencode-go/", "opencode/"];
|
|
18
18
|
|
|
19
|
-
/**
|
|
19
|
+
/** OpenRouter's trailing tier — :free is a genuinely free endpoint, :batch is a discount */
|
|
20
20
|
function stripTier(id: string): string {
|
|
21
21
|
return id.endsWith(":free") || id.endsWith(":batch")
|
|
22
22
|
? id.slice(0, id.lastIndexOf(":"))
|
|
@@ -24,8 +24,8 @@ function stripTier(id: string): string {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
* (
|
|
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
29
|
*/
|
|
30
30
|
export function candidateIds(provider: string, model: string): string[] {
|
|
31
31
|
const full = provider ? `${provider}/${model}` : model;
|
|
@@ -38,14 +38,14 @@ export function candidateIds(provider: string, model: string): string[] {
|
|
|
38
38
|
break;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
// opencode
|
|
41
|
+
// opencode appends -free to the slug of a free model (deepseek-v4-flash-free)
|
|
42
42
|
if (rest.endsWith("-free")) out.push(rest.slice(0, -"-free".length));
|
|
43
43
|
return out;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
48
|
-
* (local
|
|
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
49
|
*/
|
|
50
50
|
function isLocalProvider(provider: string): boolean {
|
|
51
51
|
return provider === "lmstudio_local";
|
|
@@ -59,14 +59,14 @@ export function resolvePrice(
|
|
|
59
59
|
if (!model || model === "(no model id)" || model === "(unknown)")
|
|
60
60
|
return { status: "unpriced", rates: null };
|
|
61
61
|
if (isLocalProvider(provider)) return { status: "free", rates: null };
|
|
62
|
-
//
|
|
63
|
-
//
|
|
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
64
|
if (model.endsWith(":free") || model.endsWith("-free"))
|
|
65
65
|
return { status: "free", rates: null };
|
|
66
66
|
for (const id of candidateIds(provider, model)) {
|
|
67
67
|
const rates = table.models[id] ?? table.models[stripTier(id)];
|
|
68
68
|
if (rates) {
|
|
69
|
-
//
|
|
69
|
+
// an all-zero rate row (:free / free model) = genuinely free, not unpriced
|
|
70
70
|
if (
|
|
71
71
|
rates.input === 0 &&
|
|
72
72
|
rates.output === 0 &&
|
|
@@ -77,10 +77,10 @@ export function resolvePrice(
|
|
|
77
77
|
return { status: "priced", rates };
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
// zcode
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
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
84
|
const slug = stripTier(
|
|
85
85
|
candidateIds(provider, model).at(-1) ?? "",
|
|
86
86
|
).toLowerCase();
|
|
@@ -114,11 +114,11 @@ export interface TokenCounts {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
117
|
+
* Pure costing: separate rates for input / cache-read / cache-write — never one rate for all
|
|
118
118
|
*
|
|
119
|
-
* reasoning
|
|
120
|
-
* (reader
|
|
121
|
-
* →
|
|
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
122
|
*/
|
|
123
123
|
export function calcCost(t: TokenCounts, rates: ModelRates): number {
|
|
124
124
|
return (
|
|
@@ -138,7 +138,7 @@ export interface ImputedModel {
|
|
|
138
138
|
tokens_cache_read: number;
|
|
139
139
|
tokens_cache_write: number;
|
|
140
140
|
status: PriceStatus;
|
|
141
|
-
/**
|
|
141
|
+
/** dollars at list-price — 0 when free/unpriced (check status, do not read the number alone) */
|
|
142
142
|
imputed_cost: number;
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -152,8 +152,8 @@ export interface ImputeSummary {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
|
-
*
|
|
156
|
-
* (usage-web)
|
|
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
157
|
*/
|
|
158
158
|
export function imputeResult(
|
|
159
159
|
result: PassiveUsageResult,
|
package/src/review-seed.ts
CHANGED
|
@@ -544,7 +544,7 @@ function hasDynamicDispatch(absFile: string): boolean {
|
|
|
544
544
|
|
|
545
545
|
// --- --body: declaration slice (indent-out, no parser) ---
|
|
546
546
|
// extractBody lives in map.ts beside extractExports — the conventions seeder
|
|
547
|
-
// reuses the same slice for wrapper detection (one implementation,
|
|
547
|
+
// reuses the same slice for wrapper detection (one implementation, rule 1).
|
|
548
548
|
|
|
549
549
|
// --- --callers: symbol→symbol over importer files (identifier scan) ---
|
|
550
550
|
|
package/src/stats/data.ts
CHANGED
|
@@ -622,15 +622,16 @@ function addSessionTokens(
|
|
|
622
622
|
}
|
|
623
623
|
|
|
624
624
|
/**
|
|
625
|
-
* tokens/pass — retry tax
|
|
625
|
+
* tokens/pass — the visible retry tax (SPEC-cost-per-pass).
|
|
626
626
|
*
|
|
627
|
-
*
|
|
628
|
-
* "
|
|
629
|
-
*
|
|
627
|
+
* The divisor is a pass-family gate, not a run: bucket is already a gate, and
|
|
628
|
+
* work "finished" is a gate that passed · token is a per-session total (deduped),
|
|
629
|
+
* not per gate — one session can emit several gates, so summing per gate would
|
|
630
|
+
* multiply unevenly across models.
|
|
630
631
|
*
|
|
631
|
-
* `passes=0`
|
|
632
|
-
*
|
|
633
|
-
* (tokensInput
|
|
632
|
+
* `passes=0` or total token 0 → null (not Infinity/NaN/0): "unmeasurable" is not
|
|
633
|
+
* "free" and never divide by zero · no cost/pass in v1 — no cache split
|
|
634
|
+
* (tokensInput is fresh+cache_read+cache_write), so price needs a guess.
|
|
634
635
|
*/
|
|
635
636
|
function tokensPerPass(
|
|
636
637
|
passes: number,
|
package/src/stats/format.ts
CHANGED
|
@@ -51,9 +51,10 @@ function modelLine(
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
54
|
+
* list-price equivalent line appended to each usage section — the one unit
|
|
55
|
+
* that compares across clients (clients that do not record cost get a price
|
|
56
|
+
* attached here). List price is not money paid · unpriced is broken out
|
|
57
|
+
* separately, never folded into 0.
|
|
57
58
|
*/
|
|
58
59
|
function imputedLines(
|
|
59
60
|
result: PassiveUsageResult,
|
|
@@ -103,8 +104,8 @@ export function formatStatsText(data: StatsData): string {
|
|
|
103
104
|
if (data.runs.total === 0) return "no runs yet";
|
|
104
105
|
|
|
105
106
|
const lines: string[] = [];
|
|
106
|
-
//
|
|
107
|
-
//
|
|
107
|
+
// list price from cache only — the query never fetches itself (works
|
|
108
|
+
// offline, missing file = shows — + hint, never throws)
|
|
108
109
|
const prices = loadPrices();
|
|
109
110
|
|
|
110
111
|
if (data.scope) {
|
package/src/usage/render.ts
CHANGED
|
@@ -304,8 +304,9 @@ function shareSection(
|
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
/**
|
|
307
|
-
*
|
|
308
|
-
*
|
|
307
|
+
* Imputes list prices for rows where the client records no cost (0) — rows
|
|
308
|
+
* that already have a real price are left alone · returns a view to render
|
|
309
|
+
* plus a note for below the table (list-price label everywhere)
|
|
309
310
|
*/
|
|
310
311
|
function withImputed(
|
|
311
312
|
data: PassiveUsageResult | null,
|
|
@@ -332,7 +333,7 @@ function withImputed(
|
|
|
332
333
|
tokens_reasoning: real?.tokens_reasoning ?? 0,
|
|
333
334
|
tokens_cache_read: m.tokens_cache_read,
|
|
334
335
|
tokens_cache_write: m.tokens_cache_write,
|
|
335
|
-
//
|
|
336
|
+
// combine with the real per-model cost (OpenCode records its own) — whichever has a value wins
|
|
336
337
|
cost: real && real.cost > 0 ? real.cost : m.imputed_cost,
|
|
337
338
|
};
|
|
338
339
|
});
|
|
@@ -363,7 +364,7 @@ export function renderUsageHtml(
|
|
|
363
364
|
.filter((k) => k !== "__global__")
|
|
364
365
|
.sort();
|
|
365
366
|
|
|
366
|
-
//
|
|
367
|
+
// list price from cache only — serving never fetches itself (works offline)
|
|
367
368
|
const table = prices === undefined ? loadPrices() : prices;
|
|
368
369
|
|
|
369
370
|
const owner = ownerName?.trim() ? esc(ownerName.trim()) : "";
|
|
@@ -395,7 +396,7 @@ export function renderUsageHtml(
|
|
|
395
396
|
|
|
396
397
|
const heading = isGlobal ? "All projects" : shortWt(label);
|
|
397
398
|
|
|
398
|
-
//
|
|
399
|
+
// a card with no sessions has nothing to show — hide it instead of showing "no sessions"
|
|
399
400
|
const cards = [
|
|
400
401
|
["OpenCode", "var(--green)", pOc],
|
|
401
402
|
["ZCode", "var(--accent)", pZc],
|
package/templates/PLAN.md
CHANGED
|
@@ -24,7 +24,7 @@ spec: SPEC-<feature>.md # if any
|
|
|
24
24
|
- **Done when:** one line, testable
|
|
25
25
|
- **Order:** what this waits on / what it unblocks (mirrors the frontmatter)
|
|
26
26
|
- **Progress:**
|
|
27
|
-
- [x] chunk 1 — <what landed> `<short sha>` <YYYY-MM-DD>
|
|
27
|
+
- [x] chunk 1 — <what landed> `<short sha>` <YYYY-MM-DD> · verdict: <grade>
|
|
28
28
|
- [ ] chunk 2 — <what is next>
|
|
29
29
|
|
|
30
30
|
---
|