pi-ui-extension 1.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 +43 -0
- package/extensions/ctx/README.md +35 -0
- package/extensions/ctx/activity.ts +226 -0
- package/extensions/ctx/data.test.ts +65 -0
- package/extensions/ctx/data.ts +230 -0
- package/extensions/ctx/panel.test.ts +103 -0
- package/extensions/ctx/panel.ts +185 -0
- package/extensions/ctx/tools.test.ts +64 -0
- package/extensions/fabric-detection.test.ts +15 -0
- package/extensions/fabric-detection.ts +7 -0
- package/extensions/footer/index.ts +62 -0
- package/extensions/index.ts +577 -0
- package/extensions/mcp/integration.test.ts +64 -0
- package/extensions/mcp/resources.test.ts +27 -0
- package/extensions/mcp/resources.ts +31 -0
- package/extensions/resource-dialog.test.ts +45 -0
- package/extensions/session/mutations.test.ts +30 -0
- package/extensions/session/mutations.ts +17 -0
- package/extensions/session/panel.test.ts +238 -0
- package/extensions/session/panel.ts +461 -0
- package/extensions/sidebar/default-panels.test.ts +97 -0
- package/extensions/sidebar/default-panels.ts +155 -0
- package/extensions/sidebar/index.ts +212 -0
- package/extensions/sidebar/layout.test.ts +68 -0
- package/extensions/sidebar/layout.ts +75 -0
- package/extensions/sidebar/panel.ts +49 -0
- package/extensions/sidebar/registry.ts +35 -0
- package/extensions/sidebar/resize.test.ts +71 -0
- package/extensions/sidebar/resize.ts +154 -0
- package/extensions/tool-view/controller.test.ts +57 -0
- package/extensions/tool-view/controller.ts +519 -0
- package/extensions/tool-view/panel.test.ts +73 -0
- package/extensions/tool-view/panel.ts +221 -0
- package/extensions/tool-view/trust.test.ts +41 -0
- package/extensions/tool-view/types.ts +6 -0
- package/extensions/tool-view/usage.test.ts +231 -0
- package/extensions/tool-view/usage.ts +303 -0
- package/extensions/tool-view/wheel.test.ts +32 -0
- package/package.json +53 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import type { ToolExecutionEndEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export type ToolUsageKind = "tool" | "mcp";
|
|
4
|
+
|
|
5
|
+
export type ToolUsageCount = {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly kind: ToolUsageKind;
|
|
8
|
+
/** Calls made while answering the current user question. */
|
|
9
|
+
readonly exchange: number;
|
|
10
|
+
readonly session: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ToolUsageSnapshot = {
|
|
14
|
+
readonly counts: readonly ToolUsageCount[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ToolUsageTracker = {
|
|
18
|
+
startExchange(): void;
|
|
19
|
+
resetSession(
|
|
20
|
+
entries: readonly unknown[],
|
|
21
|
+
mcpServerNames?: Iterable<string>,
|
|
22
|
+
options?: { readonly preserveExchange?: boolean },
|
|
23
|
+
): void;
|
|
24
|
+
record(event: ToolExecutionEndEvent): void;
|
|
25
|
+
getSnapshot(): ToolUsageSnapshot;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type MutableUsageCount = { readonly name: string; readonly kind: ToolUsageKind; count: number };
|
|
29
|
+
type MutableCounts = Map<string, MutableUsageCount>;
|
|
30
|
+
type RecordedExecution = { readonly toolName: string; readonly result: unknown };
|
|
31
|
+
type McpScriptCall = {
|
|
32
|
+
readonly operation?: unknown;
|
|
33
|
+
readonly path?: unknown;
|
|
34
|
+
readonly ok?: unknown;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type ParsedUsage = {
|
|
38
|
+
readonly topLevelName?: string;
|
|
39
|
+
readonly mcpServers: readonly string[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type FabricOperation = {
|
|
43
|
+
readonly ref?: unknown;
|
|
44
|
+
readonly provider?: unknown;
|
|
45
|
+
readonly action?: unknown;
|
|
46
|
+
readonly args?: unknown;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const FABRIC_EXEC_TOOL = "fabric_exec";
|
|
50
|
+
const FABRIC_NESTED_TOOL_CALL_ID_PREFIX = "fabric_";
|
|
51
|
+
const MCP_PROXY_TOOLS = new Set(["mcp", "mcpScript"]);
|
|
52
|
+
const CONTEXT_MODE_TOOL_NAMES = new Set([
|
|
53
|
+
"ctx_execute",
|
|
54
|
+
"ctx_execute_file",
|
|
55
|
+
"ctx_index",
|
|
56
|
+
"ctx_search",
|
|
57
|
+
"ctx_fetch_and_index",
|
|
58
|
+
"ctx_batch_execute",
|
|
59
|
+
"ctx_stats",
|
|
60
|
+
"ctx_doctor",
|
|
61
|
+
"ctx_upgrade",
|
|
62
|
+
"ctx_purge",
|
|
63
|
+
"ctx_insight",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
67
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
68
|
+
? value as Record<string, unknown>
|
|
69
|
+
: undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
73
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function countKey(name: string, kind: ToolUsageKind): string {
|
|
77
|
+
return `${kind}\0${name}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function increment(counts: MutableCounts, name: string, kind: ToolUsageKind, amount = 1): void {
|
|
81
|
+
if (amount <= 0) return;
|
|
82
|
+
const key = countKey(name, kind);
|
|
83
|
+
const current = counts.get(key);
|
|
84
|
+
if (current) current.count += amount;
|
|
85
|
+
else counts.set(key, { name, kind, count: amount });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function contextModeServer(toolName: string): string | undefined {
|
|
89
|
+
return CONTEXT_MODE_TOOL_NAMES.has(toolName) ? "context-mode" : undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sanitizedServerName(serverName: string): string {
|
|
93
|
+
return Array.from(serverName, (character) => /^[A-Za-z0-9_-]$/.test(character)
|
|
94
|
+
? character
|
|
95
|
+
: `_${character.codePointAt(0)!.toString(16)}_`).join("");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolveScriptServer(path: string, knownServers: readonly string[]): string | undefined {
|
|
99
|
+
const matches = knownServers.flatMap((server) => {
|
|
100
|
+
const sanitized = sanitizedServerName(server);
|
|
101
|
+
const shortBase = server.replace(/-?mcp$/i, "");
|
|
102
|
+
const short = sanitizedServerName(shortBase || "mcp");
|
|
103
|
+
return [...new Set([`${sanitized}_`, `${short}_`, `mcp__${sanitized}_`])]
|
|
104
|
+
.filter((prefix) => path.startsWith(prefix))
|
|
105
|
+
.map((prefix) => ({ server, prefix }));
|
|
106
|
+
});
|
|
107
|
+
const longest = matches.reduce((length, match) => Math.max(length, match.prefix.length), 0);
|
|
108
|
+
const candidates = new Set(matches.filter((match) => match.prefix.length === longest).map((match) => match.server));
|
|
109
|
+
return candidates.size === 1 ? [...candidates][0] : undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function scriptServers(result: unknown, knownServers: readonly string[]): string[] {
|
|
113
|
+
const details = asRecord(asRecord(result)?.details);
|
|
114
|
+
if (details?.mode !== "script" || !Array.isArray(details.calls)) return [];
|
|
115
|
+
|
|
116
|
+
const servers: string[] = [];
|
|
117
|
+
for (const call of details.calls as McpScriptCall[]) {
|
|
118
|
+
if (call.operation !== "call") continue;
|
|
119
|
+
const path = nonEmptyString(call.path);
|
|
120
|
+
if (!path) continue;
|
|
121
|
+
const knownServer = resolveScriptServer(path, knownServers);
|
|
122
|
+
if (knownServer) {
|
|
123
|
+
servers.push(knownServer);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// Without an authoritative server list the flattened MCP path cannot be
|
|
127
|
+
// reversed safely (server names and tool names can both contain `_`).
|
|
128
|
+
// Keep the top-level mcpScript count and wait for a later status-driven
|
|
129
|
+
// session rebuild rather than permanently attributing it to a wrong name.
|
|
130
|
+
}
|
|
131
|
+
return servers;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function directMcpServer(result: unknown): string | undefined {
|
|
135
|
+
return nonEmptyString(asRecord(asRecord(result)?.details)?.server);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function proxyMcpServer(toolName: string, result: unknown): string | undefined {
|
|
139
|
+
if (!MCP_PROXY_TOOLS.has(toolName)) return undefined;
|
|
140
|
+
return directMcpServer(result);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseUsage(toolName: string, result: unknown, knownServers: readonly string[]): ParsedUsage {
|
|
144
|
+
const topLevelName = toolName.trim() || "tool";
|
|
145
|
+
const contextServer = contextModeServer(topLevelName);
|
|
146
|
+
if (contextServer) return { topLevelName, mcpServers: [contextServer] };
|
|
147
|
+
|
|
148
|
+
if (topLevelName === "mcpScript") {
|
|
149
|
+
return { topLevelName, mcpServers: scriptServers(result, knownServers) };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const mcpServer = proxyMcpServer(topLevelName, result);
|
|
153
|
+
return { topLevelName, mcpServers: mcpServer ? [mcpServer] : [] };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function fabricOperationUsage(value: unknown, knownServers: readonly string[]): ParsedUsage | undefined {
|
|
157
|
+
const operation = asRecord(value) as FabricOperation | undefined;
|
|
158
|
+
const ref = nonEmptyString(operation?.ref);
|
|
159
|
+
if (!ref) return undefined;
|
|
160
|
+
const provider = nonEmptyString(operation?.provider) ?? ref.split(".", 1)[0];
|
|
161
|
+
const action = nonEmptyString(operation?.action);
|
|
162
|
+
|
|
163
|
+
if (provider === "mcp" || ref.startsWith("mcp.")) {
|
|
164
|
+
const suffix = ref.startsWith("mcp.") ? ref.slice(4) : action ?? "";
|
|
165
|
+
if (suffix.startsWith("$")) {
|
|
166
|
+
const server = suffix === "$call" ? nonEmptyString(asRecord(operation?.args)?.server) : undefined;
|
|
167
|
+
return server
|
|
168
|
+
? { mcpServers: [server] }
|
|
169
|
+
: { topLevelName: ref, mcpServers: [] };
|
|
170
|
+
}
|
|
171
|
+
const separator = suffix.indexOf(".");
|
|
172
|
+
const server = separator > 0 ? suffix.slice(0, separator) : undefined;
|
|
173
|
+
return server ? { mcpServers: [server] } : { topLevelName: ref, mcpServers: [] };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (provider === "pi" || ref.startsWith("pi.")) {
|
|
177
|
+
return parseUsage(action ?? ref.slice(3), undefined, knownServers);
|
|
178
|
+
}
|
|
179
|
+
if (provider === "extensions" || ref.startsWith("extensions.")) {
|
|
180
|
+
return parseUsage(action ?? ref.slice("extensions.".length), undefined, knownServers);
|
|
181
|
+
}
|
|
182
|
+
return parseUsage(ref, undefined, knownServers);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function fabricNestedUsages(result: unknown, knownServers: readonly string[]): ParsedUsage[] {
|
|
186
|
+
const details = asRecord(asRecord(result)?.details);
|
|
187
|
+
const trace = asRecord(details?.trace);
|
|
188
|
+
const operations = Array.isArray(trace?.operations)
|
|
189
|
+
? trace.operations
|
|
190
|
+
: Array.isArray(details?.audits)
|
|
191
|
+
? details.audits
|
|
192
|
+
: [];
|
|
193
|
+
return operations
|
|
194
|
+
.map((operation) => fabricOperationUsage(operation, knownServers))
|
|
195
|
+
.filter((usage): usage is ParsedUsage => usage !== undefined);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function parseUsages(toolName: string, result: unknown, knownServers: readonly string[]): ParsedUsage[] {
|
|
199
|
+
const topLevel = parseUsage(toolName, result, knownServers);
|
|
200
|
+
return toolName === FABRIC_EXEC_TOOL
|
|
201
|
+
? [topLevel, ...fabricNestedUsages(result, knownServers)]
|
|
202
|
+
: [topLevel];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function snapshotCounts(exchangeCounts: MutableCounts, sessionCounts: MutableCounts): ToolUsageSnapshot {
|
|
206
|
+
const kindOrder: Record<ToolUsageKind, number> = { tool: 0, mcp: 1 };
|
|
207
|
+
return Object.freeze({
|
|
208
|
+
counts: Object.freeze(
|
|
209
|
+
[...sessionCounts.values()]
|
|
210
|
+
.map(({ name, kind, count: session }) => Object.freeze({
|
|
211
|
+
name,
|
|
212
|
+
kind,
|
|
213
|
+
exchange: exchangeCounts.get(countKey(name, kind))?.count ?? 0,
|
|
214
|
+
session,
|
|
215
|
+
}))
|
|
216
|
+
.sort((left, right) => kindOrder[left.kind] - kindOrder[right.kind] || right.exchange - left.exchange || right.session - left.session || left.name.localeCompare(right.name)),
|
|
217
|
+
),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function createToolUsageTracker(
|
|
222
|
+
initialToolNames: Iterable<string> = [],
|
|
223
|
+
getMcpServerNames: () => Iterable<string> = () => [],
|
|
224
|
+
): ToolUsageTracker {
|
|
225
|
+
let exchangeCounts: MutableCounts = new Map();
|
|
226
|
+
let sessionCounts: MutableCounts = new Map();
|
|
227
|
+
let exchangeExecutions: RecordedExecution[] = [];
|
|
228
|
+
for (const name of initialToolNames) increment(sessionCounts, name, "tool");
|
|
229
|
+
|
|
230
|
+
const recordParsed = (counts: MutableCounts, usage: ParsedUsage): void => {
|
|
231
|
+
if (usage.topLevelName) increment(counts, usage.topLevelName, "tool");
|
|
232
|
+
for (const server of usage.mcpServers) increment(counts, server, "mcp");
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const recordExecution = (counts: MutableCounts, execution: RecordedExecution, knownServers: readonly string[]): void => {
|
|
236
|
+
for (const usage of parseUsages(execution.toolName, execution.result, knownServers)) {
|
|
237
|
+
recordParsed(counts, usage);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
startExchange(): void {
|
|
243
|
+
exchangeCounts = new Map();
|
|
244
|
+
exchangeExecutions = [];
|
|
245
|
+
},
|
|
246
|
+
resetSession(entries, mcpServerNames = getMcpServerNames(), options = {}): void {
|
|
247
|
+
const knownServers = [...mcpServerNames];
|
|
248
|
+
sessionCounts = new Map();
|
|
249
|
+
for (const count of usageCountsFromBranch(entries, knownServers)) {
|
|
250
|
+
increment(sessionCounts, count.name, count.kind);
|
|
251
|
+
}
|
|
252
|
+
if (options.preserveExchange === true) {
|
|
253
|
+
exchangeCounts = new Map();
|
|
254
|
+
for (const execution of exchangeExecutions) {
|
|
255
|
+
recordExecution(exchangeCounts, execution, knownServers);
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
exchangeCounts = new Map();
|
|
259
|
+
exchangeExecutions = [];
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
record(event): void {
|
|
263
|
+
// Fabric persists every nested operation in the outer result trace. Ignore
|
|
264
|
+
// replayed nested lifecycle events here, then count the trace once when
|
|
265
|
+
// fabric_exec ends so Pi tools, captured extensions and MCP calls cannot
|
|
266
|
+
// disappear or be double-counted depending on provider implementation.
|
|
267
|
+
if (event.toolCallId.startsWith(FABRIC_NESTED_TOOL_CALL_ID_PREFIX)) return;
|
|
268
|
+
const execution = { toolName: event.toolName, result: event.result };
|
|
269
|
+
exchangeExecutions.push(execution);
|
|
270
|
+
const knownServers = [...getMcpServerNames()];
|
|
271
|
+
recordExecution(exchangeCounts, execution, knownServers);
|
|
272
|
+
recordExecution(sessionCounts, execution, knownServers);
|
|
273
|
+
},
|
|
274
|
+
getSnapshot(): ToolUsageSnapshot {
|
|
275
|
+
return snapshotCounts(exchangeCounts, sessionCounts);
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
type UsageName = { readonly name: string; readonly kind: ToolUsageKind };
|
|
281
|
+
|
|
282
|
+
/** Reconstruct completed tool and MCP-server totals from the active branch. */
|
|
283
|
+
function usageCountsFromBranch(entries: readonly unknown[], mcpServerNames: Iterable<string> = []): UsageName[] {
|
|
284
|
+
const counts: UsageName[] = [];
|
|
285
|
+
const knownServers = [...mcpServerNames];
|
|
286
|
+
for (const value of entries) {
|
|
287
|
+
const entry = asRecord(value);
|
|
288
|
+
if (entry?.type !== "message") continue;
|
|
289
|
+
const message = asRecord(entry.message);
|
|
290
|
+
if (message?.role !== "toolResult") continue;
|
|
291
|
+
const toolName = nonEmptyString(message.toolName);
|
|
292
|
+
if (!toolName) continue;
|
|
293
|
+
for (const usage of parseUsages(toolName, { details: message.details }, knownServers)) {
|
|
294
|
+
if (usage.topLevelName) counts.push({ name: usage.topLevelName, kind: "tool" });
|
|
295
|
+
counts.push(...usage.mcpServers.map((name) => ({ name, kind: "mcp" as const })));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return counts;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function toolNamesFromBranch(entries: readonly unknown[], mcpServerNames: Iterable<string> = []): string[] {
|
|
302
|
+
return usageCountsFromBranch(entries, mcpServerNames).map((count) => count.name);
|
|
303
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { renderLayoutFrame, getScrollViewsAt } from "@earendil-works/pi-tui/dist/layout.js";
|
|
4
|
+
import { ToolViewPanel } from "./panel.ts";
|
|
5
|
+
import { createToolViewScrollView } from "../sidebar/default-panels.ts";
|
|
6
|
+
|
|
7
|
+
test("Tool View passes wheel events to its enclosing ScrollView while retaining clicks", async () => {
|
|
8
|
+
let listener: any;
|
|
9
|
+
const selections: string[] = [];
|
|
10
|
+
const tui: any = { addInputListener(fn: any) { listener = fn; return () => {}; }, requestRender() {} };
|
|
11
|
+
const theme: any = { fg: (_: string, text: string) => text };
|
|
12
|
+
const panel = new ToolViewPanel(theme, {
|
|
13
|
+
tui, getDesiredWidth: () => 60, getMode: () => "compact",
|
|
14
|
+
getUsage: () => ({ counts: Array.from({ length: 30 }, (_, i) => ({ name: `tool-${i}`, kind: "tool", exchange: 0, session: 1 })) }),
|
|
15
|
+
onSelect(mode) { selections.push(mode); },
|
|
16
|
+
});
|
|
17
|
+
try {
|
|
18
|
+
const scroll = createToolViewScrollView(panel, theme);
|
|
19
|
+
tui.currentLayout = renderLayoutFrame(scroll, 60, 8, () => {});
|
|
20
|
+
for (const button of [64, 65]) assert.equal(listener(`\x1b[<${button};5;4M`), undefined);
|
|
21
|
+
const targets = getScrollViewsAt(tui.currentLayout, 4, 3);
|
|
22
|
+
assert.equal(targets[0], scroll.scrollView);
|
|
23
|
+
targets[0]!.scrollBy(3);
|
|
24
|
+
assert.equal(scroll.scrollTop, 3);
|
|
25
|
+
targets[0]!.scrollBy(-3);
|
|
26
|
+
tui.currentLayout = renderLayoutFrame(scroll, 60, 8, () => {});
|
|
27
|
+
assert.deepEqual(listener("\x1b[<0;5;2M"), { consume: true });
|
|
28
|
+
assert.deepEqual(listener("\x1b[<0;5;2m"), { consume: true });
|
|
29
|
+
await Promise.resolve();
|
|
30
|
+
assert.deepEqual(selections, ["normal"]);
|
|
31
|
+
} finally { panel.dispose(); }
|
|
32
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-ui-extension",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Pi UI extension with footer, split-view sidebar, and configurable tool transcript rendering",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package"
|
|
8
|
+
],
|
|
9
|
+
"pi": {
|
|
10
|
+
"extensions": [
|
|
11
|
+
"./extensions/index.ts"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"typecheck": "tsc -p tsconfig.check.json",
|
|
16
|
+
"test": "node --import tsx --test",
|
|
17
|
+
"check": "npm run typecheck && npm test"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
21
|
+
"@earendil-works/pi-tui": "*"
|
|
22
|
+
},
|
|
23
|
+
"peerDependenciesMeta": {
|
|
24
|
+
"@earendil-works/pi-coding-agent": {
|
|
25
|
+
"optional": true
|
|
26
|
+
},
|
|
27
|
+
"@earendil-works/pi-tui": {
|
|
28
|
+
"optional": true
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@earendil-works/pi-coding-agent": "0.87.1",
|
|
33
|
+
"@earendil-works/pi-tui": "0.87.1",
|
|
34
|
+
"@types/node": "^24.0.0",
|
|
35
|
+
"tsx": "^4.20.6",
|
|
36
|
+
"typescript": "^7.0.2"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"extensions",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/1993yihuan/pi-ui-extension.git"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/1993yihuan/pi-ui-extension#readme",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/1993yihuan/pi-ui-extension/issues"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|