@gonrocca/zero-pi 0.1.28 → 0.1.29

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
@@ -64,6 +64,7 @@ into `/forge` for you.
64
64
  | **`/zero-sync`** | Folds each run's spec delta into a canonical, project-wide spec store. |
65
65
  | **Run memory** | Every run recalls and saves traces to Cortex, so runs learn from each other. |
66
66
  | **Provider guard** | Warns when the `anthropic` provider runs on a metered API key instead of your subscription. |
67
+ | **Startup banner** | The violet ANSI-Shadow `ZERO` wordmark, drawn once at pi startup — `ZERO_HEADER=off` to disable. |
67
68
  | **Working-phrase ticker** | Swaps pi's `Working...` for a context-aware Spanish phrase + spinner. |
68
69
  | **Conversation resume** | Writes `.pi/zero-resume.md` on exit — the restore command + a conversation tail. |
69
70
  | **Windows tree-kill** | Aborting a turn kills the whole process tree — no orphaned `claude`. |
@@ -0,0 +1,151 @@
1
+ // zero-pi — static ZERO SDD startup banner.
2
+ //
3
+ // Renders the ZERO wordmark ONCE, at extension load, as an "ANSI Shadow" 3D
4
+ // block in ceroclawd.com violet — deep-violet faces, a lit top edge, dark
5
+ // shadow strokes and a cast shadow for depth.
6
+ //
7
+ // It writes a single block to stdout before pi's UI takes over. There is
8
+ // deliberately NO setHeader and NO animation timer: an animated header that
9
+ // re-renders on a timer spammed the terminal on pi 0.75.x and could crash the
10
+ // session. A one-time stdout write cannot re-render, so it cannot spam.
11
+ //
12
+ // Disable with the ZERO_HEADER=off environment variable.
13
+
14
+ type RGB = [number, number, number];
15
+
16
+ const WORD = "ZERO";
17
+ const ROWS = 6;
18
+
19
+ // Cast-shadow offset — light from the top-left, shadow falls bottom-right.
20
+ const SHADOW_DX = 2;
21
+ const SHADOW_DY = 1;
22
+
23
+ // ANSI Shadow glyphs — six rows each, glued edge to edge like figlet output.
24
+ const FONT: Record<string, string[]> = {
25
+ Z: ["███████╗", "╚══███╔╝", " ███╔╝ ", " ███╔╝ ", "███████╗", "╚══════╝"],
26
+ E: ["███████╗", "██╔════╝", "█████╗ ", "██╔══╝ ", "███████╗", "╚══════╝"],
27
+ R: ["██████╗ ", "██╔══██╗", "██████╔╝", "██╔══██╗", "██║ ██║", "╚═╝ ╚═╝"],
28
+ O: [" ██████╗ ", "██╔═══██╗", "██║ ██║", "██║ ██║", "╚██████╔╝", " ╚═════╝ "],
29
+ " ": [" ", " ", " ", " ", " ", " "],
30
+ };
31
+
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];
40
+
41
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
42
+
43
+ function fg([r, g, b]: RGB, text: string): string {
44
+ return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
45
+ }
46
+
47
+ function lerp(a: number, b: number, t: number): number {
48
+ return a + (b - a) * t;
49
+ }
50
+
51
+ function mix(a: RGB, b: RGB, t: number): RGB {
52
+ const k = Math.max(0, Math.min(1, t));
53
+ return [
54
+ Math.round(lerp(a[0], b[0], k)),
55
+ Math.round(lerp(a[1], b[1], k)),
56
+ Math.round(lerp(a[2], b[2], k)),
57
+ ];
58
+ }
59
+
60
+ /** Printable width of a string, ignoring ANSI colour escapes. */
61
+ export function visibleWidth(text: string): number {
62
+ return text.replace(ANSI_RE, "").length;
63
+ }
64
+
65
+ function center(text: string, width: number): string {
66
+ const pad = Math.max(0, Math.floor((width - visibleWidth(text)) / 2));
67
+ return " ".repeat(pad) + text;
68
+ }
69
+
70
+ function matrixFor(text: string): { rows: string[]; width: number } {
71
+ const rows = Array.from({ length: ROWS }, () => "");
72
+ for (const raw of Array.from(text)) {
73
+ const glyph = FONT[raw.toUpperCase()] ?? FONT[" "];
74
+ for (let row = 0; row < ROWS; row++) rows[row] += glyph[row];
75
+ }
76
+ return { rows, width: rows[0]?.length ?? 0 };
77
+ }
78
+
79
+ /** Front-face colour: violet vertical gradient with a lit top edge. */
80
+ 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;
84
+ }
85
+
86
+ /** Extrusion side: a darker, shaded version of the face it belongs to. */
87
+ 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);
90
+ }
91
+
92
+ function renderLogo(width: number): string[] {
93
+ const matrix = matrixFor(WORD);
94
+ const W = matrix.width;
95
+ const cellAt = (r: number, c: number): string =>
96
+ r >= 0 && r < ROWS && c >= 0 && c < W ? matrix.rows[r][c] ?? " " : " ";
97
+ const isFill = (r: number, c: number): boolean => cellAt(r, c) !== " ";
98
+
99
+ const lines: string[] = [];
100
+ for (let gr = 0; gr < ROWS + SHADOW_DY; gr++) {
101
+ let out = "";
102
+ for (let gc = 0; gc < W + SHADOW_DX; gc++) {
103
+ const ch = cellAt(gr, gc);
104
+ if (ch !== " ") {
105
+ out += ch === "█" ? fg(faceColor(gr, !isFill(gr - 1, gc)), ch) : fg(sideColor(gr), ch);
106
+ } else if ((gr >= ROWS || gc >= W) && isFill(gr - SHADOW_DY, gc - SHADOW_DX)) {
107
+ out += fg(INK, "█");
108
+ } else {
109
+ out += " ";
110
+ }
111
+ }
112
+ lines.push(center(out, width));
113
+ }
114
+ return lines;
115
+ }
116
+
117
+ /** A thin violet rule, brightest in the middle. */
118
+ function ornament(width: number): string {
119
+ const length = Math.min(46, Math.max(22, Math.floor(width * 0.5)));
120
+ let line = "";
121
+ for (let i = 0; i < length; i++) {
122
+ const t = i / Math.max(1, length - 1);
123
+ line += fg(mix(VIOLET_DEEP, VIOLET, Math.sin(t * Math.PI)), "─");
124
+ }
125
+ return center(line, width);
126
+ }
127
+
128
+ /** The full banner block, centered to the given width. */
129
+ export function bannerBlock(width: number): string[] {
130
+ if (width < 64) {
131
+ return [center(fg(VIOLET, "ZERO SDD"), width), center(fg(MUTED, "pi.dev · spec-driven work"), width)];
132
+ }
133
+ const tag = fg(VIOLET, "ZERO SDD") + fg(MUTED, " explore → plan → build → veredicto");
134
+ return [ornament(width), "", ...renderLogo(width), "", center(tag, width), ornament(width)];
135
+ }
136
+
137
+ /**
138
+ * The pi extension entry point. Renders the banner exactly once, synchronously,
139
+ * at load time. Any failure is swallowed — a banner must never break a session.
140
+ */
141
+ export default function register(_pi?: unknown): void {
142
+ try {
143
+ if ((process.env.ZERO_HEADER ?? "").trim().toLowerCase() === "off") return;
144
+ const stream = process.stdout;
145
+ if (!stream || !stream.isTTY || process.env.NO_COLOR) return;
146
+ const width = stream.columns && stream.columns > 0 ? stream.columns : 80;
147
+ stream.write("\n" + bannerBlock(width).join("\n") + "\n\n");
148
+ } catch {
149
+ // A banner failure must never break a pi session.
150
+ }
151
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonrocca/zero-pi",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
4
4
  "description": "zero-pi — an installable layer for pi (pi.dev): the zero spec-driven development workflow, per-phase model autotune, and skill auto-learning. Adds capability to pi without modifying pi.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -23,6 +23,7 @@
23
23
  "./themes"
24
24
  ],
25
25
  "extensions": [
26
+ "./extensions/zero-banner.ts",
26
27
  "./extensions/working-phrases.ts",
27
28
  "./extensions/win-tree-kill.ts",
28
29
  "./extensions/sdd-agents.ts",
@@ -37,6 +38,7 @@
37
38
  "prompts",
38
39
  "skills",
39
40
  "themes",
41
+ "extensions/zero-banner.ts",
40
42
  "extensions/working-phrases.ts",
41
43
  "extensions/win-tree-kill.ts",
42
44
  "extensions/sdd-agents.ts",