@gonrocca/zero-pi 0.1.55 → 0.1.57

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 CHANGED
@@ -76,6 +76,7 @@ into `/forge` for you.
76
76
  | **Strict TDD** | The build phase drives RED → GREEN → TRIANGULATE → REFACTOR with a TDD Cycle Evidence table; veredicto audits it. On by default, runtime-gated on a test runner; `tdd.mode: "off"` disables it. |
77
77
  | **`/zero-models`** | Pick the model + provider + thinking level for each SDD phase — a boxed-window picker, or set one directly. |
78
78
  | **Autotune** | Learns which model fits each phase from your run history and re-tunes itself. |
79
+ | **`/zero-cost`** | Aggregates a run's sub-agent `meta.json` files into a per-phase token/cost/duration report — no schema change, reads what pi already writes. |
79
80
  | **`/zero-sync` / `/zero-archive`** | Folds each run's spec delta into a canonical, project-wide spec store and archives approved runs. |
80
81
  | **Git / PR / Issues** | `/zero-branch`, `/zero-git-validate`, `/zero-pr`, and `/zero-issue` keep branches and GitHub links audit-ready. |
81
82
  | **Run memory** | Every run recalls and saves traces to Cortex, so runs learn from each other. |
@@ -86,6 +87,7 @@ into `/forge` for you.
86
87
  | **Windows tree-kill** | Aborting a turn kills the whole process tree — no orphaned `claude`. |
87
88
  | **Skill auto-learning** | Distills reusable skills from substantial tasks and surfaces them later. |
88
89
  | **`zero-sdd` theme** | A dark, high-contrast pi theme tuned for SDD work. |
90
+ | **`zero-sunset` theme** | A warm sunset variant — gold/coral/magenta accents over warm-dark panels, with one cool tone kept for syntax legibility. Activate with `/theme zero-sunset`. |
89
91
 
90
92
  ## ⌨️ Commands
91
93
 
@@ -97,6 +99,7 @@ into `/forge` for you.
97
99
  | `/zero-archive <slug>` | Merge an approved run into `.sdd/specs/`, move it to `.sdd/archive/YYYY-MM-DD-<slug>/`, and persist `archivePath`. |
98
100
  | `/zero-validate <slug>` | Validate proposal/spec/design/tasks artifacts, including task schema and per-domain specs. |
99
101
  | `/zero-status` | Show each `.sdd/` run's artifact, sync, latest-verdict, and GitHub-link status. |
102
+ | `/zero-cost [<slug>]` | Report tokens (in/out/cache), USD cost, duration, and tool-count per SDD phase for a run — `<slug>` for a specific run, no argument for the most recent. |
100
103
  | `/zero-branch <slug>` | Create/reuse the configured SDD Git branch and persist `branch`/`baseBranch`. |
101
104
  | `/zero-git-validate <slug>` | Check worktree, branch, remote, `gh auth`, and verdict gating before PR/archive. |
102
105
  | `/zero-pr <slug>` | Create an audit-ready GitHub PR from a run that already has verdict `pasa`. |
