@bugabinga/pi-ext-ghost 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.
package/overlay.ts ADDED
@@ -0,0 +1,602 @@
1
+ import {
2
+ type AgentSessionEvent,
3
+ ToolExecutionComponent,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
6
+ import {
7
+ Container,
8
+ type Focusable,
9
+ Input,
10
+ Key,
11
+ matchesKey,
12
+ type OverlayMargin,
13
+ type SizeValue,
14
+ type TUI,
15
+ Text,
16
+ truncateToWidth,
17
+ visibleWidth,
18
+ } from "@earendil-works/pi-tui";
19
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
20
+
21
+ const DEFAULT_STATUS = "Ask something quick.";
22
+ const MIN_FRAME_WIDTH = 4;
23
+ const MIN_FRAME_HEIGHT = 6;
24
+ const MIN_TRANSCRIPT_ROWS = 0;
25
+ const FRAME_CHROME_ROWS = 5;
26
+ const HEIGHT_RATIO = 0.55;
27
+ const DEFAULT_VERTICAL_MARGIN = 2;
28
+ const DEFAULT_WIDTH_RATIO = 1;
29
+ const DEFAULT_MAX_HEIGHT: SizeValue = "55%";
30
+ const PAGE_SCROLL_ROWS = 10;
31
+ const BOTTOM_SCROLL = Number.MAX_SAFE_INTEGER;
32
+
33
+ type GhostHistoryMessage = {
34
+ role: string;
35
+ content: unknown;
36
+ };
37
+
38
+ type GhostTheme = ExtensionContext["ui"]["theme"];
39
+
40
+ type MessageStartEvent = Extract<AgentSessionEvent, { type: "message_start" }>;
41
+ type MessageUpdateEvent = Extract<AgentSessionEvent, { type: "message_update" }>;
42
+ type MessageEndEvent = Extract<AgentSessionEvent, { type: "message_end" }>;
43
+ type ToolUpdateEvent = Extract<AgentSessionEvent, { type: "tool_execution_update" }>;
44
+ type ToolEndEvent = Extract<AgentSessionEvent, { type: "tool_execution_end" }>;
45
+
46
+ interface GhostOverlayLayout {
47
+ innerWidth: number;
48
+ frameHeight: number;
49
+ inputLines: string[];
50
+ transcriptRows: number;
51
+ transcriptLines: string[];
52
+ maxScroll: number;
53
+ }
54
+
55
+ export interface GhostOverlayOptions {
56
+ tui: TUI;
57
+ theme: GhostTheme;
58
+ sessionCwd: string;
59
+ modelLabel: string;
60
+ maxHeight?: SizeValue;
61
+ margin?: OverlayMargin;
62
+ onSubmitMessage(text: string): void;
63
+ onHideOverlay(): void;
64
+ onCloseOverlay(): void;
65
+ }
66
+
67
+ class GhostMessageComponent {
68
+ private readonly text = new Text("", 0, 0);
69
+
70
+ constructor(
71
+ private readonly role: "user" | "assistant",
72
+ private readonly theme: GhostTheme,
73
+ message: { content: unknown },
74
+ ) {
75
+ this.updateContent(message);
76
+ }
77
+
78
+ updateContent(message: { content: unknown }): void {
79
+ const label = this.role === "user"
80
+ ? this.theme.fg("accent", "you")
81
+ : this.theme.fg("muted", "ghost");
82
+ const body = extractMessageText(message);
83
+ this.text.setText(body ? `${label} ${body}` : label);
84
+ }
85
+
86
+ render(width: number): string[] {
87
+ return this.text.render(width);
88
+ }
89
+
90
+ invalidate(): void {
91
+ this.text.invalidate();
92
+ }
93
+ }
94
+
95
+ export class GhostOverlayComponent implements Focusable {
96
+ private readonly transcriptContainer = new Container();
97
+ private readonly input = new Input();
98
+ private readonly tui: TUI;
99
+ private readonly theme: GhostTheme;
100
+ private readonly sessionCwd: string;
101
+ private readonly modelLabel: string;
102
+ private readonly maxHeight: SizeValue;
103
+ private readonly verticalMargin: number;
104
+ private readonly onSubmitMessage: (text: string) => void;
105
+ private readonly onHideOverlay: () => void;
106
+ private readonly onCloseOverlay: () => void;
107
+ private streamingComponent?: GhostMessageComponent;
108
+ private pendingTools = new Map<string, ToolExecutionComponent>();
109
+ private statusText = DEFAULT_STATUS;
110
+ private scrollOffset = BOTTOM_SCROLL;
111
+ private followMode = true;
112
+ private cachedTranscriptLines?: string[];
113
+ private cachedTranscriptWidth?: number;
114
+ private lastInnerWidth = 0;
115
+ private _focused = false;
116
+
117
+ get focused(): boolean {
118
+ return this._focused;
119
+ }
120
+
121
+ set focused(value: boolean) {
122
+ this._focused = value;
123
+ this.input.focused = value;
124
+ }
125
+
126
+ constructor(options: GhostOverlayOptions) {
127
+ this.tui = options.tui;
128
+ this.theme = options.theme;
129
+ this.sessionCwd = options.sessionCwd;
130
+ this.modelLabel = options.modelLabel;
131
+ this.maxHeight = options.maxHeight ?? DEFAULT_MAX_HEIGHT;
132
+ this.verticalMargin = marginHeight(options.margin);
133
+ this.onSubmitMessage = options.onSubmitMessage;
134
+ this.onHideOverlay = options.onHideOverlay;
135
+ this.onCloseOverlay = options.onCloseOverlay;
136
+
137
+ this.input.onSubmit = (value) => this.submit(value);
138
+ this.input.onEscape = () => this.closeOverlay("close");
139
+ }
140
+
141
+ loadMessages(messages: readonly unknown[], isStreaming = false): void {
142
+ this.resetTranscript();
143
+
144
+ const history = messages.map(toHistoryMessage);
145
+ const lastAssistantIndex = findLastAssistantIndex(history);
146
+ for (let index = 0; index < history.length; index++) {
147
+ const message = history[index];
148
+ if (!message) continue;
149
+
150
+ if (message.role === "user") {
151
+ this.transcriptContainer.addChild(new GhostMessageComponent("user", this.theme, message));
152
+ continue;
153
+ }
154
+
155
+ if (message.role !== "assistant") continue;
156
+
157
+ const component = new GhostMessageComponent("assistant", this.theme, message);
158
+ this.transcriptContainer.addChild(component);
159
+ if (isStreaming && index === lastAssistantIndex) this.streamingComponent = component;
160
+ }
161
+
162
+ this.setStatus(isStreaming ? "Streaming response..." : DEFAULT_STATUS);
163
+ this.markTranscriptChanged();
164
+ }
165
+
166
+ loadEvents(events: readonly AgentSessionEvent[]): void {
167
+ this.resetTranscript();
168
+ for (const event of events) this.handleSessionEvent(event);
169
+ this.tui.requestRender(true);
170
+ }
171
+
172
+ private resetTranscript(): void {
173
+ this.transcriptContainer.clear();
174
+ this.streamingComponent = undefined;
175
+ this.pendingTools.clear();
176
+ this.statusText = DEFAULT_STATUS;
177
+ this.invalidateTranscriptCache();
178
+ this.scrollToBottom();
179
+ }
180
+
181
+ private closeOverlay(result: "hide" | "close"): void {
182
+ if (result === "hide") this.onHideOverlay();
183
+ else this.onCloseOverlay();
184
+ this.tui.requestRender(true);
185
+ }
186
+
187
+ private submit(value: string): void {
188
+ const text = value.trim();
189
+ if (!text) return;
190
+
191
+ this.input.setValue("");
192
+ this.scrollToBottom();
193
+ this.onSubmitMessage(text);
194
+ }
195
+
196
+ private invalidateTranscriptCache(): void {
197
+ this.cachedTranscriptLines = undefined;
198
+ this.cachedTranscriptWidth = undefined;
199
+ }
200
+
201
+ private markTranscriptChanged(): void {
202
+ this.invalidateTranscriptCache();
203
+ if (this.followMode) this.scrollToBottom();
204
+ }
205
+
206
+ private scrollToBottom(): void {
207
+ this.followMode = true;
208
+ this.scrollOffset = BOTTOM_SCROLL;
209
+ }
210
+
211
+ private getFrameHeight(): number {
212
+ const rows = Math.max(1, this.tui.terminal.rows);
213
+ const available = Math.max(1, rows - this.verticalMargin);
214
+ const configured = parseSize(this.maxHeight, rows) ?? Math.floor(rows * HEIGHT_RATIO);
215
+ return Math.max(1, Math.min(configured, available));
216
+ }
217
+
218
+ private getFallbackInnerWidth(): number {
219
+ const columns = Math.max(MIN_FRAME_WIDTH, this.tui.terminal.columns);
220
+ return Math.max(1, Math.floor(columns * DEFAULT_WIDTH_RATIO) - 2);
221
+ }
222
+
223
+ private getTranscriptLines(width: number): string[] {
224
+ if (this.cachedTranscriptLines && this.cachedTranscriptWidth === width) {
225
+ return this.cachedTranscriptLines;
226
+ }
227
+
228
+ const lines = this.transcriptContainer.render(width).map((line) =>
229
+ fitLine(line, width).trimEnd()
230
+ );
231
+ this.cachedTranscriptLines = lines;
232
+ this.cachedTranscriptWidth = width;
233
+ return lines;
234
+ }
235
+
236
+ private getCurrentMaxScroll(): number {
237
+ const innerWidth = this.lastInnerWidth || this.getFallbackInnerWidth();
238
+ return this.calculateLayout(innerWidth + 2).maxScroll;
239
+ }
240
+
241
+ setStatus(text: string): void {
242
+ this.statusText = text;
243
+ this.tui.requestRender();
244
+ }
245
+
246
+ handleInput(data: string): void {
247
+ if (matchesKey(data, Key.ctrl("s"))) return this.closeOverlay("hide");
248
+ if (this.tryHandleScrollKey(data)) return;
249
+
250
+ this.input.handleInput(data);
251
+ this.tui.requestRender();
252
+ }
253
+
254
+ private tryHandleScrollKey(data: string): boolean {
255
+ if (matchesKey(data, Key.up)) return this.scrollBy(-1);
256
+ if (matchesKey(data, Key.down)) return this.scrollBy(1);
257
+ if (matchesKey(data, Key.pageUp)) return this.scrollBy(-PAGE_SCROLL_ROWS);
258
+ if (matchesKey(data, Key.pageDown)) return this.scrollBy(PAGE_SCROLL_ROWS);
259
+ if (matchesKey(data, Key.home)) return this.scrollHome();
260
+ if (matchesKey(data, Key.end)) return this.scrollEnd();
261
+ return false;
262
+ }
263
+
264
+ private scrollBy(delta: number): true {
265
+ const maxScroll = this.getCurrentMaxScroll();
266
+
267
+ if (delta < 0) {
268
+ this.followMode = false;
269
+ this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScroll) + delta);
270
+ this.tui.requestRender();
271
+ return true;
272
+ }
273
+
274
+ if (!this.followMode) {
275
+ this.scrollOffset = Math.min(maxScroll, this.scrollOffset + delta);
276
+ if (this.scrollOffset >= maxScroll) this.scrollToBottom();
277
+ }
278
+
279
+ this.tui.requestRender();
280
+ return true;
281
+ }
282
+
283
+ private scrollHome(): true {
284
+ this.followMode = false;
285
+ this.scrollOffset = 0;
286
+ this.tui.requestRender();
287
+ return true;
288
+ }
289
+
290
+ private scrollEnd(): true {
291
+ this.scrollToBottom();
292
+ this.tui.requestRender();
293
+ return true;
294
+ }
295
+
296
+ handleSessionEvent(event: AgentSessionEvent): void {
297
+ switch (event.type) {
298
+ case "message_start":
299
+ this.handleMessageStart(event.message);
300
+ break;
301
+ case "message_update":
302
+ this.handleMessageUpdate(event.message);
303
+ break;
304
+ case "message_end":
305
+ this.handleMessageEnd(event.message);
306
+ break;
307
+ case "tool_execution_start":
308
+ this.handleToolStart(event.toolCallId, event.toolName, event.args);
309
+ break;
310
+ case "tool_execution_update":
311
+ this.handleToolUpdate(event.toolCallId, event.partialResult);
312
+ break;
313
+ case "tool_execution_end":
314
+ this.handleToolEnd(event.toolCallId, event.result, event.isError);
315
+ break;
316
+ case "agent_end":
317
+ this.handleAgentEnd();
318
+ return;
319
+ }
320
+
321
+ this.tui.requestRender();
322
+ }
323
+
324
+ private handleMessageStart(message: MessageStartEvent["message"]): void {
325
+ if (message.role === "user") {
326
+ this.transcriptContainer.addChild(new GhostMessageComponent("user", this.theme, message));
327
+ this.setStatus("Thinking...");
328
+ this.markTranscriptChanged();
329
+ return;
330
+ }
331
+
332
+ if (message.role !== "assistant") return;
333
+
334
+ this.streamingComponent = new GhostMessageComponent("assistant", this.theme, message);
335
+ this.transcriptContainer.addChild(this.streamingComponent);
336
+ this.setStatus("Streaming response...");
337
+ this.markTranscriptChanged();
338
+ }
339
+
340
+ private handleMessageUpdate(message: MessageUpdateEvent["message"]): void {
341
+ if (message.role !== "assistant" || !this.streamingComponent) return;
342
+
343
+ this.streamingComponent.updateContent(message);
344
+ this.markTranscriptChanged();
345
+ }
346
+
347
+ private handleMessageEnd(message: MessageEndEvent["message"]): void {
348
+ if (message.role !== "assistant") return;
349
+
350
+ this.streamingComponent?.updateContent(message);
351
+ this.finishPendingTools(message);
352
+ this.streamingComponent = undefined;
353
+ this.setStatus(DEFAULT_STATUS);
354
+ this.markTranscriptChanged();
355
+ }
356
+
357
+ private finishPendingTools(message: AssistantMessage): void {
358
+ if (!this.pendingTools.size) return;
359
+
360
+ if (message.stopReason === "aborted" || message.stopReason === "error") {
361
+ this.failPendingTools(message.errorMessage || "Error");
362
+ return;
363
+ }
364
+
365
+ for (const component of this.pendingTools.values()) component.setArgsComplete();
366
+ }
367
+
368
+ private failPendingTools(errorMessage: string): void {
369
+ for (const component of this.pendingTools.values()) {
370
+ component.updateResult({
371
+ content: [{ type: "text", text: errorMessage }],
372
+ isError: true,
373
+ });
374
+ }
375
+ this.pendingTools.clear();
376
+ }
377
+
378
+ private handleToolStart(toolCallId: string, toolName: string, args: unknown): void {
379
+ let component = this.pendingTools.get(toolCallId);
380
+
381
+ if (!component) {
382
+ component = new ToolExecutionComponent(
383
+ toolName,
384
+ toolCallId,
385
+ args,
386
+ { showImages: true },
387
+ undefined,
388
+ this.tui,
389
+ this.sessionCwd,
390
+ );
391
+ this.transcriptContainer.addChild(component);
392
+ this.pendingTools.set(toolCallId, component);
393
+ }
394
+
395
+ component.markExecutionStarted();
396
+ this.setStatus(`Running ${toolName}...`);
397
+ this.markTranscriptChanged();
398
+ }
399
+
400
+ private handleToolUpdate(
401
+ toolCallId: string,
402
+ partialResult: ToolUpdateEvent["partialResult"],
403
+ ): void {
404
+ const component = this.pendingTools.get(toolCallId);
405
+ if (!component) return;
406
+
407
+ component.updateResult({ ...partialResult, isError: false }, true);
408
+ this.markTranscriptChanged();
409
+ }
410
+
411
+ private handleToolEnd(
412
+ toolCallId: string,
413
+ result: ToolEndEvent["result"],
414
+ isError: boolean,
415
+ ): void {
416
+ const component = this.pendingTools.get(toolCallId);
417
+ if (!component) return;
418
+
419
+ component.updateResult({ ...result, isError });
420
+ this.pendingTools.delete(toolCallId);
421
+ this.markTranscriptChanged();
422
+ }
423
+
424
+ private handleAgentEnd(): void {
425
+ this.streamingComponent = undefined;
426
+ this.pendingTools.clear();
427
+ this.setStatus(DEFAULT_STATUS);
428
+ this.markTranscriptChanged();
429
+ this.tui.requestRender();
430
+ }
431
+
432
+ invalidate(): void {
433
+ this.transcriptContainer.invalidate();
434
+ this.input.invalidate();
435
+ this.invalidateTranscriptCache();
436
+ }
437
+
438
+ render(width: number): string[] {
439
+ if (width <= 0) return [];
440
+ if (width < MIN_FRAME_WIDTH) return this.renderTiny(width);
441
+
442
+ const layout = this.calculateLayout(width);
443
+ if (layout.frameHeight < MIN_FRAME_HEIGHT) return this.renderCompact(width, layout.frameHeight);
444
+
445
+ this.lastInnerWidth = layout.innerWidth;
446
+ this.updateScroll(layout.maxScroll);
447
+
448
+ const transcript = layout.transcriptLines.slice(
449
+ this.scrollOffset,
450
+ this.scrollOffset + layout.transcriptRows,
451
+ );
452
+
453
+ return [
454
+ this.renderHeader(layout.innerWidth),
455
+ this.borderLine("├", "┤", layout.innerWidth),
456
+ ...this.renderTranscript(transcript, layout.transcriptRows, layout.innerWidth),
457
+ this.borderLine("├", "┤", layout.innerWidth),
458
+ this.renderFooter(layout),
459
+ ...layout.inputLines.map((line) => this.boxedLine(line, layout.innerWidth)),
460
+ this.borderLine("╰", "╯", layout.innerWidth),
461
+ ].slice(0, layout.frameHeight);
462
+ }
463
+
464
+ private calculateLayout(width: number): GhostOverlayLayout {
465
+ const innerWidth = Math.max(1, width - 2);
466
+ const frameHeight = this.getFrameHeight();
467
+ const maxInputRows = Math.max(1, frameHeight - FRAME_CHROME_ROWS - MIN_TRANSCRIPT_ROWS);
468
+ const inputLines = this.input.render(innerWidth)
469
+ .slice(-maxInputRows)
470
+ .map((line) => fitLine(line, innerWidth).trimEnd());
471
+ const transcriptRows = Math.max(
472
+ MIN_TRANSCRIPT_ROWS,
473
+ frameHeight - FRAME_CHROME_ROWS - inputLines.length,
474
+ );
475
+ const transcriptLines = this.getTranscriptLines(innerWidth);
476
+ const maxScroll = Math.max(0, transcriptLines.length - transcriptRows);
477
+
478
+ return { innerWidth, frameHeight, inputLines, transcriptRows, transcriptLines, maxScroll };
479
+ }
480
+
481
+ private updateScroll(maxScroll: number): void {
482
+ if (this.followMode) {
483
+ this.scrollOffset = maxScroll;
484
+ return;
485
+ }
486
+
487
+ this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScroll));
488
+ if (this.scrollOffset >= maxScroll) this.scrollToBottom();
489
+ }
490
+
491
+ private renderTiny(width: number): string[] {
492
+ return [truncateToWidth(this.theme.fg("accent", "ghost"), width, "")];
493
+ }
494
+
495
+ private renderCompact(width: number, height: number): string[] {
496
+ const content = `${this.theme.fg("accent", "ghost")} ${this.theme.fg("dim", this.statusText)}`;
497
+ return Array.from({ length: Math.max(1, height) }, (_, index) =>
498
+ index === 0 ? fitLine(content, width) : fitLine("", width)
499
+ );
500
+ }
501
+
502
+ private renderHeader(innerWidth: number): string {
503
+ const content = truncateToWidth(
504
+ this.theme.fg("accent", ` ${this.theme.bold("ghost pi")} `) +
505
+ this.theme.fg("dim", "same model • ephemeral session • ↑↓ scroll • ctrl+s hide • esc close"),
506
+ innerWidth,
507
+ );
508
+ const pad = "─".repeat(Math.max(0, innerWidth - visibleWidth(content)));
509
+ return this.theme.fg("border", "╭") + content + this.theme.fg("border", pad + "╮");
510
+ }
511
+
512
+ private renderTranscript(lines: string[], rowCount: number, innerWidth: number): string[] {
513
+ const rows: string[] = [];
514
+ for (const line of lines) rows.push(this.boxedLine(line, innerWidth));
515
+ while (rows.length < rowCount) rows.push(this.boxedLine("", innerWidth));
516
+ return rows;
517
+ }
518
+
519
+ private renderFooter(layout: GhostOverlayLayout): string {
520
+ const info = layout.transcriptLines.length > layout.transcriptRows && layout.transcriptRows > 0
521
+ ? `${this.scrollOffset + 1}-${Math.min(this.scrollOffset + layout.transcriptRows, layout.transcriptLines.length)}/${layout.transcriptLines.length}`
522
+ : `${layout.transcriptLines.length}L`;
523
+ const followIcon = this.followMode ? this.theme.fg("success", "●") : this.theme.fg("dim", "○");
524
+ const content = [
525
+ this.theme.fg("dim", this.statusText),
526
+ this.theme.fg("accent", this.modelLabel),
527
+ this.theme.fg("dim", formatGhostCwd(this.sessionCwd)),
528
+ this.theme.fg("dim", info),
529
+ followIcon,
530
+ ].join(` ${this.theme.fg("border", "│")} `);
531
+
532
+ return this.boxedLine(content, layout.innerWidth);
533
+ }
534
+
535
+ private borderLine(left: string, right: string, innerWidth: number): string {
536
+ return this.theme.fg("border", left + "─".repeat(innerWidth) + right);
537
+ }
538
+
539
+ private boxedLine(line: string, innerWidth: number): string {
540
+ return this.theme.fg("border", "│") + fitLine(line, innerWidth) + this.theme.fg("border", "│");
541
+ }
542
+ }
543
+
544
+ export function fitLine(line: string, width: number): string {
545
+ if (width <= 0) return "";
546
+
547
+ const truncated = truncateToWidth(line, width, "");
548
+ return truncated + " ".repeat(Math.max(0, width - visibleWidth(truncated)));
549
+ }
550
+
551
+ export function extractMessageText(message: { content: unknown }): string {
552
+ if (typeof message.content === "string") return message.content.trim();
553
+ if (!Array.isArray(message.content)) return "";
554
+
555
+ return message.content
556
+ .filter((part): part is { type: "text"; text: string } =>
557
+ typeof part === "object" &&
558
+ part !== null &&
559
+ (part as { type?: unknown }).type === "text" &&
560
+ typeof (part as { text?: unknown }).text === "string"
561
+ )
562
+ .map((part) => part.text)
563
+ .join("\n")
564
+ .trim();
565
+ }
566
+
567
+ export function formatGhostCwd(cwd: string): string {
568
+ const home = process.env.HOME;
569
+ if (home && cwd.startsWith(home)) {
570
+ return `~${cwd.slice(home.length)}` || "~";
571
+ }
572
+ return cwd;
573
+ }
574
+
575
+ function parseSize(value: SizeValue, reference: number): number | undefined {
576
+ if (typeof value === "number") return Math.floor(value);
577
+
578
+ const match = /^(\d+(?:\.\d+)?)%$/.exec(value);
579
+ if (!match) return undefined;
580
+ return Math.floor((reference * Number(match[1])) / 100);
581
+ }
582
+
583
+ function toHistoryMessage(message: unknown): GhostHistoryMessage | undefined {
584
+ if (typeof message !== "object" || message === null) return undefined;
585
+ const candidate = message as { role?: unknown; content?: unknown };
586
+ if (typeof candidate.role !== "string") return undefined;
587
+ return { role: candidate.role, content: candidate.content };
588
+ }
589
+
590
+ function findLastAssistantIndex(messages: readonly (GhostHistoryMessage | undefined)[]): number {
591
+ for (let index = messages.length - 1; index >= 0; index--) {
592
+ if (messages[index]?.role === "assistant") return index;
593
+ }
594
+ return -1;
595
+ }
596
+
597
+ function marginHeight(margin: OverlayMargin | undefined): number {
598
+ return Math.max(
599
+ DEFAULT_VERTICAL_MARGIN,
600
+ Math.max(0, margin?.top ?? 0) + Math.max(0, margin?.bottom ?? 0),
601
+ );
602
+ }
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@bugabinga/pi-ext-ghost",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "index.ts",
6
+ "peerDependencies": {
7
+ "@earendil-works/pi-ai": "*",
8
+ "@earendil-works/pi-coding-agent": "*",
9
+ "@earendil-works/pi-tui": "*"
10
+ },
11
+ "license": "MIT",
12
+ "description": "Background ghost agent helper for Pi.",
13
+ "keywords": [
14
+ "pi",
15
+ "pi-extension"
16
+ ]
17
+ }