@herbertgao/pi-extensions 2026.8.7 → 2026.8.8
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 +7 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +63 -15
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +78 -16
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +13 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +17 -6
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +4 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +154 -96
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +3 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +7 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +3 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +137 -96
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +44 -32
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +56 -34
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +50 -4
- package/node_modules/@narumitw/pi-btw/package.json +2 -2
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
- package/node_modules/pi-lens/CHANGELOG.md +90 -0
- package/node_modules/pi-lens/README.md +106 -0
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +97 -6
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +16 -6
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +14 -0
- package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +22 -1
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +8 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +10 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +19 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +35 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +8 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +110 -23
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +117 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +16 -14
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +8 -9
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +22 -0
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +5 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +7 -2
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +27 -2
- package/node_modules/pi-lens/dist/clients/lsp/client.js +144 -5
- package/node_modules/pi-lens/dist/clients/lsp/index.js +151 -6
- package/node_modules/pi-lens/dist/clients/module-report.js +14 -1
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +18 -9
- package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +7 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +88 -19
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +44 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +41 -0
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +47 -2
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
- package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
- package/node_modules/pi-lens/dist/index.js +1224 -393
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +10 -3
- package/node_modules/pi-lens/package.json +18 -5
- package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
- package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
- package/node_modules/pi-web-access/CHANGELOG.md +14 -0
- package/node_modules/pi-web-access/README.md +6 -5
- package/node_modules/pi-web-access/chrome-cookies.ts +86 -15
- package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
- package/node_modules/pi-web-access/extract.ts +12 -2
- package/node_modules/pi-web-access/firecrawl.ts +18 -2
- package/node_modules/pi-web-access/github-extract.ts +65 -36
- package/node_modules/pi-web-access/index.ts +19 -4
- package/node_modules/pi-web-access/openai-search.ts +17 -6
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/pdf-extract.ts +18 -5
- package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
- package/package.json +9 -9
|
@@ -61,6 +61,47 @@ const OVERFLOW_UP = "↑";
|
|
|
61
61
|
const OVERFLOW_DOWN = "↓";
|
|
62
62
|
const OVERFLOW_BOTH = "↕";
|
|
63
63
|
|
|
64
|
+
/** No-overflow path: append the residual spacer rows after the footer. */
|
|
65
|
+
function renderFitsTerminal(natural: string[], spacerRows: number): string[] {
|
|
66
|
+
return spacerRows > 0 ? [...natural, ...Array<string>(spacerRows).fill("")] : natural;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Terminal too small for any middle content — show just chrome, hard-clamped to termRows. */
|
|
70
|
+
function renderChromeOnly(natural: string[], topFixed: number, bottomFixed: number, termRows: number): string[] {
|
|
71
|
+
const chromeOnly = [...natural.slice(0, topFixed), ...natural.slice(natural.length - bottomFixed)];
|
|
72
|
+
return chromeOnly.length > termRows ? chromeOnly.slice(0, termRows) : chromeOnly;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Scroll window start, centered on the focused option; top-anchored when there is no interactive focus. */
|
|
76
|
+
function computeScrollStart(
|
|
77
|
+
bodyRange: [number, number] | undefined,
|
|
78
|
+
headingCount: number,
|
|
79
|
+
availableMiddle: number,
|
|
80
|
+
middleRows: number,
|
|
81
|
+
): number {
|
|
82
|
+
if (!bodyRange) return 0;
|
|
83
|
+
const focusedRowInMiddle = headingCount + bodyRange[0];
|
|
84
|
+
const focusedHeight = bodyRange[1] - bodyRange[0];
|
|
85
|
+
// Center the focused item vertically in the available middle space.
|
|
86
|
+
const idealStart = focusedRowInMiddle - Math.floor(Math.max(0, availableMiddle - focusedHeight) / 2);
|
|
87
|
+
return Math.max(0, Math.min(idealStart, middleRows - availableMiddle));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Mark the scroll window edges with overflow arrows; combined ↕ on a single-row middle. */
|
|
91
|
+
function decorateOverflow(scrollableMiddle: string[], hasUp: boolean, hasDown: boolean, theme: Theme): void {
|
|
92
|
+
if (hasUp && hasDown && scrollableMiddle.length === 1) {
|
|
93
|
+
// Single-row middle: combined ↕ avoids the prior collision where ↓ overwrote ↑.
|
|
94
|
+
scrollableMiddle[0] = theme.fg("dim", OVERFLOW_BOTH);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (hasUp && scrollableMiddle.length > 0) {
|
|
98
|
+
scrollableMiddle[0] = theme.fg("dim", OVERFLOW_UP);
|
|
99
|
+
}
|
|
100
|
+
if (hasDown && scrollableMiddle.length > 0) {
|
|
101
|
+
scrollableMiddle[scrollableMiddle.length - 1] = theme.fg("dim", OVERFLOW_DOWN);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
64
105
|
export type DialogState = QuestionnaireState;
|
|
65
106
|
|
|
66
107
|
/** Per-tick projection of dialog state. Written by the adapter; read by the strategy thunk. */
|
|
@@ -127,6 +168,7 @@ export class DialogView implements StatefulView<DialogProps> {
|
|
|
127
168
|
theme: config.theme,
|
|
128
169
|
questions: config.questions,
|
|
129
170
|
submitPicker: config.submitPicker,
|
|
171
|
+
notesInput: config.notesInput,
|
|
130
172
|
})
|
|
131
173
|
: undefined;
|
|
132
174
|
this.maxFooterRowCount = Math.max(this.questionStrategy.footerRowCount, this.submitStrategy?.footerRowCount ?? 0);
|
|
@@ -173,48 +215,28 @@ export class DialogView implements StatefulView<DialogProps> {
|
|
|
173
215
|
const termRows = this.config.getTerminalRows();
|
|
174
216
|
|
|
175
217
|
if (natural.length + spacerRows <= termRows) {
|
|
176
|
-
|
|
177
|
-
return spacerRows > 0 ? [...natural, ...Array<string>(spacerRows).fill("")] : natural;
|
|
218
|
+
return renderFitsTerminal(natural, spacerRows);
|
|
178
219
|
}
|
|
179
220
|
|
|
180
221
|
// OVERFLOW — apply 3-region partition with scroll-to-focus.
|
|
181
222
|
const availableMiddle = Math.max(0, termRows - topFixed - bottomFixed);
|
|
182
223
|
if (availableMiddle === 0) {
|
|
183
|
-
|
|
184
|
-
const chromeOnly = [...natural.slice(0, topFixed), ...natural.slice(natural.length - bottomFixed)];
|
|
185
|
-
return chromeOnly.length > termRows ? chromeOnly.slice(0, termRows) : chromeOnly;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const bodyRange = strategy.focusedItemRowRange(width, state);
|
|
189
|
-
|
|
190
|
-
// Compute scroll window centered on the focused option.
|
|
191
|
-
let scrollStart: number;
|
|
192
|
-
if (bodyRange) {
|
|
193
|
-
const focusedRowInMiddle = headingCount + bodyRange[0];
|
|
194
|
-
const focusedHeight = bodyRange[1] - bodyRange[0];
|
|
195
|
-
// Center the focused item vertically in the available middle space.
|
|
196
|
-
const idealStart = focusedRowInMiddle - Math.floor(Math.max(0, availableMiddle - focusedHeight) / 2);
|
|
197
|
-
scrollStart = Math.max(0, Math.min(idealStart, middleRows - availableMiddle));
|
|
198
|
-
} else {
|
|
199
|
-
// No interactive focus (submit tab) — top-anchor the middle.
|
|
200
|
-
scrollStart = 0;
|
|
224
|
+
return renderChromeOnly(natural, topFixed, bottomFixed, termRows);
|
|
201
225
|
}
|
|
202
226
|
|
|
227
|
+
const scrollStart = computeScrollStart(
|
|
228
|
+
strategy.focusedItemRowRange(width, state),
|
|
229
|
+
headingCount,
|
|
230
|
+
availableMiddle,
|
|
231
|
+
middleRows,
|
|
232
|
+
);
|
|
203
233
|
const scrollableMiddle = natural.slice(topFixed + scrollStart, topFixed + scrollStart + availableMiddle);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
} else {
|
|
211
|
-
if (hasUp && scrollableMiddle.length > 0) {
|
|
212
|
-
scrollableMiddle[0] = this.config.theme.fg("dim", OVERFLOW_UP);
|
|
213
|
-
}
|
|
214
|
-
if (hasDown && scrollableMiddle.length > 0) {
|
|
215
|
-
scrollableMiddle[scrollableMiddle.length - 1] = this.config.theme.fg("dim", OVERFLOW_DOWN);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
234
|
+
decorateOverflow(
|
|
235
|
+
scrollableMiddle,
|
|
236
|
+
scrollStart > 0,
|
|
237
|
+
scrollStart + availableMiddle < middleRows,
|
|
238
|
+
this.config.theme,
|
|
239
|
+
);
|
|
218
240
|
|
|
219
241
|
const result = [
|
|
220
242
|
...natural.slice(0, topFixed),
|
|
@@ -25,6 +25,9 @@ import type { StatefulView } from "./stateful-view.js";
|
|
|
25
25
|
import type { TabComponents } from "./tab-components.js";
|
|
26
26
|
|
|
27
27
|
const NOTES_HEADER = "Notes:";
|
|
28
|
+
const GLOBAL_NOTES_HEADER = "Global note:";
|
|
29
|
+
const REVIEW_GLOBAL_HINT = "n to add a note";
|
|
30
|
+
const REVIEW_NOTE_LABEL = "Note";
|
|
28
31
|
|
|
29
32
|
/**
|
|
30
33
|
* Single-row, width-clipped chrome cell. The footer row count is invariant
|
|
@@ -160,10 +163,12 @@ export interface SubmitTabStrategyConfig {
|
|
|
160
163
|
theme: Theme;
|
|
161
164
|
questions: readonly QuestionData[];
|
|
162
165
|
submitPicker: Component | undefined;
|
|
166
|
+
/** Shared notes Editor — mounted into midRows while the global-note editor is open on this tab. */
|
|
167
|
+
notesInput: Editor;
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
export class SubmitTabStrategy implements TabContentStrategy {
|
|
166
|
-
/** Spacer(1) + Text(prompt, 1) +
|
|
171
|
+
/** Spacer(1) + Text(prompt, 1) + submitPicker(2) + OneLineClippedText(hint, 1) = 5 rendered rows. Fallback path lands at 5 via 2 Spacer(1)s in the picker slot. */
|
|
167
172
|
readonly footerRowCount = 5;
|
|
168
173
|
|
|
169
174
|
constructor(private readonly config: SubmitTabStrategyConfig) {}
|
|
@@ -191,6 +196,18 @@ export class SubmitTabStrategy implements TabContentStrategy {
|
|
|
191
196
|
c.addChild(new Text(this.config.theme.fg("dim", ` notes: ${a.notes}`), 1, 0));
|
|
192
197
|
}
|
|
193
198
|
}
|
|
199
|
+
// Committed global note (#182) as a review entry — pressing `n` gets visible
|
|
200
|
+
// feedback and the note is reviewable before submit. Same presence predicate as
|
|
201
|
+
// the reducer's doneFor lift. Hidden while the editor is open: the midRows editor
|
|
202
|
+
// (seeded with this text) is the live surface then, and a stale copy above it
|
|
203
|
+
// would read as a second note.
|
|
204
|
+
const globalNote = state.notesByTab.get(this.config.questions.length);
|
|
205
|
+
if (!state.notesVisible && globalNote && globalNote.length > 0) {
|
|
206
|
+
c.addChild(new Text(this.config.theme.fg("muted", ` ● ${t("review.note_label", REVIEW_NOTE_LABEL)}`), 1, 0));
|
|
207
|
+
c.addChild(
|
|
208
|
+
new Text(` ${this.config.theme.fg("muted", "→")} ${this.config.theme.fg("text", globalNote)}`, 1, 0),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
194
211
|
return c;
|
|
195
212
|
}
|
|
196
213
|
|
|
@@ -198,8 +215,17 @@ export class SubmitTabStrategy implements TabContentStrategy {
|
|
|
198
215
|
return this.bodyComponent(state).render(width).length;
|
|
199
216
|
}
|
|
200
217
|
|
|
201
|
-
midRows(
|
|
202
|
-
|
|
218
|
+
midRows(state: DialogState): Component[] {
|
|
219
|
+
// notesVisible-gated mirror of QuestionTabStrategy.midRows: while the global-note
|
|
220
|
+
// editor is open, the shared notesInput mounts below the answer summary under its
|
|
221
|
+
// own header. The pseudo-index draft (notesByTab[questions.length]) is reducer-owned
|
|
222
|
+
// state; the strategy only renders.
|
|
223
|
+
if (!state.notesVisible) return [];
|
|
224
|
+
return [
|
|
225
|
+
new Text(this.config.theme.fg("muted", t("notes.global_header", GLOBAL_NOTES_HEADER)), 1, 0),
|
|
226
|
+
this.config.notesInput,
|
|
227
|
+
new Spacer(1),
|
|
228
|
+
];
|
|
203
229
|
}
|
|
204
230
|
|
|
205
231
|
footerRows(state: DialogState): Component[] {
|
|
@@ -217,7 +243,7 @@ export class SubmitTabStrategy implements TabContentStrategy {
|
|
|
217
243
|
"warning",
|
|
218
244
|
`${t("review.incomplete", INCOMPLETE_WARNING_PREFIX)} ${missing.join(", ")}`,
|
|
219
245
|
);
|
|
220
|
-
const out: Component[] = [new Spacer(1), new Text(promptText, 1, 0)
|
|
246
|
+
const out: Component[] = [new Spacer(1), new Text(promptText, 1, 0)];
|
|
221
247
|
if (this.config.submitPicker) {
|
|
222
248
|
out.push(this.config.submitPicker);
|
|
223
249
|
} else {
|
|
@@ -225,6 +251,12 @@ export class SubmitTabStrategy implements TabContentStrategy {
|
|
|
225
251
|
out.push(new Spacer(1));
|
|
226
252
|
out.push(new Spacer(1));
|
|
227
253
|
}
|
|
254
|
+
// Bottom key-hint row, mirroring QuestionTabStrategy's footer idiom (one dim
|
|
255
|
+
// `·`-joined line below everything) — the prompt reads straight into its picker
|
|
256
|
+
// with no hint wedged between them. Always present so footerRowCount stays
|
|
257
|
+
// exactly 5. OneLineClippedText (not pi-tui Text) — a wrapped hint would inflate
|
|
258
|
+
// the footer past footerRowCount and desync the chrome's cross-tab height math.
|
|
259
|
+
out.push(new OneLineClippedText(this.config.theme.fg("dim", buildSubmitHintText(state)), 1));
|
|
228
260
|
return out;
|
|
229
261
|
}
|
|
230
262
|
|
|
@@ -268,3 +300,17 @@ export function buildHintText(
|
|
|
268
300
|
if (state.inputMode) parts.push(t("hint.clear", HINT_PART_CLEAR));
|
|
269
301
|
return parts.join(" · ");
|
|
270
302
|
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Submit-tab counterpart of `buildHintText` — the same `·`-joined bottom-row idiom.
|
|
306
|
+
* Resting: Enter · ↑/↓ · `n to add a note` · Esc. While the global-note editor is
|
|
307
|
+
* open the note part drops (the editor is the affordance then) and the Shift+Enter
|
|
308
|
+
* newline hint is appended after cancel, mirroring the question tabs' notes-open shape.
|
|
309
|
+
*/
|
|
310
|
+
export function buildSubmitHintText(state: DialogState): string {
|
|
311
|
+
const parts: string[] = [t("hint.enter", HINT_PART_ENTER), t("hint.navigate", HINT_PART_NAV)];
|
|
312
|
+
if (!state.notesVisible) parts.push(t("review.global_hint", REVIEW_GLOBAL_HINT));
|
|
313
|
+
parts.push(t("hint.cancel", HINT_PART_CANCEL));
|
|
314
|
+
if (state.notesVisible) parts.push(t("hint.newline", HINT_PART_NEW_LINE));
|
|
315
|
+
return parts.join(" · ");
|
|
316
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-btw",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.1",
|
|
4
4
|
"description": "Pi extension that adds a /btw side-question command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"directory": "packages/pi-btw"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@narumitw/pi-tui-kit": "^0.
|
|
50
|
+
"@narumitw/pi-tui-kit": "^0.57.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"@earendil-works/pi-ai": "*",
|