@pi-extensions/pi-pacman 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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -0
  3. package/index.ts +518 -0
  4. package/package.json +43 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Saeed Marzban
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the Software), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # pi-pacman
2
+
3
+ Pac-Man working indicator for [pi](https://github.com/earendil-works/pi).
4
+
5
+ Replaces the streaming spinner with pellet runs, ghost chases, arcade tunnels, and fruit bonuses.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ # from npm (after publish)
11
+ pi install npm:@pi-extensions/pi-pacman
12
+
13
+ # local path (this monorepo)
14
+ pi install /absolute/path/to/pi-extentions/packages/pi-pacman
15
+ ```
16
+
17
+ ### Publish to npm
18
+
19
+ Releases are **tag-driven** (OIDC trusted publishing) — see [docs/releases.md](../../docs/releases.md).
20
+
21
+ ```bash
22
+ # after version bump on main:
23
+ git tag v0.1.0 && git push origin v0.1.0
24
+ ```
25
+
26
+ One-time: reservation publish with OTP + Trusted Publisher on npmjs.com (same doc).
27
+
28
+ Then anyone can:
29
+
30
+ ```bash
31
+ pi install npm:@pi-extensions/pi-pacman
32
+ pi install npm:@pi-extensions/pi-pacman@0.1.0
33
+ ```
34
+
35
+ ## Commands
36
+
37
+ | Command | Effect |
38
+ |---------|--------|
39
+ | `/pacman` | Show current look |
40
+ | `/pacman list` | Catalog of looks |
41
+ | `/pacman <look>` | Lock to that look (stops rotate) |
42
+ | `/pacman rotate` | Cycle a different look every message |
43
+ | `/pacman off` | Hide the indicator (stops rotate) |
44
+ | `/pacman message <text>` | Custom working message |
45
+ | `/pacman message` | Reset message to look default |
46
+ | `/pacman clear` | Hide the look list widget |
47
+
48
+ ### Looks
49
+
50
+ | Look | Vibe |
51
+ |------|------|
52
+ | `classic` | Full-width pellet run (default) |
53
+ | `chase` | Full-width Blinky hunt → revenge |
54
+ | `mini` | 7-cell pellet run |
55
+ | `arcade` | 7-cell maze tunnel |
56
+ | `fruit` | 7-cell cherry bonus |
57
+
58
+ Speeds: full-width **80ms**/frame · fixed **110ms**/frame.
59
+
60
+ ## Persistence
61
+
62
+ Look, rotate on/off, rotate index, and custom message save to `~/.pi/agent/pacman-thinking.json`.
63
+
64
+ ## One-shot (without install)
65
+
66
+ ```bash
67
+ pi --extension ./index.ts
68
+ ```
package/index.ts ADDED
@@ -0,0 +1,518 @@
1
+ /**
2
+ * Pac-Man Thinking Extension
3
+ *
4
+ * Replaces pi's streaming working indicator with a Pac-Man animation.
5
+ *
6
+ * Install:
7
+ * pi install /path/to/pi-extentions/packages/pi-pacman
8
+ * pi --extension ./index.ts
9
+ *
10
+ * Commands:
11
+ * /pacman Show current mode
12
+ * /pacman list List all looks
13
+ * /pacman <look> Lock to one look (stops rotate)
14
+ * /pacman rotate Cycle a different look on every message
15
+ * /pacman off Hide indicator (also stops rotate)
16
+ * /pacman message ... Custom working message (empty = mode default)
17
+ */
18
+
19
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { dirname, join } from "node:path";
21
+ import type {
22
+ ExtensionAPI,
23
+ ExtensionContext,
24
+ ExtensionUIContext,
25
+ WorkingIndicatorOptions,
26
+ } from "@earendil-works/pi-coding-agent";
27
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
28
+
29
+ // ── palette ──────────────────────────────────────────────────────────
30
+ const RESET = "\x1b[39m";
31
+ const YELLOW = "\x1b[38;2;255;221;0m";
32
+ const PELLET = "\x1b[38;2;255;184;174m";
33
+ const POWER = "\x1b[38;2;255;184;255m";
34
+ const WALL = "\x1b[38;2;33;33;255m";
35
+ const CHERRY = "\x1b[38;2;255;0;0m";
36
+ const SCORE = "\x1b[38;2;255;255;255m";
37
+ const GHOSTS = {
38
+ blinky: "\x1b[38;2;255;0;0m",
39
+ scared: "\x1b[38;2;33;33;255m",
40
+ } as const;
41
+
42
+ /** Full-width looks (classic / chase) — snappier chomp. */
43
+ const FRAME_MS_FULL = 80;
44
+ /** Fixed 7-cell looks (mini / arcade / fruit) — a bit slower. */
45
+ const FRAME_MS_FIXED = 110;
46
+
47
+ type Facing = "right" | "left";
48
+
49
+ interface Look {
50
+ id: string;
51
+ blurb: string;
52
+ message: string;
53
+ /** `track` = number of cells across the strip. */
54
+ frames: (track: number) => string[];
55
+ fullWidth?: boolean;
56
+ }
57
+
58
+ function paint(color: string, text: string): string {
59
+ return `${color}${text}${RESET}`;
60
+ }
61
+
62
+ function pac(open: boolean, facing: Facing = "right"): string {
63
+ if (!open) return paint(YELLOW, "○"); // empty circle = closed mouth
64
+ return paint(YELLOW, facing === "right" ? "ᗧ" : "ᗤ");
65
+ }
66
+
67
+ function pellet(): string {
68
+ return paint(PELLET, "·");
69
+ }
70
+
71
+ function ghost(color: string): string {
72
+ return paint(color, "ᗣ");
73
+ }
74
+
75
+ function power(): string {
76
+ return paint(POWER, "○");
77
+ }
78
+
79
+ function flash(): string {
80
+ return paint(YELLOW, "✦");
81
+ }
82
+
83
+ function cherry(): string {
84
+ return paint(CHERRY, "♦");
85
+ }
86
+
87
+ function termCols(): number {
88
+ const cols = process.stdout.columns || Number(process.env.COLUMNS) || 80;
89
+ return Math.max(20, cols);
90
+ }
91
+
92
+ /** Visible width budget for the indicator strip (cells). */
93
+ function indicatorWidth(message: string): number {
94
+ const reserve = Math.max(message.length, 12) + 3;
95
+ return Math.max(8, termCols() - reserve);
96
+ }
97
+
98
+ /** Pac-Man walks right eating pellets across `track` cells, then left. */
99
+ function runTrack(track: number): string[] {
100
+ const frames: string[] = [];
101
+ const last = Math.max(1, track - 1);
102
+
103
+ for (let i = 0; i <= last; i++) {
104
+ for (const open of [true, false]) {
105
+ let row = "";
106
+ for (let c = 0; c <= last; c++) {
107
+ if (c === i) row += pac(open, "right");
108
+ else if (c > i) row += pellet();
109
+ else row += " ";
110
+ }
111
+ frames.push(row);
112
+ }
113
+ }
114
+ for (let i = last; i >= 0; i--) {
115
+ for (const open of [true, false]) {
116
+ let row = "";
117
+ for (let c = 0; c <= last; c++) {
118
+ if (c === i) row += pac(open, "left");
119
+ else if (c < i) row += pellet();
120
+ else row += " ";
121
+ }
122
+ frames.push(row);
123
+ }
124
+ }
125
+ return frames;
126
+ }
127
+
128
+ // ── looks ────────────────────────────────────────────────────────────
129
+
130
+ /** Fixed strip length for mini / arcade / fruit (cells). */
131
+ const FIXED_CELLS = 7;
132
+
133
+ const LOOKS: Look[] = [
134
+ {
135
+ id: "classic",
136
+ blurb: "Full-width pellet run — dots span the terminal",
137
+ message: "waka waka...",
138
+ fullWidth: true,
139
+ frames: (track) => runTrack(track),
140
+ },
141
+ {
142
+ id: "chase",
143
+ blurb: "Full-width: Blinky hunts → power pellet → revenge",
144
+ message: "run from blinky...",
145
+ fullWidth: true,
146
+ frames: (track) => {
147
+ const last = Math.max(6, track - 1);
148
+ const gap = Math.max(2, Math.min(5, Math.floor(last / 10)));
149
+ const frames: string[] = [];
150
+
151
+ const rowAt = (
152
+ pacPos: number,
153
+ open: boolean,
154
+ facing: Facing,
155
+ ghostPos: number | null,
156
+ ghostColor: string,
157
+ opts?: { powerAt?: number; flash?: boolean },
158
+ ) => {
159
+ let row = "";
160
+ for (let c = 0; c <= last; c++) {
161
+ if (opts?.flash && c === pacPos) row += flash();
162
+ else if (c === pacPos) row += pac(open, facing);
163
+ else if (ghostPos !== null && c === ghostPos) row += ghost(ghostColor);
164
+ else if (opts?.powerAt === c) row += power();
165
+ else if (facing === "right" && c > pacPos) row += pellet();
166
+ else if (facing === "left" && c < pacPos) row += pellet();
167
+ else row += " ";
168
+ }
169
+ return row;
170
+ };
171
+
172
+ for (let i = gap; i <= last - 1; i++) {
173
+ for (const open of [true, false]) {
174
+ frames.push(rowAt(i, open, "right", i - gap, GHOSTS.blinky));
175
+ }
176
+ }
177
+
178
+ frames.push(rowAt(last - 1, true, "right", last - 1 - gap, GHOSTS.blinky, { powerAt: last }));
179
+ frames.push(rowAt(last - 1, false, "right", last - 1 - gap, GHOSTS.blinky, { powerAt: last }));
180
+ frames.push(rowAt(last, true, "right", last - gap, GHOSTS.blinky, { flash: true }));
181
+ frames.push(rowAt(last, false, "right", last - gap, GHOSTS.scared, { flash: true }));
182
+
183
+ for (let i = last; i >= 0; i--) {
184
+ for (const open of [true, false]) {
185
+ const gPos = i - gap;
186
+ frames.push(rowAt(i, open, "left", gPos >= 0 ? gPos : null, GHOSTS.scared));
187
+ }
188
+ }
189
+ frames.push(rowAt(0, true, "left", null, GHOSTS.scared, { flash: true }));
190
+ frames.push(" ".repeat(last + 1));
191
+ return frames;
192
+ },
193
+ },
194
+ {
195
+ id: "mini",
196
+ blurb: "7-cell pellet run",
197
+ message: "chomp chomp...",
198
+ frames: () => runTrack(FIXED_CELLS),
199
+ },
200
+ {
201
+ id: "arcade",
202
+ blurb: "7-cell maze tunnel with blue walls",
203
+ message: "insert coin...",
204
+ frames: () => {
205
+ const last = FIXED_CELLS - 1;
206
+ const frames: string[] = [];
207
+ const wall = paint(WALL, "│");
208
+ for (let i = 0; i <= last; i++) {
209
+ for (const open of [true, false]) {
210
+ let mid = "";
211
+ for (let c = 0; c <= last; c++) {
212
+ if (c === i) mid += pac(open, "right");
213
+ else if (c > i) mid += pellet();
214
+ else mid += " ";
215
+ }
216
+ frames.push(`${wall}${mid}${wall}`);
217
+ }
218
+ }
219
+ frames.push(`${wall}${paint(WALL, "≈".repeat(FIXED_CELLS))}${wall}`);
220
+ for (let i = last; i >= 0; i--) {
221
+ for (const open of [true, false]) {
222
+ let mid = "";
223
+ for (let c = 0; c <= last; c++) {
224
+ if (c === i) mid += pac(open, "left");
225
+ else if (c < i) mid += pellet();
226
+ else mid += " ";
227
+ }
228
+ frames.push(`${wall}${mid}${wall}`);
229
+ }
230
+ }
231
+ return frames;
232
+ },
233
+ },
234
+ {
235
+ id: "fruit",
236
+ blurb: "7-cell cherry bonus run",
237
+ message: "fruit bonus...",
238
+ frames: () => {
239
+ const last = FIXED_CELLS - 1;
240
+ const frames: string[] = [];
241
+
242
+ // Rightward: cherry waits at the far end
243
+ for (let i = 0; i <= last; i++) {
244
+ for (const open of [true, false]) {
245
+ let row = "";
246
+ for (let c = 0; c <= last; c++) {
247
+ if (c === i && i === last && !open) row += flash();
248
+ else if (c === i) row += pac(open, "right");
249
+ else if (c === last && i < last) row += cherry();
250
+ else if (c > i) row += pellet();
251
+ else row += " ";
252
+ }
253
+ frames.push(row);
254
+ }
255
+ }
256
+ frames.push(`${" ".repeat(Math.max(0, FIXED_CELLS - 3))}${paint(SCORE, "100")}`);
257
+ frames.push(`${" ".repeat(Math.max(0, FIXED_CELLS - 3))}${paint(SCORE, "100")}`);
258
+
259
+ // Leftward: cherry waits at the start
260
+ for (let i = last; i >= 0; i--) {
261
+ for (const open of [true, false]) {
262
+ let row = "";
263
+ for (let c = 0; c <= last; c++) {
264
+ if (c === i && i === 0 && !open) row += flash();
265
+ else if (c === i) row += pac(open, "left");
266
+ else if (c === 0 && i > 0) row += cherry();
267
+ else if (c < i) row += pellet();
268
+ else row += " ";
269
+ }
270
+ frames.push(row);
271
+ }
272
+ }
273
+ frames.push(`${paint(SCORE, "300")}${" ".repeat(Math.max(0, FIXED_CELLS - 3))}`);
274
+ frames.push(`${paint(SCORE, "300")}${" ".repeat(Math.max(0, FIXED_CELLS - 3))}`);
275
+ return frames;
276
+ },
277
+ },
278
+ ];
279
+
280
+ const LOOK_BY_ID = new Map(LOOKS.map((l) => [l.id, l]));
281
+ /** Looks eligible for rotate/random (excludes off/default). */
282
+ const ROTATE_LOOKS = LOOKS.map((l) => l.id);
283
+
284
+ type Mode = string;
285
+
286
+ interface PersistedState {
287
+ mode?: string;
288
+ /** @deprecated old field — migrated to `rotate` */
289
+ mix?: string;
290
+ rotate?: boolean;
291
+ rotateIndex?: number;
292
+ customMessage?: string;
293
+ }
294
+
295
+ function statePath(): string {
296
+ return join(getAgentDir(), "pacman-thinking.json");
297
+ }
298
+
299
+ function loadState(): PersistedState {
300
+ const path = statePath();
301
+ if (!existsSync(path)) return {};
302
+ try {
303
+ return JSON.parse(readFileSync(path, "utf-8")) as PersistedState;
304
+ } catch {
305
+ return {};
306
+ }
307
+ }
308
+
309
+ function saveState(state: PersistedState): void {
310
+ const path = statePath();
311
+ try {
312
+ mkdirSync(dirname(path), { recursive: true });
313
+ writeFileSync(path, `${JSON.stringify(state, null, "\t")}\n`, "utf-8");
314
+ } catch {
315
+ // Non-fatal — indicator still works without persistence
316
+ }
317
+ }
318
+
319
+ function resolveTrack(look: Look, message: string): number {
320
+ if (look.fullWidth) return indicatorWidth(message);
321
+ return FIXED_CELLS;
322
+ }
323
+
324
+ function getIndicator(mode: Mode, message: string): WorkingIndicatorOptions | undefined {
325
+ if (mode === "off") return { frames: [] };
326
+ const look = LOOK_BY_ID.get(mode);
327
+ if (!look) return undefined;
328
+ const track = resolveTrack(look, message);
329
+ const intervalMs = look.fullWidth ? FRAME_MS_FULL : FRAME_MS_FIXED;
330
+ return { frames: look.frames(track), intervalMs };
331
+ }
332
+
333
+ function describeMode(mode: Mode): string {
334
+ if (mode === "off") return "hidden";
335
+ const look = LOOK_BY_ID.get(mode);
336
+ return look ? `${look.id} — ${look.blurb}` : mode;
337
+ }
338
+
339
+ function defaultMessageFor(mode: Mode): string | undefined {
340
+ if (mode === "off") return " ";
341
+ return LOOK_BY_ID.get(mode)?.message;
342
+ }
343
+
344
+ function listLooks(): string {
345
+ const lines = LOOKS.map((l) => {
346
+ const tag = l.fullWidth ? " (full width)" : "";
347
+ return ` ${l.id.padEnd(8)} ${l.blurb}${tag}`;
348
+ });
349
+ return [
350
+ "Pac-Man looks:",
351
+ ...lines,
352
+ "",
353
+ " rotate Different look each message (cycle)",
354
+ " off Hide indicator",
355
+ "",
356
+ "Pick a look name to lock it (stops rotate).",
357
+ ].join("\n");
358
+ }
359
+
360
+ export default function (pi: ExtensionAPI) {
361
+ const saved = loadState();
362
+ // Migrate old persisted values
363
+ const savedMode =
364
+ saved.mode === "default" || saved.mode === "random" || saved.mode === "score"
365
+ ? "classic"
366
+ : saved.mode;
367
+ let mode: Mode =
368
+ savedMode && (LOOK_BY_ID.has(savedMode) || savedMode === "off") ? savedMode : "classic";
369
+ let rotate =
370
+ typeof saved.rotate === "boolean"
371
+ ? saved.rotate
372
+ : saved.mix === "rotate"; // migrate old mix field
373
+ let rotateIndex =
374
+ typeof saved.rotateIndex === "number" && saved.rotateIndex >= 0 ? saved.rotateIndex : 0;
375
+ let customMessage: string | undefined =
376
+ typeof saved.customMessage === "string" && saved.customMessage.length > 0
377
+ ? saved.customMessage
378
+ : undefined;
379
+ let lastUi: ExtensionUIContext | undefined;
380
+
381
+ const persist = () => {
382
+ saveState({
383
+ mode,
384
+ rotate,
385
+ rotateIndex,
386
+ customMessage,
387
+ });
388
+ };
389
+
390
+ const statusLabel = () => {
391
+ if (mode === "off") return `ᗧ off`;
392
+ return `ᗧ ${mode}${rotate ? " ↻" : ""}`;
393
+ };
394
+
395
+ const apply = (ctx: ExtensionContext) => {
396
+ lastUi = ctx.ui;
397
+ const message = customMessage ?? defaultMessageFor(mode) ?? "Working...";
398
+ ctx.ui.setWorkingIndicator(getIndicator(mode, message));
399
+ ctx.ui.setWorkingMessage(mode === "off" ? " " : message);
400
+ ctx.ui.setStatus("pacman-thinking", ctx.ui.theme.fg("dim", statusLabel()));
401
+ persist();
402
+ };
403
+
404
+ const isFullWidthMode = () => LOOK_BY_ID.get(mode)?.fullWidth === true;
405
+
406
+ const pickNextRotatedLook = () => {
407
+ mode = ROTATE_LOOKS[rotateIndex % ROTATE_LOOKS.length]!;
408
+ rotateIndex++;
409
+ };
410
+
411
+ const reapplyIfFullWidth = () => {
412
+ if (!lastUi || !isFullWidthMode()) return;
413
+ const message = customMessage ?? defaultMessageFor(mode) ?? "Working...";
414
+ lastUi.setWorkingIndicator(getIndicator(mode, message));
415
+ };
416
+
417
+ process.stdout.on?.("resize", reapplyIfFullWidth);
418
+
419
+ pi.on("session_start", async (_event, ctx) => {
420
+ // Don't advance rotate here — agent_start picks per message.
421
+ apply(ctx);
422
+ });
423
+
424
+ pi.on("agent_start", async (_event, ctx) => {
425
+ if (rotate) {
426
+ pickNextRotatedLook();
427
+ apply(ctx);
428
+ return;
429
+ }
430
+ if (isFullWidthMode()) apply(ctx);
431
+ });
432
+
433
+ pi.registerCommand("pacman", {
434
+ description: "Pac-Man indicator. Try: /pacman list · /pacman rotate",
435
+ handler: async (args, ctx) => {
436
+ const raw = args.trim();
437
+ if (!raw) {
438
+ const msg = customMessage ?? defaultMessageFor(mode) ?? "Working...";
439
+ const extra = isFullWidthMode()
440
+ ? ` · width ${indicatorWidth(msg)} cells`
441
+ : LOOK_BY_ID.has(mode)
442
+ ? ` · width ${FIXED_CELLS} cells`
443
+ : "";
444
+ const rotInfo = rotate ? " · rotate on" : "";
445
+ ctx.ui.notify(
446
+ `Pac-Man: ${describeMode(mode)}${rotInfo} · message: "${msg}"${extra}`,
447
+ "info",
448
+ );
449
+ return;
450
+ }
451
+
452
+ const [cmd, ...rest] = raw.split(/\s+/);
453
+ const head = cmd!.toLowerCase();
454
+
455
+ if (head === "list" || head === "help" || head === "looks") {
456
+ ctx.ui.setWidget("pacman-looks", listLooks().split("\n"), { placement: "belowEditor" });
457
+ ctx.ui.notify(
458
+ "Looks listed below the editor. /pacman <name> to switch. /pacman clear to hide list.",
459
+ "info",
460
+ );
461
+ return;
462
+ }
463
+
464
+ if (head === "clear") {
465
+ ctx.ui.setWidget("pacman-looks", undefined);
466
+ ctx.ui.notify("Cleared look list", "info");
467
+ return;
468
+ }
469
+
470
+ if (head === "message") {
471
+ const text = rest.join(" ").trim();
472
+ customMessage = text.length > 0 ? text : undefined;
473
+ apply(ctx);
474
+ ctx.ui.notify(
475
+ customMessage
476
+ ? `Working message set to: ${customMessage}`
477
+ : "Working message reset to mode default",
478
+ "info",
479
+ );
480
+ return;
481
+ }
482
+
483
+ if (head === "rotate" || head === "cycle") {
484
+ rotate = true;
485
+ const idx = ROTATE_LOOKS.indexOf(mode);
486
+ rotateIndex = idx >= 0 ? idx : 0;
487
+ pickNextRotatedLook();
488
+ apply(ctx);
489
+ ctx.ui.notify(
490
+ `Pac-Man rotate on — cycles each message (now: ${mode}). Pick a look to lock it.`,
491
+ "info",
492
+ );
493
+ return;
494
+ }
495
+
496
+ if (head === "off" || head === "none" || head === "hide") {
497
+ rotate = false;
498
+ mode = "off";
499
+ ctx.ui.setWidget("pacman-looks", undefined);
500
+ apply(ctx);
501
+ ctx.ui.notify("Pac-Man indicator hidden", "info");
502
+ return;
503
+ }
504
+
505
+ if (!LOOK_BY_ID.has(head)) {
506
+ ctx.ui.notify(`Unknown look "${head}". Try /pacman list`, "error");
507
+ return;
508
+ }
509
+
510
+ // Picking a look locks it and stops rotate
511
+ rotate = false;
512
+ mode = head;
513
+ ctx.ui.setWidget("pacman-looks", undefined);
514
+ apply(ctx);
515
+ ctx.ui.notify(`Pac-Man indicator: ${describeMode(mode)}`, "info");
516
+ },
517
+ });
518
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@pi-extensions/pi-pacman",
3
+ "version": "0.1.0",
4
+ "description": "Pac-Man working indicator for pi — replaces the streaming spinner with pellet runs, ghost chases, and more",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi-extension",
9
+ "pacman",
10
+ "working-indicator",
11
+ "coding-agent"
12
+ ],
13
+ "author": "Saeed Marzban",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/smarzban/pi-extentions.git",
18
+ "directory": "packages/pi-pacman"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/smarzban/pi-extentions/issues"
22
+ },
23
+ "homepage": "https://github.com/smarzban/pi-extentions/tree/main/packages/pi-pacman#readme",
24
+ "engines": {
25
+ "node": ">=18.0.0"
26
+ },
27
+ "pi": {
28
+ "extensions": [
29
+ "./index.ts"
30
+ ]
31
+ },
32
+ "files": [
33
+ "index.ts",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "prepublishOnly": "node -e \"require('fs').accessSync('index.ts')\""
42
+ }
43
+ }