pi-goal-pro 1.0.1 → 1.1.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,67 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.1.1] - 2026-06-11
11
+
12
+ ### Added
13
+
14
+ - Separate `test.yml` (push/PR) and `release.yml` (tag + workflow_dispatch) CI/CD pipelines
15
+ - `CHANGELOG.md` following Keep a Changelog format
16
+ - `README.ru.md` — Russian language documentation
17
+ - `workflow_dispatch` trigger for manual releases
18
+ - `npm publish --provenance` for supply chain security
19
+
20
+ ### Changed
21
+
22
+ - Split monolithic CI/CD into focused workflows (test quality vs. release)
23
+ - README installation order: npm install first, manual copy second
24
+ - Updated `.gitignore` to exclude `dist/` and `.env`
25
+ - Cleaner package.json scripts (`npm test` = only tests, `npm run test:all` = lint + tests)
26
+
27
+ ### Fixed
28
+
29
+ - Biome linter warnings (non-null assertions → type casts)
30
+ - Message renderer cognitive complexity
31
+
32
+ ## [1.1.0] - 2026-06-11
33
+
34
+ ### Added
35
+
36
+ - 48 unit tests covering all pure functions
37
+ - Biome linter + strict TypeScript configuration
38
+ - GitHub Actions CI/CD pipeline
39
+
40
+ ### Changed
41
+
42
+ - Split README into English (`README.md`) and Russian (`README.ru.md`)
43
+ - Simplified message renderer to reduce cognitive complexity
44
+
45
+ ### Fixed
46
+
47
+ - Non-null assertion warnings in `parseTokenBudget` and `parseMaxAutoTurns`
48
+
49
+ ## [1.0.1] - 2026-06-11
50
+
51
+ ### Added
52
+
53
+ - Initial npm publish
54
+
55
+ ## [1.0.0] - 2026-06-11
56
+
57
+ ### Added
58
+
59
+ - `/goal <objective> [--tokens N] [--max-turns N]` command
60
+ - `get_goal` and `update_goal` tools for the agent
61
+ - Auto-continuation loop with no-progress detection
62
+ - Evidence-based completion (evidence/blocker required)
63
+ - Token budget tracking with auto-pause
64
+ - User input suspends auto-continuation
65
+ - Session entry persistence (survives reload, compaction, tree navigation)
66
+ - Footer status bar
67
+ - Bilingual README (English + Russian)
package/README.md CHANGED
@@ -22,15 +22,18 @@ Then walk away. The agent keeps going. When it's done, it reports with evidence.
22
22
 
23
23
  ## Installation
24
24
 
25
+ ### Via npm (recommended)
26
+
25
27
  ```bash
26
- mkdir -p ~/.pi/agent/extensions/pi-goal-pro
27
- # Copy the extension file:
28
- cp ./index.ts ~/.pi/agent/extensions/pi-goal-pro/
28
+ pi install npm:pi-goal-pro
29
+ /reload
29
30
  ```
30
31
 
31
- Then reload Pi:
32
+ ### Manual
32
33
 
33
- ```
34
+ ```bash
35
+ mkdir -p ~/.pi/agent/extensions/pi-goal-pro
36
+ cp ./index.ts ~/.pi/agent/extensions/pi-goal-pro/
34
37
  /reload
