pi-tic-tac-toe 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/README.md +93 -0
- package/extensions/game.ts +128 -0
- package/extensions/index.ts +245 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# pi-tic-tac-toe
|
|
2
|
+
|
|
3
|
+
Play a quick game of tic tac toe against the LLM inside [Pi](https://github.com/earendil-works/pi).
|
|
4
|
+
|
|
5
|
+
`pi-tic-tac-toe` adds a persistent TUI overlay opened with `/tic-tac-toe`. You play `X`, the LLM plays `O`, and the game stays in your Pi session so you can close the overlay and resume later.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Centered terminal overlay, built with Pi TUI
|
|
10
|
+
- Human vs LLM gameplay
|
|
11
|
+
- Arrow-key and number-key input
|
|
12
|
+
- Session persistence across Pi reloads/resumes
|
|
13
|
+
- Validated saved state and legal move checking
|
|
14
|
+
- TypeScript source, no build step required
|
|
15
|
+
- Manual npm publish workflow included
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pi install npm:pi-tic-tac-toe
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or add it to `~/.pi/agent/settings.json`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"packages": ["npm:pi-tic-tac-toe"]
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
Start or resume a game:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
/tic-tac-toe
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Start a fresh game:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
/tic-tac-toe new
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Controls
|
|
46
|
+
|
|
47
|
+
| Key | Action |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| Arrow keys | Move cursor |
|
|
50
|
+
| `1`-`9` | Jump to a square |
|
|
51
|
+
| Enter | Place `X` |
|
|
52
|
+
| `n` | New game |
|
|
53
|
+
| `q` | Close overlay |
|
|
54
|
+
|
|
55
|
+
The overlay is TUI-only. It will not open in Pi print, JSON, or RPC modes.
|
|
56
|
+
|
|
57
|
+
## How It Works
|
|
58
|
+
|
|
59
|
+
After you place `X`, the extension asks the LLM to play `O` by calling:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
make_tic_tac_toe_move
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The extension validates every move, so the LLM cannot move out of turn, overwrite a square, or continue after the game is over.
|
|
66
|
+
|
|
67
|
+
## Development
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install
|
|
71
|
+
npm test
|
|
72
|
+
npm run check
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Try the extension locally:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pi -e ./extensions/index.ts
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Then run:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
/tic-tac-toe
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Package Notes
|
|
88
|
+
|
|
89
|
+
Pi loads TypeScript extensions directly, so this package publishes the source `.ts` files. Pi core packages are declared as peer dependencies to avoid bundling duplicate Pi runtime packages.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export type Mark = "X" | "O";
|
|
2
|
+
export type Winner = Mark | "draw" | null;
|
|
3
|
+
|
|
4
|
+
export type Move = {
|
|
5
|
+
cell: number;
|
|
6
|
+
mark: Mark;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type GameState = {
|
|
10
|
+
board: Array<Mark | null>;
|
|
11
|
+
turn: Mark;
|
|
12
|
+
history: Move[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class TicTacToeGame {
|
|
16
|
+
board: Array<Mark | null>;
|
|
17
|
+
turn: Mark;
|
|
18
|
+
history: Move[];
|
|
19
|
+
|
|
20
|
+
constructor(state?: GameState) {
|
|
21
|
+
const parsed = state ? parseGameState(state) : initialState();
|
|
22
|
+
this.board = parsed.board;
|
|
23
|
+
this.turn = parsed.turn;
|
|
24
|
+
this.history = parsed.history;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
play(cell: number, mark: Mark): boolean {
|
|
28
|
+
if (!Number.isInteger(cell) || cell < 0 || cell > 8 || mark !== this.turn || this.board[cell] || this.winner()) return false;
|
|
29
|
+
this.board[cell] = mark;
|
|
30
|
+
this.history.push({ cell, mark });
|
|
31
|
+
this.turn = mark === "X" ? "O" : "X";
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
legalMoves(): number[] {
|
|
36
|
+
if (this.winner()) return [];
|
|
37
|
+
return this.board.flatMap((mark, cell) => mark ? [] : [cell]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
winner(): Winner {
|
|
41
|
+
const winners = winnersFor(this.board);
|
|
42
|
+
if (winners.length) return winners[0];
|
|
43
|
+
return this.board.every(Boolean) ? "draw" : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
status(): string {
|
|
47
|
+
const winner = this.winner();
|
|
48
|
+
if (winner === "draw") return "Draw";
|
|
49
|
+
if (winner) return `${winner} wins`;
|
|
50
|
+
return `${this.turn} to move`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
toState(): GameState {
|
|
54
|
+
return { board: this.board.slice(), turn: this.turn, history: this.history.slice() };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static fromState(state: unknown): TicTacToeGame {
|
|
58
|
+
return new TicTacToeGame(parseGameState(state));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function boardPrompt(game: TicTacToeGame): string {
|
|
63
|
+
const cells = game.board.map((mark, i) => mark ?? String(i + 1));
|
|
64
|
+
return [
|
|
65
|
+
`${cells[0]} | ${cells[1]} | ${cells[2]}`,
|
|
66
|
+
`${cells[3]} | ${cells[4]} | ${cells[5]}`,
|
|
67
|
+
`${cells[6]} | ${cells[7]} | ${cells[8]}`,
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function parseGameState(value: unknown): GameState {
|
|
72
|
+
if (!isRecord(value)) throw new Error("Invalid state");
|
|
73
|
+
if (!Array.isArray(value.board) || value.board.length !== 9) throw new Error("Invalid board");
|
|
74
|
+
if (value.turn !== "X" && value.turn !== "O") throw new Error("Invalid turn");
|
|
75
|
+
if (!Array.isArray(value.history)) throw new Error("Invalid history");
|
|
76
|
+
|
|
77
|
+
const board = value.board.map(parseCell);
|
|
78
|
+
const turn: Mark = value.turn;
|
|
79
|
+
const history = value.history.map(parseMove);
|
|
80
|
+
const state = { board, turn, history };
|
|
81
|
+
validateState(state);
|
|
82
|
+
return state;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function initialState(): GameState {
|
|
86
|
+
return { board: Array<Mark | null>(9).fill(null), turn: "X", history: [] };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseCell(value: unknown): Mark | null {
|
|
90
|
+
if (value === null || value === "X" || value === "O") return value;
|
|
91
|
+
throw new Error("Invalid board");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseMove(value: unknown): Move {
|
|
95
|
+
if (!isRecord(value) || (value.mark !== "X" && value.mark !== "O")) throw new Error("Invalid history");
|
|
96
|
+
const cell = value.cell;
|
|
97
|
+
if (typeof cell !== "number" || !Number.isInteger(cell) || cell < 0 || cell > 8) {
|
|
98
|
+
throw new Error("Invalid history");
|
|
99
|
+
}
|
|
100
|
+
return { cell, mark: value.mark };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function validateState(state: GameState): void {
|
|
104
|
+
const xs = state.board.filter((mark) => mark === "X").length;
|
|
105
|
+
const os = state.board.filter((mark) => mark === "O").length;
|
|
106
|
+
if (os > xs || xs > os + 1) throw new Error("Invalid mark counts");
|
|
107
|
+
|
|
108
|
+
const winners = winnersFor(state.board);
|
|
109
|
+
const winnerSet = new Set(winners);
|
|
110
|
+
if (winnerSet.size > 1) throw new Error("Invalid board: mixed winners");
|
|
111
|
+
if (state.turn !== (xs === os ? "X" : "O")) throw new Error("Invalid turn");
|
|
112
|
+
if (winners[0] === "X" && xs !== os + 1) throw new Error("Invalid winner");
|
|
113
|
+
if (winners[0] === "O" && xs !== os) throw new Error("Invalid winner");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function winnersFor(board: Array<Mark | null>): Mark[] {
|
|
117
|
+
return LINES.flatMap(([a, b, c]) => board[a] && board[a] === board[b] && board[a] === board[c] ? [board[a]] : []);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
121
|
+
return typeof value === "object" && value !== null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const LINES = [
|
|
125
|
+
[0, 1, 2], [3, 4, 5], [6, 7, 8],
|
|
126
|
+
[0, 3, 6], [1, 4, 7], [2, 5, 8],
|
|
127
|
+
[0, 4, 8], [2, 4, 6],
|
|
128
|
+
] as const;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolResult,
|
|
3
|
+
ExtensionAPI,
|
|
4
|
+
ExtensionCommandContext,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import {
|
|
7
|
+
type Component,
|
|
8
|
+
Key,
|
|
9
|
+
matchesKey,
|
|
10
|
+
type TUI,
|
|
11
|
+
truncateToWidth,
|
|
12
|
+
visibleWidth,
|
|
13
|
+
} from "@earendil-works/pi-tui";
|
|
14
|
+
import { Type } from "typebox";
|
|
15
|
+
import { boardPrompt, type GameState, TicTacToeGame } from "./game.ts";
|
|
16
|
+
|
|
17
|
+
const SAVE_TYPE = "tic-tac-toe-save";
|
|
18
|
+
const moveSchema = Type.Object({
|
|
19
|
+
square: Type.Integer({
|
|
20
|
+
minimum: 1,
|
|
21
|
+
maximum: 9,
|
|
22
|
+
description: "Board square from 1 to 9.",
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
type MoveDetails = { square: number; state: GameState; status: string };
|
|
27
|
+
|
|
28
|
+
class TicTacToeComponent implements Component {
|
|
29
|
+
cursor = 4;
|
|
30
|
+
error = "";
|
|
31
|
+
private tui: TUI;
|
|
32
|
+
private game: TicTacToeGame;
|
|
33
|
+
private onClose: () => void;
|
|
34
|
+
private onMove: (cell: number) => void;
|
|
35
|
+
private onNew: () => void;
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
tui: TUI,
|
|
39
|
+
game: TicTacToeGame,
|
|
40
|
+
onClose: () => void,
|
|
41
|
+
onMove: (cell: number) => void,
|
|
42
|
+
onNew: () => void,
|
|
43
|
+
) {
|
|
44
|
+
this.tui = tui;
|
|
45
|
+
this.game = game;
|
|
46
|
+
this.onClose = onClose;
|
|
47
|
+
this.onMove = onMove;
|
|
48
|
+
this.onNew = onNew;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
handleInput(data: string): void {
|
|
52
|
+
if (matchesKey(data, "q")) return this.onClose();
|
|
53
|
+
if (matchesKey(data, "n")) return this.onNew();
|
|
54
|
+
if (this.game.winner() || this.game.turn !== "X") return;
|
|
55
|
+
|
|
56
|
+
const digit = Number(data);
|
|
57
|
+
if (Number.isInteger(digit) && digit >= 1 && digit <= 9)
|
|
58
|
+
return this.pick(digit - 1);
|
|
59
|
+
if (matchesKey(data, Key.enter) || matchesKey(data, Key.return))
|
|
60
|
+
return this.pick(this.cursor);
|
|
61
|
+
if (matchesKey(data, Key.up)) this.cursor = Math.max(0, this.cursor - 3);
|
|
62
|
+
if (matchesKey(data, Key.down)) this.cursor = Math.min(8, this.cursor + 3);
|
|
63
|
+
if (matchesKey(data, Key.left) && this.cursor % 3) this.cursor--;
|
|
64
|
+
if (matchesKey(data, Key.right) && this.cursor % 3 < 2) this.cursor++;
|
|
65
|
+
this.tui.requestRender();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
pick(cell: number): void {
|
|
69
|
+
this.cursor = cell;
|
|
70
|
+
this.onMove(cell);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
render(width: number): string[] {
|
|
74
|
+
return [
|
|
75
|
+
this.center("TIC TAC TOE - You(X) vs LLM(O)", width),
|
|
76
|
+
"",
|
|
77
|
+
...this.boardLines(width),
|
|
78
|
+
"",
|
|
79
|
+
this.center(`Status: ${this.game.status()}`, width),
|
|
80
|
+
this.error ? this.center(`Error: ${this.error}`, width) : "",
|
|
81
|
+
this.center("Arrows/1-9=move Enter=place n=new q=quit", width),
|
|
82
|
+
].map((line) => truncateToWidth(line, width));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
boardLines(width: number): string[] {
|
|
86
|
+
const rows = [0, 3, 6].map((start) =>
|
|
87
|
+
[0, 1, 2].map((i) => this.cell(start + i)).join("|"),
|
|
88
|
+
);
|
|
89
|
+
return rows.flatMap((row, i) =>
|
|
90
|
+
i < 2
|
|
91
|
+
? [this.center(row, width), this.center("---+---+---", width)]
|
|
92
|
+
: [this.center(row, width)],
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
cell(i: number): string {
|
|
97
|
+
const mark = this.game.board[i] ?? String(i + 1);
|
|
98
|
+
return i === this.cursor && this.game.turn === "X" && !this.game.winner()
|
|
99
|
+
? `[${mark}]`
|
|
100
|
+
: ` ${mark} `;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
center(text: string, width: number): string {
|
|
104
|
+
return (
|
|
105
|
+
" ".repeat(Math.max(0, Math.floor((width - visibleWidth(text)) / 2))) +
|
|
106
|
+
text
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setGame(game: TicTacToeGame): void {
|
|
111
|
+
this.game = game;
|
|
112
|
+
this.cursor = game.legalMoves()[0] ?? 4;
|
|
113
|
+
this.error = "";
|
|
114
|
+
this.tui.requestRender();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
setError(error: string): void {
|
|
118
|
+
this.error = error;
|
|
119
|
+
this.tui.requestRender();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
invalidate(): void {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default function ticTacToe(pi: ExtensionAPI): void {
|
|
126
|
+
let game: TicTacToeGame | null = null;
|
|
127
|
+
let active: TicTacToeComponent | null = null;
|
|
128
|
+
|
|
129
|
+
pi.on("session_start", (_event, ctx) => {
|
|
130
|
+
game = loadGame(ctx.sessionManager.getBranch());
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
pi.registerCommand("tic-tac-toe", {
|
|
134
|
+
description: "Play tic tac toe against the LLM",
|
|
135
|
+
handler: async (args, ctx) => {
|
|
136
|
+
if (ctx.mode !== "tui") {
|
|
137
|
+
ctx.ui.notify("Tic tac toe overlay only works in TUI mode", "error");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (!game || game.winner() || args.trim() === "new") {
|
|
141
|
+
game = new TicTacToeGame();
|
|
142
|
+
save();
|
|
143
|
+
}
|
|
144
|
+
await show(ctx);
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
pi.registerTool<typeof moveSchema, MoveDetails>({
|
|
149
|
+
name: "make_tic_tac_toe_move",
|
|
150
|
+
label: "Tic Tac Toe Move",
|
|
151
|
+
description: "Place O on the tic tac toe board for the LLM.",
|
|
152
|
+
parameters: moveSchema,
|
|
153
|
+
async execute(_id, params) {
|
|
154
|
+
if (!game || game.turn !== "O") throw new Error("Not the LLM turn.");
|
|
155
|
+
const cell = params.square - 1;
|
|
156
|
+
if (!game.play(cell, "O"))
|
|
157
|
+
throw new Error(`Invalid square. Legal: ${legal()}`);
|
|
158
|
+
save();
|
|
159
|
+
active?.setGame(game);
|
|
160
|
+
return result(
|
|
161
|
+
`Moved to ${params.square}. ${game.winner() ? game.status() : "X to move."}`,
|
|
162
|
+
params.square,
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
function handleHumanMove(cell: number): void {
|
|
168
|
+
if (!game?.play(cell, "X"))
|
|
169
|
+
return active?.setError(`Invalid square: ${cell + 1}`);
|
|
170
|
+
save();
|
|
171
|
+
active?.setGame(game);
|
|
172
|
+
if (game.winner()) return;
|
|
173
|
+
pi.sendUserMessage(
|
|
174
|
+
`You are O in tic tac toe. Pick the best legal square and call make_tic_tac_toe_move.\n\nBoard:\n${boardPrompt(game)}\n\nLegal squares: ${legal()}`,
|
|
175
|
+
{ deliverAs: "steer" },
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function show(ctx: ExtensionCommandContext): Promise<void> {
|
|
180
|
+
await ctx.ui.custom(
|
|
181
|
+
(tui, _theme, _keybindings, done) => {
|
|
182
|
+
active = new TicTacToeComponent(
|
|
183
|
+
tui,
|
|
184
|
+
game!,
|
|
185
|
+
() => {
|
|
186
|
+
active = null;
|
|
187
|
+
done(undefined);
|
|
188
|
+
},
|
|
189
|
+
handleHumanMove,
|
|
190
|
+
() => {
|
|
191
|
+
game = new TicTacToeGame();
|
|
192
|
+
save();
|
|
193
|
+
active?.setGame(game);
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
return active;
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
overlay: true,
|
|
200
|
+
overlayOptions: {
|
|
201
|
+
width: "44%",
|
|
202
|
+
maxHeight: "70%",
|
|
203
|
+
anchor: "center",
|
|
204
|
+
margin: { top: 1 },
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function save(): void {
|
|
211
|
+
if (game) pi.appendEntry(SAVE_TYPE, game.toState());
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function legal(): string {
|
|
215
|
+
return (
|
|
216
|
+
game
|
|
217
|
+
?.legalMoves()
|
|
218
|
+
.map((cell) => cell + 1)
|
|
219
|
+
.join(", ") ?? ""
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function result(text: string, square: number): AgentToolResult<MoveDetails> {
|
|
224
|
+
return {
|
|
225
|
+
content: [{ type: "text", text }],
|
|
226
|
+
details: { square, state: game!.toState(), status: game!.status() },
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function loadGame(
|
|
232
|
+
entries: Array<{ type: string; customType?: string; data?: unknown }>,
|
|
233
|
+
): TicTacToeGame | null {
|
|
234
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
235
|
+
const entry = entries[i];
|
|
236
|
+
if (entry.type === "custom" && entry.customType === SAVE_TYPE) {
|
|
237
|
+
try {
|
|
238
|
+
return TicTacToeGame.fromState(entry.data);
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-tic-tac-toe",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tic tac toe extension for Pi",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./extensions/index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node --test tests/*.test.ts",
|
|
9
|
+
"check": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"keywords": ["pi-package", "pi", "extension", "tic-tac-toe"],
|
|
12
|
+
"files": ["extensions", "README.md"],
|
|
13
|
+
"pi": {
|
|
14
|
+
"extensions": ["./extensions/index.ts"]
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
18
|
+
"@earendil-works/pi-tui": "*",
|
|
19
|
+
"typebox": "*"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@earendil-works/pi-coding-agent": "latest",
|
|
23
|
+
"@earendil-works/pi-tui": "latest",
|
|
24
|
+
"@types/node": "latest",
|
|
25
|
+
"typebox": "latest",
|
|
26
|
+
"typescript": "latest"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT"
|
|
29
|
+
}
|