@@ -0,0 +1,14 @@
1
+ // Pure helper: compact token count to a short human string.
2
+ // 1234 → "1.2k", 4_695_040 → "4.7M", 999 → "999".
3
+ //
4
+ // Intentionally separate from `formatTokenCount` in zero-statusline.ts,
5
+ // which uses a different contract; this helper requires lowercase `k`,
6
+ // uppercase `M`, and uppercase `B` suffixes.
7
+
8
+ export function formatTokens(n: number): string {
9
+ if (!Number.isFinite(n) || n < 0) return "0";
10
+ if (n < 1000) return `${Math.floor(n)}`;
11
+ if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`;
12
+ if (n < 1_000_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
13
+ return `${(n / 1_000_000_000).toFixed(1)}B`;
14
+ }
@@ -1,8 +1,9 @@
1
1
  // zero-pi — static ZERO SDD startup banner.
2
2
  //
3
3
  // Renders the ZERO wordmark ONCE, at extension load, as an "ANSI Shadow" 3D
4
- // block in ceroclawd.com violetdeep-violet faces, a lit top edge, dark
5
- // shadow strokes and a cast shadow for depth.
4
+ // block in a sunset gradient gold at the lit top edge melting down through
5
+ // coral and magenta to a ceroclawd-violet base, with dark shadow strokes and
6
+ // a cast shadow for depth.
6
7
  //
7
8
  // It writes a single block to stdout before pi's UI takes over. There is
8
9
  // deliberately NO setHeader and NO animation timer: an animated header that
@@ -29,14 +30,23 @@ const FONT: Record<string, string[]> = {
29
30
  " ": [" ", " ", " ", " ", " ", " "],
30
31
  };
31
32
 
32
- // ceroclawd.com palette — violet, glow and darkness.
33
- const VIOLET_DEEP: RGB = [124, 58, 237];
34
- const LAVENDER: RGB = [205, 188, 255];
35
- const PEAK: RGB = [248, 244, 255];
36
- const SHADOW: RGB = [38, 24, 66];
37
- const INK: RGB = [20, 13, 34];
38
- const VIOLET: RGB = [167, 139, 250];
39
- const MUTED: RGB = [120, 110, 150];
33
+ // Sunset palette — a warm sky gradient (gold → peach → coral → rose → magenta
34
+ // violet) for the letter faces, plus glow, shadow and darkness. The violet
35
+ // base keeps the ZERO wordmark ending on the ceroclawd brand colour.
36
+ const SKY: RGB[] = [
37
+ [255, 214, 130], // gold
38
+ [255, 168, 99], // peach
39
+ [255, 124, 92], // coral
40
+ [255, 92, 122], // rose
41
+ [214, 74, 140], // magenta
42
+ [142, 59, 158], // violet
43
+ ];
44
+ const PEAK: RGB = [255, 244, 224];
45
+ const SHADOW: RGB = [46, 22, 54];
46
+ const INK: RGB = [22, 12, 30];
47
+ const CORAL: RGB = [255, 124, 92];
48
+ const PEACH: RGB = [255, 168, 99];
49
+ const MUTED: RGB = [150, 120, 130];
40
50
 
41
51
  const ANSI_RE = /\x1b\[[0-9;]*m/g;
42
52
 
@@ -57,6 +67,14 @@ function mix(a: RGB, b: RGB, t: number): RGB {
57
67
  ];
58
68
  }
59
69
 
70
+ /** Sample a multi-stop colour ramp at t∈[0,1] — the sunset sky, top to base. */
71
+ function ramp(stops: RGB[], t: number): RGB {
72
+ const k = Math.max(0, Math.min(1, t));
73
+ const seg = k * (stops.length - 1);
74
+ const i = Math.min(stops.length - 2, Math.floor(seg));
75
+ return mix(stops[i], stops[i + 1], seg - i);
76
+ }
77
+
60
78
  /** Printable width of a string, ignoring ANSI colour escapes. */
61
79
  export function visibleWidth(text: string): number {
62
80
  return text.replace(ANSI_RE, "").length;
@@ -76,17 +94,17 @@ function matrixFor(text: string): { rows: string[]; width: number } {
76
94
  return { rows, width: rows[0]?.length ?? 0 };
77
95
  }
78
96
 
79
- /** Front-face colour: violet vertical gradient with a lit top edge. */
97
+ /** Front-face colour: sunset vertical gradient with a lit top edge. */
80
98
  function faceColor(row: number, topEdge: boolean): RGB {
81
- const vt = Math.pow(row / (ROWS - 1), 0.85);
82
- const base = mix(LAVENDER, VIOLET_DEEP, vt);
83
- return topEdge ? mix(base, PEAK, 0.5) : base;
99
+ const vt = Math.pow(row / (ROWS - 1), 0.82);
100
+ const base = ramp(SKY, vt);
101
+ return topEdge ? mix(base, PEAK, 0.55) : base;
84
102
  }
85
103
 
86
104
  /** Extrusion side: a darker, shaded version of the face it belongs to. */
87
105
  function sideColor(row: number): RGB {
88
- const vt = Math.pow(row / (ROWS - 1), 0.85);
89
- return mix(mix(LAVENDER, VIOLET_DEEP, vt), SHADOW, 0.66);
106
+ const vt = Math.pow(row / (ROWS - 1), 0.82);
107
+ return mix(ramp(SKY, vt), SHADOW, 0.62);
90
108
  }
91
109
 
92
110
  function renderLogo(width: number): string[] {
@@ -114,13 +132,13 @@ function renderLogo(width: number): string[] {
114
132
  return lines;
115
133
  }
116
134
 
117
- /** A thin violet rule, brightest in the middle. */
135
+ /** A thin sunset rule, brightest in the middle. */
118
136
  function ornament(width: number): string {
119
137
  const length = Math.min(46, Math.max(22, Math.floor(width * 0.5)));
120
138
  let line = "";
121
139
  for (let i = 0; i < length; i++) {
122
140
  const t = i / Math.max(1, length - 1);
123
- line += fg(mix(VIOLET_DEEP, VIOLET, Math.sin(t * Math.PI)), "─");
141
+ line += fg(mix(CORAL, PEACH, Math.sin(t * Math.PI)), "─");
124
142
  }
125
143
  return center(line, width);
126
144
  }
@@ -136,9 +154,9 @@ function ornament(width: number): string {
136
154
  */
137
155
  export function bannerBlock(width: number): string[] {
138
156
  if (width < 64) {
139
- return [center(fg(VIOLET, "ZERO SDD"), width), center(fg(MUTED, "pi.dev · spec-driven work"), width)];
157
+ return [center(fg(PEACH, "ZERO SDD"), width), center(fg(MUTED, "pi.dev · spec-driven work"), width)];
140
158
  }
141
- const tag = fg(VIOLET, "ZERO SDD") + fg(MUTED, " explore → plan → build → veredicto");
159
+ const tag = fg(PEACH, "ZERO SDD") + fg(MUTED, " explore → plan → build → veredicto");
142
160
  return [ornament(width), ...renderLogo(width), center(tag, width), ornament(width)];
143
161
  }
144
162
 
@@ -0,0 +1,59 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { parseMeta, selectRunMetas, aggregateRun, formatReport, type PhaseMeta } from "./zero-cost.ts";
6
+
7
+ type NotifyType = "info" | "warning" | "error";
8
+ interface PiCommandContext { ui: { notify(message: string, type?: NotifyType): void } }
9
+ interface PiExtensionAPI { registerCommand(name: string, options: { description?: string; handler: (args: string, ctx: PiCommandContext) => void | Promise<void> }): void }
10
+
11
+ /** Read every sub-agent `meta.json` across all pi session dirs and parse the
12
+ * zero-<phase> ones. Scanning every session keeps us decoupled from pi's
13
+ * cwd-encoding scheme; selection by slug/timestamp narrows to one run. */
14
+ function readAllPhaseMetas(): PhaseMeta[] {
15
+ const root = join(homedir(), ".pi", "agent", "sessions");
16
+ if (!existsSync(root)) return [];
17
+ const out: PhaseMeta[] = [];
18
+ for (const session of readdirSync(root, { withFileTypes: true })) {
19
+ if (!session.isDirectory()) continue;
20
+ const artifacts = join(root, session.name, "subagent-artifacts");
21
+ if (!existsSync(artifacts)) continue;
22
+ for (const f of readdirSync(artifacts)) {
23
+ if (!f.endsWith("_meta.json")) continue;
24
+ try {
25
+ const meta = parseMeta(JSON.parse(readFileSync(join(artifacts, f), "utf8")));
26
+ if (meta) out.push(meta);
27
+ } catch { /* skip unreadable / malformed meta */ }
28
+ }
29
+ }
30
+ return out;
31
+ }
32
+
33
+ function runCost(args: string, ctx: PiCommandContext): void {
34
+ const notify = (m: string, t?: NotifyType) => { try { ctx.ui.notify(m, t); } catch {} };
35
+ const slug = args.trim() || null;
36
+ const metas = readAllPhaseMetas();
37
+ const selected = selectRunMetas(metas, slug);
38
+ if (selected.length === 0) {
39
+ notify(
40
+ slug
41
+ ? `zero-cost: no encontré datos de costo para "${slug}".`
42
+ : "zero-cost: no encontré runs con datos de costo todavía.",
43
+ "info",
44
+ );
45
+ return;
46
+ }
47
+ notify(formatReport(aggregateRun(selected, slug ?? selected[0]?.slug ?? null)), "info");
48
+ }
49
+
50
+ export default function register(pi?: PiExtensionAPI): void {
51
+ if (!pi || typeof pi.registerCommand !== "function") return;
52
+ pi.registerCommand("zero-cost", {
53
+ description: "Reporta tokens, costo y duración por fase de un run SDD (slug opcional; sin slug = el más reciente)",
54
+ handler: (args: string, ctx: PiCommandContext): void => {
55
+ try { if (ctx?.ui?.notify) runCost(args ?? "", ctx); }
56
+ catch (err) { try { ctx.ui.notify(`zero-cost: ${err instanceof Error ? err.message : String(err)}`, "error"); } catch {} }
57
+ },
58
+ });
59
+ }
@@ -0,0 +1,220 @@
1
+ // zero-pi — per-run cost/usage report, pure-logic module.
2
+ //
3
+ // A `/forge` run delegates each phase to a sub-agent that writes a
4
+ // `*_meta.json` under `~/.pi/agent/sessions/<session>/subagent-artifacts/`
5
+ // carrying real `usage` (tokens + cost), `model`, `durationMs` and
6
+ // `toolCount`. `~/.pi/zero-runs.jsonl` records the verdict/rounds/model but
7
+ // never cost — so there is no aggregate view of what a run cost, by phase.
8
+ //
9
+ // This module turns those raw meta records into a per-phase + total report.
10
+ // Every decision (phase mapping, slug extraction, selection, aggregation,
11
+ // formatting) lives here as plain, dependency-free TypeScript so it is
12
+ // testable in isolation. The pi wiring lives in `zero-cost-extension.ts`.
13
+
14
+ import { formatTokens } from "./format-tokens.ts";
15
+
16
+ /** The SDD phases a run is composed of, in pipeline order. */
17
+ export const COST_PHASES = ["explore", "plan", "build", "veredicto"] as const;
18
+ export type CostPhase = (typeof COST_PHASES)[number];
19
+
20
+ /** Token + cost usage of a single sub-agent run. */
21
+ export interface PhaseUsage {
22
+ input: number;
23
+ output: number;
24
+ cacheRead: number;
25
+ cacheWrite: number;
26
+ cost: number;
27
+ turns: number;
28
+ }
29
+
30
+ /** One normalized sub-agent `meta.json` belonging to a phase. */
31
+ export interface PhaseMeta {
32
+ runId: string;
33
+ phase: CostPhase;
34
+ /** Feature slug extracted from the sub-agent task, or `null`. */
35
+ slug: string | null;
36
+ model: string;
37
+ usage: PhaseUsage;
38
+ durationMs: number;
39
+ toolCount: number;
40
+ /** Epoch ms the meta was written (newest wins for selection/model). */
41
+ timestamp: number;
42
+ }
43
+
44
+ /** One phase row: summed usage of every sub-agent that ran that phase. */
45
+ export interface PhaseAggregate extends PhaseUsage {
46
+ phase: CostPhase;
47
+ model: string;
48
+ subAgents: number;
49
+ durationMs: number;
50
+ toolCount: number;
51
+ }
52
+
53
+ /** The aggregated cost of a whole run. */
54
+ export interface RunCost {
55
+ slug: string | null;
56
+ phases: PhaseAggregate[];
57
+ total: PhaseUsage & { durationMs: number; toolCount: number; subAgents: number };
58
+ }
59
+
60
+ const PHASE_INDEX: Record<CostPhase, number> = { explore: 0, plan: 1, build: 2, veredicto: 3 };
61
+
62
+ /** Map a sub-agent name `zero-<phase>` to its phase, or `null`. */
63
+ export function phaseFromAgent(agent: unknown): CostPhase | null {
64
+ if (typeof agent !== "string") return null;
65
+ const m = /^zero-(explore|plan|build|veredicto)$/.exec(agent);
66
+ return m ? (m[1] as CostPhase) : null;
67
+ }
68
+
69
+ /** Extract the feature slug from the first `.sdd/<slug>/` in a task string.
70
+ * The internal `specs` and `archive` directories are never slugs. */
71
+ export function extractSlug(task: unknown): string | null {
72
+ if (typeof task !== "string") return null;
73
+ const re = /\.sdd\/([A-Za-z0-9._-]+)\//g;
74
+ let m: RegExpExecArray | null;
75
+ while ((m = re.exec(task)) !== null) {
76
+ const slug = m[1];
77
+ if (slug && slug !== "specs" && slug !== "archive") return slug;
78
+ }
79
+ return null;
80
+ }
81
+
82
+ function num(v: unknown): number {
83
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
84
+ }
85
+
86
+ function asUsage(raw: unknown): PhaseUsage | null {
87
+ if (!raw || typeof raw !== "object") return null;
88
+ const u = raw as Record<string, unknown>;
89
+ // A usage object must carry at least the token fields as numbers.
90
+ if (typeof u.input !== "number" && typeof u.output !== "number") return null;
91
+ return {
92
+ input: num(u.input),
93
+ output: num(u.output),
94
+ cacheRead: num(u.cacheRead),
95
+ cacheWrite: num(u.cacheWrite),
96
+ cost: num(u.cost),
97
+ turns: num(u.turns),
98
+ };
99
+ }
100
+
101
+ /** Parse one raw `meta.json` object into a `PhaseMeta`, or `null` if it is not
102
+ * a zero-<phase> sub-agent or lacks a usage object. */
103
+ export function parseMeta(raw: unknown): PhaseMeta | null {
104
+ if (!raw || typeof raw !== "object") return null;
105
+ const r = raw as Record<string, unknown>;
106
+ const phase = phaseFromAgent(r.agent);
107
+ if (!phase) return null;
108
+ const usage = asUsage(r.usage);
109
+ if (!usage) return null;
110
+ return {
111
+ runId: typeof r.runId === "string" ? r.runId : "",
112
+ phase,
113
+ slug: extractSlug(r.task),
114
+ model: typeof r.model === "string" ? r.model : "",
115
+ usage,
116
+ durationMs: num(r.durationMs),
117
+ toolCount: num(r.toolCount),
118
+ timestamp: num(r.timestamp),
119
+ };
120
+ }
121
+
122
+ /** Select the phase-metas for one run: the given slug, or — by default — the
123
+ * slug whose newest meta has the greatest timestamp. Slug-less metas are
124
+ * ignored for the default pick. */
125
+ export function selectRunMetas(metas: readonly PhaseMeta[], slug?: string | null): PhaseMeta[] {
126
+ if (slug) return metas.filter((m) => m.slug === slug);
127
+ let bestSlug: string | null = null;
128
+ let bestTs = -Infinity;
129
+ for (const m of metas) {
130
+ if (m.slug && m.timestamp > bestTs) {
131
+ bestTs = m.timestamp;
132
+ bestSlug = m.slug;
133
+ }
134
+ }
135
+ if (bestSlug === null) return [];
136
+ return metas.filter((m) => m.slug === bestSlug);
137
+ }
138
+
139
+ /** Aggregate selected phase-metas into per-phase rows (pipeline order) plus a
140
+ * run total. A phase that ran multiple sub-agents sums them; its model is the
141
+ * newest meta's model. */
142
+ export function aggregateRun(metas: readonly PhaseMeta[], slug: string | null): RunCost {
143
+ const byPhase = new Map<CostPhase, PhaseAggregate & { _modelTs: number }>();
144
+ for (const m of metas) {
145
+ let agg = byPhase.get(m.phase);
146
+ if (!agg) {
147
+ agg = {
148
+ phase: m.phase,
149
+ model: m.model,
150
+ subAgents: 0,
151
+ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0,
152
+ durationMs: 0, toolCount: 0,
153
+ _modelTs: -Infinity,
154
+ };
155
+ byPhase.set(m.phase, agg);
156
+ }
157
+ agg.subAgents += 1;
158
+ agg.input += m.usage.input;
159
+ agg.output += m.usage.output;
160
+ agg.cacheRead += m.usage.cacheRead;
161
+ agg.cacheWrite += m.usage.cacheWrite;
162
+ agg.cost += m.usage.cost;
163
+ agg.turns += m.usage.turns;
164
+ agg.durationMs += m.durationMs;
165
+ agg.toolCount += m.toolCount;
166
+ if (m.timestamp >= agg._modelTs) {
167
+ agg._modelTs = m.timestamp;
168
+ agg.model = m.model;
169
+ }
170
+ }
171
+ const phases = [...byPhase.values()]
172
+ .sort((a, b) => PHASE_INDEX[a.phase] - PHASE_INDEX[b.phase])
173
+ .map(({ _modelTs, ...row }) => row);
174
+
175
+ const total = phases.reduce(
176
+ (t, p) => {
177
+ t.input += p.input; t.output += p.output; t.cacheRead += p.cacheRead;
178
+ t.cacheWrite += p.cacheWrite; t.cost += p.cost; t.turns += p.turns;
179
+ t.durationMs += p.durationMs; t.toolCount += p.toolCount; t.subAgents += p.subAgents;
180
+ return t;
181
+ },
182
+ { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0, durationMs: 0, toolCount: 0, subAgents: 0 },
183
+ );
184
+
185
+ return { slug, phases, total };
186
+ }
187
+
188
+ /** Human-readable duration: `12s`, `5m31s`. */
189
+ export function formatDuration(ms: number): string {
190
+ const safe = Number.isFinite(ms) && ms > 0 ? ms : 0;
191
+ const totalSec = Math.round(safe / 1000);
192
+ const min = Math.floor(totalSec / 60);
193
+ const sec = totalSec % 60;
194
+ return min > 0 ? `${min}m${sec.toString().padStart(2, "0")}s` : `${sec}s`;
195
+ }
196
+
197
+ /** USD with two decimals: `$2.48`. */
198
+ export function formatUsd(n: number): string {
199
+ const safe = Number.isFinite(n) ? n : 0;
200
+ return `$${safe.toFixed(2)}`;
201
+ }
202
+
203
+ /** Render an aligned per-phase + total report. */
204
+ export function formatReport(run: RunCost): string {
205
+ if (run.phases.length === 0) return "zero-cost: no encontré datos de costo para ese run.";
206
+ const head = `zero-cost: ${run.slug ?? "(run reciente)"}`;
207
+ const cols = `fase sub in out cache tools dur costo`;
208
+ const lines = [head, cols];
209
+ const row = (
210
+ label: string, subs: string, inn: number, out: number, cache: number,
211
+ tools: number, dur: number, cost: number,
212
+ ): string =>
213
+ `${label.padEnd(10)} ${subs.padStart(3)} ${formatTokens(inn).padStart(6)} ${formatTokens(out).padStart(6)} ${formatTokens(cache).padStart(7)} ${String(tools).padStart(5)} ${formatDuration(dur).padStart(6)} ${formatUsd(cost).padStart(7)}`;
214
+ for (const p of run.phases) {
215
+ lines.push(row(p.phase, String(p.subAgents), p.input, p.output, p.cacheRead, p.toolCount, p.durationMs, p.cost));
216
+ }
217
+ const t = run.total;
218
+ lines.push(row("TOTAL", String(t.subAgents), t.input, t.output, t.cacheRead, t.toolCount, t.durationMs, t.cost));
219
+ return lines.join("\n");
220
+ }
@@ -4,8 +4,8 @@
4
4
  //
5
5
  // claude-opus-4-7 · tok ↑12.3K ↓4.1K · diff +50/-12 · ctx 45% · master · www.ceroclawd.com
6
6
  //
7
- // Themed: model violet, tokens cyan/blue, diff mint/rose, ctx mint→amber→rose
8
- // by load, branch steel, brand amber. Pure 24-bit ANSI, no runtime deps.
7
+ // Themed (sunset): model coral, tokens gold/peach, diff mint/rose, ctx
8
+ // mint→amber→rose by load, branch steel, brand orchid. Pure 24-bit ANSI.
9
9
  //
10
10
  // Refreshes on `session_start`, `model_select`, `message_update`
11
11
  // (accumulates tokens), and `tool_execution_end` (re-reads git, since tools
@@ -17,17 +17,18 @@ import { promisify } from "node:util";
17
17
 
18
18
  const execAsync = promisify(exec);
19
19
 
20
- // ─── Color palette (matches the zero-sdd theme `vars`) ─────────────────────
20
+ // ─── Color palette (matches the zero-sunset theme `vars`) ─────────────────────
21
21
 
22
22
  type RGB = [number, number, number];
23
- const VIOLET: RGB = [175, 138, 255];
24
- const CYAN: RGB = [80, 210, 255];
25
- const BLUE: RGB = [116, 151, 255];
26
- const MINT: RGB = [79, 221, 171];
27
- const AMBER: RGB = [238, 190, 92];
28
- const ROSE: RGB = [255, 106, 122];
29
- const STEEL: RGB = [143, 152, 168];
30
- const DIM: RGB = [95, 104, 120];
23
+ const CORAL: RGB = [255, 124, 92]; // model
24
+ const GOLD: RGB = [255, 214, 130]; // tokens in
25
+ const PEACH: RGB = [255, 168, 99]; // tokens out
26
+ const ORCHID: RGB = [176, 106, 179]; // brand
27
+ const MINT: RGB = [79, 221, 171]; // diff added / ctx ok (semantic)
28
+ const AMBER: RGB = [238, 190, 92]; // ctx mid (semantic)
29
+ const ROSE: RGB = [255, 106, 122]; // diff removed / ctx hot (semantic)
30
+ const STEEL: RGB = [176, 152, 142]; // branch (warm gray)
31
+ const DIM: RGB = [120, 104, 98]; // labels / separators (warm gray)
31
32
 
32
33
  function fg([r, g, b]: RGB, text: string): string {
33
34
  return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
@@ -78,11 +79,11 @@ const SEP = fg(DIM, " · ");
78
79
  export function composeStatusline(p: StatuslineParts): string {
79
80
  const parts: string[] = [];
80
81
 
81
- if (p.model) parts.push(fg(VIOLET, p.model));
82
+ if (p.model) parts.push(fg(CORAL, p.model));
82
83
 
83
84
  if (p.tokensIn != null || p.tokensOut != null) {
84
- const inS = fg(CYAN, `↑${formatTokenCount(p.tokensIn ?? 0)}`);
85
- const outS = fg(BLUE, `↓${formatTokenCount(p.tokensOut ?? 0)}`);
85
+ const inS = fg(GOLD, `↑${formatTokenCount(p.tokensIn ?? 0)}`);
86
+ const outS = fg(PEACH, `↓${formatTokenCount(p.tokensOut ?? 0)}`);
86
87
  parts.push(`${fg(DIM, "tok")} ${inS} ${outS}`);
87
88
  }
88
89
 
@@ -98,7 +99,7 @@ export function composeStatusline(p: StatuslineParts): string {
98
99
 
99
100
  if (p.branch) parts.push(fg(STEEL, p.branch));
100
101
 
101
- if (p.brand) parts.push(fg(VIOLET, p.brand));
102
+ if (p.brand) parts.push(fg(ORCHID, p.brand));
102
103
 
103
104
  return parts.join(SEP);
104
105
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonrocca/zero-pi",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
4
4
  "description": "zero-pi — an installable layer for pi (pi.dev): the zero spec-driven development workflow (explore → plan → build → veredicto) with per-phase model autotune and token-efficient batched builds. Adds capability to pi without modifying pi.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -35,6 +35,7 @@
35
35
  "./extensions/spec-merge-extension.ts",
36
36
  "./extensions/zero-validate-extension.ts",
37
37
  "./extensions/zero-status-extension.ts",
38
+ "./extensions/zero-cost-extension.ts",
38
39
  "./extensions/zero-pr-extension.ts",
39
40
  "./extensions/zero-branch-extension.ts",
40
41
  "./extensions/zero-git-validate-extension.ts",
@@ -66,6 +67,9 @@
66
67
  "extensions/zero-validate-extension.ts",
67
68
  "extensions/zero-status.ts",
68
69
  "extensions/zero-status-extension.ts",
70
+ "extensions/zero-cost.ts",
71
+ "extensions/zero-cost-extension.ts",
72
+ "extensions/format-tokens.ts",
69
73
  "extensions/pr-body.ts",
70
74
  "extensions/sdd-links.ts",
71
75
  "extensions/sdd-config.ts",
@@ -0,0 +1,79 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "zero-sunset",
4
+ "vars": {
5
+ "gold": "#ffd682",
6
+ "peach": "#ffa863",
7
+ "coral": "#ff7c5c",
8
+ "rose": "#ff5c7a",
9
+ "magenta": "#d64a8c",
10
+ "orchid": "#b06ab3",
11
+ "sky": "#6db8e6",
12
+ "mint": "#5fd6a0",
13
+ "amber": "#eebe5c",
14
+ "steel": "#b0978f",
15
+ "dimSteel": "#7d6a64",
16
+ "panel": "#1c1117",
17
+ "selected": "#3a2230",
18
+ "okPanel": "#152318",
19
+ "errPanel": "#2e1518"
20
+ },
21
+ "colors": {
22
+ "accent": "coral",
23
+ "border": "orchid",
24
+ "borderAccent": "gold",
25
+ "borderMuted": "dimSteel",
26
+ "success": "mint",
27
+ "error": "rose",
28
+ "warning": "amber",
29
+ "muted": "steel",
30
+ "dim": "dimSteel",
31
+ "text": "",
32
+ "thinkingText": "steel",
33
+ "selectedBg": "selected",
34
+ "userMessageBg": "panel",
35
+ "userMessageText": "coral",
36
+ "customMessageBg": "panel",
37
+ "customMessageText": "",
38
+ "customMessageLabel": "gold",
39
+ "toolPendingBg": "panel",
40
+ "toolSuccessBg": "okPanel",
41
+ "toolErrorBg": "errPanel",
42
+ "toolTitle": "peach",
43
+ "toolOutput": "steel",
44
+ "mdHeading": "gold",
45
+ "mdLink": "peach",
46
+ "mdLinkUrl": "dimSteel",
47
+ "mdCode": "mint",
48
+ "mdCodeBlock": "mint",
49
+ "mdCodeBlockBorder": "orchid",
50
+ "mdQuote": "steel",
51
+ "mdQuoteBorder": "dimSteel",
52
+ "mdHr": "dimSteel",
53
+ "mdListBullet": "gold",
54
+ "toolDiffAdded": "mint",
55
+ "toolDiffRemoved": "rose",
56
+ "toolDiffContext": "steel",
57
+ "syntaxComment": "dimSteel",
58
+ "syntaxKeyword": "magenta",
59
+ "syntaxFunction": "peach",
60
+ "syntaxVariable": "gold",
61
+ "syntaxString": "mint",
62
+ "syntaxNumber": "rose",
63
+ "syntaxType": "sky",
64
+ "syntaxOperator": "coral",
65
+ "syntaxPunctuation": "steel",
66
+ "thinkingOff": "dimSteel",
67
+ "thinkingMinimal": "steel",
68
+ "thinkingLow": "sky",
69
+ "thinkingMedium": "peach",
70
+ "thinkingHigh": "coral",
71
+ "thinkingXhigh": "rose",
72
+ "bashMode": "gold"
73
+ },
74
+ "export": {
75
+ "pageBg": "#160e13",
76
+ "cardBg": "#1c1117",
77
+ "infoBg": "#241620"
78
+ }
79
+ }