35
38
  ```
36
39
 
@@ -283,116 +286,3 @@ Inspired by and building upon:
283
286
  ## License
284
287
 
285
288
  MIT
286
-
287
- ---
288
-
289
- # pi-goal-pro 🎯
290
-
291
- > Персистентные автономные цели для [Pi](https://pi.dev) — с детекцией отсутствия прогресса, завершением на основе доказательств, бюджетом токенов и автопродолжением.
292
-
293
- Задай долгоживущую цель — и агент будет работать автономно, пока не закончит, не будет приостановлен или не упрётся в ограничение. Без необходимости повторять промпт каждый turn.
294
-
295
- ```bash
296
- /goal Переписать auth модуль на JWT с нормальной обработкой ошибок
297
- ```
298
-
299
- Можно отойти от клавиатуры. Агент продолжает сам. Когда закончит — отчитается с доказательствами.
300
-
301
- ---
302
-
303
- ## Установка
304
-
305
- ```bash
306
- mkdir -p ~/.pi/agent/extensions/pi-goal-pro
307
- # Скопировать файл расширения:
308
- cp ./index.ts ~/.pi/agent/extensions/pi-goal-pro/
309
- ```
310
-
311
- Перезагрузить Pi:
312
-
313
- ```
314
- /reload
315
- ```
316
-
317
- Проверить что загрузилось:
318
-
319
- ```
320
- /goal status
321
- ```
322
-
323
- ---
324
-
325
- ## Быстрый старт
326
-
327
- Задай цель — агент начнёт работать:
328
-
329
- ```text
330
- /goal Добавить retry логику в API клиент с экспоненциальной задержкой
331
- ```
332
-
333
- Агент начинает немедленно. Статус в футере:
334
-
335
- ```
336
- 🎯 goal active (1.2K/50K) ← статус в футере
337
- ```
338
-
339
- Управление жизненным циклом:
340
-
341
- ```text
342
- /goal status # Показать текущее состояние
343
- /goal pause # Приостановить активную цель
344
- /goal resume # Возобновить приостановленную
345
- /goal clear # Удалить все цели
346
- ```
347
-
348
- ---
349
-
350
- ## Инструменты агента
351
-
352
- Когда цель активна, агент получает два инструмента:
353
-
354
- **`get_goal`** — прочитать состояние цели.
355
-
356
- **`update_goal`** — завершить цель (с доказательствами) или признать недостижимой (с причиной):
357
-
358
- ```typescript
359
- // Завершено — нужно подтверждение
360
- update_goal({
361
- status: "complete",
362
- evidence: "JWT middleware реализован, 12 тестов проходят, CI без регрессий"
363
- })
364
-
365
- // Недостижимо — нужно объяснение
366
- update_goal({
367
- status: "unmet",
368
- blocker: "Заблокировано решением по JWT библиотеке — ожидание security review"
369
- })
370
- ```
371
-
372
- ---
373
-
374
- ## Как это работает
375
-
376
- Состояние цели хранится в session entry (custom type `pi-goal-pro`). Оно переживает:
377
- - Перезагрузку сессии (`/reload`)
378
- - Компактизацию (compaction)
379
- - Навигацию по дереву сессии (`/tree`)
380
- - Возобновление сессии
381
-
382
- Состояние привязано к ветке — при переходе на другую ветку восстанавливается состояние целей для этой ветки.
383
-
384
- ---
385
-
386
- ## Философия дизайна
387
-
388
- 1. **Пользователь владеет целью** — Агент не может молча изменить objective.
389
- 2. **Доказательства перед завершением** — Агент должен верифицировать по реальным артефактам.
390
- 3. **Никаких бесконечных циклов** — Детекция отсутствия прогресса, лимит turn-ов и бюджет токенов.
391
- 4. **Ввод пользователя приостанавливает** — Когда ты печатаешь, автопродолжение ставится на паузу.
392
- 5. **Состояние привязано к ветке** — `/tree` в другую точку восстанавливает цели этой точки.
393
-
394
- ---
395
-
396
- ## Лицензия
397
-
398
- MIT
package/README.ru.md ADDED
@@ -0,0 +1,252 @@
1
+
2
+ <p align="center">
3
+ <img src="https://img.shields.io/badge/pi-extension-8B5CF6?style=flat-square&logo=pi-hole&logoColor=white" alt="pi extension">
4
+ <img src="https://img.shields.io/npm/v/pi-goal-pro?style=flat-square&color=cb3837" alt="npm">
5
+ <img src="https://img.shields.io/github/actions/workflow/status/izzzzzi/pi-goal-pro/ci.yml?style=flat-square&branch=main" alt="CI">
6
+ <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="MIT">
7
+ </p>
8
+
9
+ # pi-goal-pro 🎯
10
+
11
+ > Персистентные автономные цели для [Pi](https://pi.dev) — с детекцией отсутствия прогресса, завершением на основе доказательств, бюджетом токенов и автопродолжением.
12
+
13
+ Задай долгоживущую цель — и агент будет работать автономно, пока не закончит, не будет приостановлен или не упрётся в ограничение. Без необходимости повторять промпт каждый turn.
14
+
15
+ ```bash
16
+ /goal Переписать auth модуль на JWT с нормальной обработкой ошибок
17
+ ```
18
+
19
+ Можно отойти от клавиатуры. Агент продолжает сам. Когда закончит — отчитается с доказательствами.
20
+
21
+ ---
22
+
23
+ ## Установка
24
+
25
+ ```bash
26
+ pi install npm:pi-goal-pro
27
+ ```
28
+
29
+ Или вручную:
30
+
31
+ ```bash
32
+ mkdir -p ~/.pi/agent/extensions/pi-goal-pro
33
+ cp ./index.ts ~/.pi/agent/extensions/pi-goal-pro/
34
+ ```
35
+
36
+ Перезагрузить Pi:
37
+
38
+ ```
39
+ /reload
40
+ ```
41
+
42
+ Проверить что загрузилось:
43
+
44
+ ```
45
+ /goal status
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Быстрый старт
51
+
52
+ Задай цель — агент начнёт работать:
53
+
54
+ ```text
55
+ /goal Добавить retry логику в API клиент с экспоненциальной задержкой
56
+ ```
57
+
58
+ Агент начинает немедленно. Статус в футере:
59
+
60
+ ```
61
+ 🎯 goal active (1.2K/50K) ← статус в футере
62
+ ```
63
+
64
+ Управление жизненным циклом:
65
+
66
+ ```text
67
+ /goal status # Показать текущее состояние
68
+ /goal pause # Приостановить активную цель
69
+ /goal resume # Возобновить приостановленную
70
+ /goal clear # Удалить все цели
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Возможности
76
+
77
+ ### 🎯 Установка цели
78
+
79
+ ```text
80
+ /goal Переписать auth модуль
81
+
82
+ # С бюджетом токенов (автопауза при превышении):
83
+ /goal Переписать auth модуль --tokens 100k
84
+
85
+ # С лимитом авто-продолжений:
86
+ /goal Переписать auth модуль --max-turns 10
87
+ ```
88
+
89
+ ### 🤖 Инструменты агента
90
+
91
+ Когда цель активна, агент получает два инструмента:
92
+
93
+ **`get_goal`** — прочитать состояние цели:
94
+
95
+ ```json
96
+ {
97
+ "active": {
98
+ "objective": "Переписать auth модуль на JWT",
99
+ "status": "active",
100
+ "tokens_used": 12400,
101
+ "token_budget": 50000,
102
+ "remaining_tokens": 37600,
103
+ "time_used_seconds": 89,
104
+ "auto_turns": 3,
105
+ "max_auto_turns": 25
106
+ }
107
+ }
108
+ ```
109
+
110
+ **`update_goal`** — завершить цель или признать недостижимой:
111
+
112
+ ```typescript
113
+ // Завершено — нужно подтверждение
114
+ update_goal({
115
+ status: "complete",
116
+ evidence: "JWT middleware реализован, 12 тестов проходят, CI без регрессий"
117
+ })
118
+
119
+ // Недостижимо — нужно объяснение
120
+ update_goal({
121
+ status: "unmet",
122
+ blocker: "Заблокировано решением по JWT библиотеке — ожидание security review"
123
+ })
124
+ ```
125
+
126
+ ### 🔄 Автопродолжение
127
+
128
+ После каждого turn агента расширение автоматически отправляет continuation prompt, если:
129
+ - Цель всё ещё `active`
130
+ - Предыдущий turn был goal-driven
131
+ - Пользователь ничего не вводил (ввод приостанавливает автопродолжение)
132
+ - Не достигнуты лимиты
133
+
134
+ ### 🛡️ Детекция отсутствия прогресса
135
+
136
+ Если агент генерирует очень мало выходных токенов (по умолчанию <50) 2 раза подряд, цель автоматически приостанавливается:
137
+
138
+ ```
139
+ ⏸ Goal paused (no progress for 2 turns). Use /goal resume to continue.
140
+ ```
141
+
142
+ Это предотвращает бесконечные циклы, когда агент просто подтверждает получение без реального прогресса.
143
+
144
+ ### 💰 Бюджет токенов
145
+
146
+ Установи бюджет с `--tokens`:
147
+
148
+ ```text
149
+ /goal Написать документацию для всех API эндпоинтов --tokens 100k
150
+ ```
151
+
152
+ Когда бюджет исчерпан, цель приостанавливается с wrap-up промптом — агент подводит итог.
153
+
154
+ ### 📋 Завершение с доказательствами
155
+
156
+ Агент обязан предоставить конкретные доказательства перед отметкой цели как выполненной. Это предотвращает преждевременные "done" и гарантирует верификацию по реальным файлам, тестам и выводам команд.
157
+
158
+ ---
159
+
160
+ ## Команды
161
+
162
+ | Команда | Описание |
163
+ |---------|----------|
164
+ | `/goal <objective>` | Установить новую цель |
165
+ | `/goal <text> --tokens N` | Цель с бюджетом токенов |
166
+ | `/goal <text> --max-turns N` | Цель с лимитом авто-продолжений |
167
+ | `/goal status` | Показать состояние |
168
+ | `/goal pause` | Приостановить |
169
+ | `/goal resume` | Возобновить |
170
+ | `/goal clear` | Удалить все |
171
+ | `/goal help` | Справка |
172
+
173
+ ---
174
+
175
+ ## Как это работает
176
+
177
+ ```
178
+ /goal Переписать auth модуль
179
+
180
+
181
+ ✓ Цель создана, сохранена в session entry
182
+ ✓ Агент получил get_goal + update_goal
183
+ ✓ Первое продолжение запущено немедленно
184
+
185
+
186
+ ┌── Цикл автопродолжения ───────────────────┐
187
+ │ │
188
+ │ turn_start → turn_end → agent_end │
189
+ │ │ │
190
+ │ ┌─────────┴─────────┐ │
191
+ │ │ Цель активна? │ │
192
+ │ │ Нет прогресса? │ │
193
+ │ │ Пользователь ввёл?│ │
194
+ │ │ Бюджет исчерпан? │ │
195
+ │ │ Лимит turn-ов? │ │
196
+ │ └─────────┬─────────┘ │
197
+ │ │ │
198
+ │ ┌─────────┴──────────┐ │
199
+ │ │ Да → continuation │ │
200
+ │ │ Нет → stop/pause │ │
201
+ │ └────────────────────┘ │
202
+ │ │
203
+ └───────────────────────────────────────────┘
204
+
205
+
206
+ Агент вызывает update_goal({ status: "complete", evidence })
207
+ → Цель архивирована, агент остановлен
208
+ ```
209
+
210
+ ### Сохранение состояния
211
+
212
+ Состояние цели хранится в session entry (custom type `pi-goal-pro`). Оно переживает:
213
+ - Перезагрузку сессии (`/reload`)
214
+ - Компактизацию (compaction)
215
+ - Навигацию по дереву сессии (`/tree`)
216
+ - Возобновление сессии
217
+
218
+ Состояние привязано к ветке — при переходе на другую ветку восстанавливается состояние целей для этой ветки.
219
+
220
+ ---
221
+
222
+ ## Философия дизайна
223
+
224
+ 1. **Пользователь владеет целью** — Агент не может молча изменить objective.
225
+ 2. **Доказательства перед завершением** — Агент должен верифицировать по реальным артефактам.
226
+ 3. **Никаких бесконечных циклов** — Детекция отсутствия прогресса, лимит turn-ов и бюджет токенов.
227
+ 4. **Ввод пользователя приостанавливает** — Когда ты печатаешь, автопродолжение ставится на паузу.
228
+ 5. **Состояние привязано к ветке** — `/tree` в другую точку восстанавливает цели этой точки.
229
+
230
+ ---
231
+
232
+ ## Разработка
233
+
234
+ Расширение в одном файле — не требует сборки. Правишь `index.ts`, затем `/reload`.
235
+
236
+ Запуск без установки:
237
+
238
+ ```bash
239
+ pi -e ~/.pi/agent/extensions/pi-goal-pro/index.ts
240
+ ```
241
+
242
+ Тесты:
243
+
244
+ ```bash
245
+ node --test tests/
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Лицензия
251
+
252
+ MIT
package/index.ts CHANGED
@@ -22,7 +22,6 @@
22
22
 
23
23
  import { StringEnum } from '@earendil-works/pi-ai';
24
24
  import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
25
- import { matchesKey } from '@earendil-works/pi-tui';
26
25
  import { Type } from 'typebox';
27
26
 
28
27
  // ─── Types ───────────────────────────────────────────────────────────────
@@ -119,7 +118,7 @@ function parseTokenBudget(input: string): { objective: string; tokenBudget: numb
119
118
  if (!Number.isFinite(num) || num <= 0) return { objective: input.trim(), tokenBudget: null };
120
119
  const mult = m[2]?.toLowerCase() === 'm' ? 1_000_000 : m[2]?.toLowerCase() === 'k' ? 1_000 : 1;
121
120
  const budget = Math.round(num * mult);
122
- const idx = m.index!;
121
+ const idx = m.index as number;
123
122
  const objective = (input.slice(0, idx) + input.slice(idx + m[0].length)).trim();
124
123
  return { objective, tokenBudget: budget };
125
124
  }
@@ -129,7 +128,7 @@ function parseMaxAutoTurns(input: string): { rest: string; maxAutoTurns: number
129
128
  if (!m) return { rest: input.trim(), maxAutoTurns: null };
130
129
  const turns = Number.parseInt(m[1], 10);
131
130
  if (!Number.isFinite(turns) || turns <= 0) return { rest: input.trim(), maxAutoTurns: null };
132
- const idx = m.index!;
131
+ const idx = m.index as number;
133
132
  const rest = (input.slice(0, idx) + input.slice(idx + m[0].length)).trim();
134
133
  return { rest, maxAutoTurns: turns };
135
134
  }
@@ -692,65 +691,50 @@ Do not call update_goal unless the goal is actually complete.`;
692
691
 
693
692
  // ── Message renderer for goal events ───────────────────────────────
694
693
 
694
+ const GOAL_KIND_LABELS: Record<string, (th: typeof theme) => string> = {
695
+ active: (th) => th.fg('accent', 'active'),
696
+ continuation: (th) => th.fg('muted', 'continuing'),
697
+ paused: (th) => th.fg('warning', 'paused'),
698
+ resumed: (th) => th.fg('accent', 'resumed'),
699
+ cleared: (th) => th.fg('dim', 'cleared'),
700
+ budget_limited: (th) => th.fg('warning', 'budget'),
701
+ complete: (th) => th.fg('success', 'achieved'),
702
+ unmet: (th) => th.fg('error', 'unmet'),
703
+ };
704
+
695
705
  pi.registerMessageRenderer(`${GOAL_STORAGE_TYPE}:event`, (message, options, theme) => {
696
706
  const details = message.details as GoalEvent | undefined;
697
707
  const kind = details?.kind ?? 'continuation';
698
708
  const state = details?.goal ?? null;
699
709
 
700
- const box = new (class {
701
- private children: import('@earendil-works/pi-tui').Component[] = [];
702
-
703
- render(_width: number): string[] {
704
- const lines: string[] = [];
705
- const isExpanded = options.expanded;
706
- const prefix = theme.fg('accent', theme.bold('Goal'));
707
- const kindLabel = this.kindLabel(kind, theme);
708
- const statusText = theme.fg('dim', isExpanded ? '' : '(ctrl+o to expand)');
709
-
710
- lines.push(`${prefix} ${kindLabel} ${!isExpanded ? statusText : ''}`);
711
-
712
- if (isExpanded && state) {
713
- lines.push(`${theme.fg('dim', ' Status: ')}${theme.fg('text', kind)}`);
714
- lines.push(`${theme.fg('dim', ' Goal: ')}${theme.fg('text', state.objective)}`);
715
- if (state.completionEvidence) {
716
- lines.push(`${theme.fg('dim', ' Evidence: ')}${theme.fg('success', state.completionEvidence)}`);
717
- }
718
- if (state.blocker) {
719
- lines.push(`${theme.fg('dim', ' Blocker: ')}${theme.fg('warning', state.blocker)}`);
720
- }
721
- const usage = state.tokenBudget
722
- ? `${formatTokens(state.tokensUsed)}/${formatTokens(state.tokenBudget)}`
723
- : formatDuration(state.timeUsedMs);
724
- lines.push(`${theme.fg('dim', ' Usage: ')}${theme.fg('text', usage)}`);
725
- }
726
-
727
- return lines;
728
- }
710
+ const renderGoalEvent = (_width: number): string[] => {
711
+ const lines: string[] = [];
712
+ const isExpanded = options.expanded;
713
+ const prefix = theme.fg('accent', theme.bold('Goal'));
714
+ const kindLabel = (GOAL_KIND_LABELS[kind] ?? ((th: typeof theme) => th.fg('text', kind)))(theme);
715
+ const statusText = theme.fg('dim', isExpanded ? '' : '(ctrl+o to expand)');
729
716
 
730
- invalidate(): void {}
717
+ lines.push(`${prefix} ${kindLabel} ${!isExpanded ? statusText : ''}`);
731
718
 
732
- handleInput?(data: string): void {
733
- if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {
734
- // no-op, let parent handle
719
+ if (isExpanded && state) {
720
+ lines.push(`${theme.fg('dim', ' Status: ')}${theme.fg('text', kind)}`);
721
+ lines.push(`${theme.fg('dim', ' Goal: ')}${theme.fg('text', state.objective)}`);
722
+ if (state.completionEvidence) {
723
+ lines.push(`${theme.fg('dim', ' Evidence: ')}${theme.fg('success', state.completionEvidence)}`);
735
724
  }
725
+ if (state.blocker) {
726
+ lines.push(`${theme.fg('dim', ' Blocker: ')}${theme.fg('warning', state.blocker)}`);
727
+ }
728
+ const usage = state.tokenBudget
729
+ ? `${formatTokens(state.tokensUsed)}/${formatTokens(state.tokenBudget)}`
730
+ : formatDuration(state.timeUsedMs);
731
+ lines.push(`${theme.fg('dim', ' Usage: ')}${theme.fg('text', usage)}`);
736
732
  }
737
733
 
738
- private kindLabel(k: string, th: typeof theme): string {
739
- const labels: Record<string, string> = {
740
- active: th.fg('accent', 'active'),
741
- continuation: th.fg('muted', 'continuing'),
742
- paused: th.fg('warning', 'paused'),
743
- resumed: th.fg('accent', 'resumed'),
744
- cleared: th.fg('dim', 'cleared'),
745
- budget_limited: th.fg('warning', 'budget'),
746
- complete: th.fg('success', 'achieved'),
747
- unmet: th.fg('error', 'unmet'),
748
- };
749
- return labels[k] ?? k;
750
- }
751
- })();
734
+ return lines;
735
+ };
752
736
 
753
- return box;
737
+ return { render: renderGoalEvent, invalidate: () => {} };
754
738
  });
755
739
 
756
740
  // ── Commands ───────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-pro",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "Persistent autonomous goals for Pi — with no-progress detection, evidence-based completion, token budgets, and auto-continuation",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -8,6 +8,8 @@
8
8
  "files": [
9
9
  "index.ts",
10
10
  "README.md",
11
+ "README.ru.md",
12
+ "CHANGELOG.md",
11
13
  "LICENSE"
12
14
  ],
13
15
  "keywords": [
@@ -53,11 +55,12 @@
53
55
  "tsx": "^4.0.0"
54
56
  },
55
57
  "scripts": {
56
- "test": "npm run check && npm run test:unit",
58
+ "test": "npm run test:unit",
57
59
  "test:unit": "tsx --test tests/index.test.ts",
60
+ "test:all": "npm run lint && npm run test:unit",
61
+ "lint": "biome lint .",
58
62
  "check": "biome check .",
59
63
  "format": "biome format --write .",
60
- "lint": "biome lint .",
61
64
  "dev": "pi -e ./index.ts"
62
65
  }
63
66
  }