@luxass/pi-personal 0.0.0 → 0.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.
@@ -0,0 +1,38 @@
1
+ import { type AssistantMessage, isRetryableAssistantError } from "@earendil-works/pi-ai";
2
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+
4
+ const TRANSIENT_STREAM_ERROR_PATTERN =
5
+ /\bclosed network connection\b|\bstream disconnected before completion: stream closed before response\.completed\b|\binvalid SSE data JSON\b/i;
6
+ const NETWORK_ERROR_PREFIX = "network error:";
7
+
8
+ export function normalizeTransientNetworkError(message: AssistantMessage): AssistantMessage {
9
+ if (message.stopReason !== "error" || !message.errorMessage) {
10
+ return message;
11
+ }
12
+ if (
13
+ isRetryableAssistantError(message) ||
14
+ !TRANSIENT_STREAM_ERROR_PATTERN.test(message.errorMessage)
15
+ ) {
16
+ return message;
17
+ }
18
+
19
+ return {
20
+ ...message,
21
+ errorMessage: `${NETWORK_ERROR_PREFIX} ${message.errorMessage}`,
22
+ };
23
+ }
24
+
25
+ export function registerTransientNetworkErrorRetry(pi: ExtensionAPI, providerId: string): void {
26
+ pi.on("message_end", (event) => {
27
+ const message = event.message;
28
+ if (message.role !== "assistant" || message.provider !== providerId) {
29
+ return undefined;
30
+ }
31
+
32
+ const normalized = normalizeTransientNetworkError(message);
33
+ if (normalized === message) {
34
+ return undefined;
35
+ }
36
+ return { message: normalized };
37
+ });
38
+ }
@@ -0,0 +1,292 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir } from "node:fs/promises";
3
+ import { basename, dirname, join, relative, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
7
+ import { keyText, VERSION } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ Key,
10
+ matchesKey,
11
+ truncateToWidth,
12
+ visibleWidth,
13
+ wrapTextWithAnsi,
14
+ } from "@earendil-works/pi-tui";
15
+
16
+ const HEADER_MAX_WIDTH = 88;
17
+ const LOGO_MIN_WIDTH = 64;
18
+ const PI_MASCOT = [
19
+ " o ",
20
+ " | ",
21
+ " .---+---.",
22
+ " | o o |",
23
+ " | \\_/ |",
24
+ " '-------'",
25
+ " / \\ ",
26
+ ];
27
+
28
+ type ResourceSnapshot = {
29
+ context: string[];
30
+ skills: string[];
31
+ extensions: string[];
32
+ };
33
+
34
+ function fit(text: string, width: number): string {
35
+ const clipped = truncateToWidth(text, width, "…");
36
+ return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
37
+ }
38
+
39
+ function resourceName(path: string): string {
40
+ const parts = path.replaceAll("\\", "/").split("/");
41
+ const file = parts.at(-1) ?? path;
42
+ if (file === "index.ts" || file === "index.js") return parts.at(-2) ?? file;
43
+ return file.replace(/\.[cm]?[jt]sx?$/i, "");
44
+ }
45
+
46
+ async function collectResources(
47
+ pi: ExtensionAPI,
48
+ ctx: ExtensionContext,
49
+ ): Promise<ResourceSnapshot> {
50
+ const extensionPaths = new Set<string>();
51
+ try {
52
+ const extensionDir = dirname(fileURLToPath(import.meta.url));
53
+ for (const entry of await readdir(extensionDir, { withFileTypes: true })) {
54
+ if (entry.isFile() && /\.[cm]?[jt]sx?$/i.test(entry.name)) {
55
+ extensionPaths.add(join(extensionDir, entry.name));
56
+ }
57
+ }
58
+ } catch {
59
+ // The extension directory may not be readable.
60
+ }
61
+
62
+ for (const command of pi.getCommands()) {
63
+ if (command.source === "extension") extensionPaths.add(command.sourceInfo.path);
64
+ }
65
+ for (const tool of pi.getAllTools()) {
66
+ if (tool.sourceInfo.source !== "builtin" && tool.sourceInfo.source !== "sdk") {
67
+ extensionPaths.add(tool.sourceInfo.path);
68
+ }
69
+ }
70
+
71
+ const context: string[] = [];
72
+ let directory = resolve(ctx.cwd);
73
+ while (true) {
74
+ for (const filename of ["AGENTS.md", "CLAUDE.md"]) {
75
+ const path = join(directory, filename);
76
+ if (existsSync(path)) context.push(relative(ctx.cwd, path) || filename);
77
+ }
78
+ const parent = dirname(directory);
79
+ if (parent === directory) break;
80
+ directory = parent;
81
+ }
82
+
83
+ return {
84
+ context: context.toSorted(),
85
+ skills: pi
86
+ .getCommands()
87
+ .filter((command) => command.source === "skill")
88
+ .map((command) => command.name.replace(/^skill:/, ""))
89
+ .toSorted(),
90
+ extensions: [...extensionPaths].map(resourceName).filter(Boolean).toSorted(),
91
+ };
92
+ }
93
+
94
+ interface HeaderOptions {
95
+ theme: Theme;
96
+ width: number;
97
+ project: string;
98
+ model: Pick<NonNullable<ExtensionContext["model"]>, "provider" | "id" | "reasoning"> | undefined;
99
+ thinking: ReturnType<ExtensionAPI["getThinkingLevel"]>;
100
+ thinkingKey: string;
101
+ }
102
+
103
+ export function renderHeader({
104
+ theme,
105
+ width,
106
+ project,
107
+ model,
108
+ thinking,
109
+ thinkingKey,
110
+ }: HeaderOptions): string[] {
111
+ if (width < 1) return [""];
112
+
113
+ const accent = (text: string) => theme.fg("accent", text);
114
+ const muted = (text: string) => theme.fg("muted", text);
115
+ const dim = (text: string) => theme.fg("dim", text);
116
+ const expanded = width >= LOGO_MIN_WIDTH;
117
+ const border = (text: string) => theme.fg(expanded ? "borderMuted" : "borderAccent", text);
118
+ const version = dim(`v${VERSION}`);
119
+ const title = `${accent("π")} ${theme.bold("Pi")} ${version}`;
120
+ const indent = width >= 44 ? " " : "";
121
+ const panelWidth = Math.min(expanded ? HEADER_MAX_WIDTH : 72, width - indent.length);
122
+ const boxed = width >= 24;
123
+ const contentWidth = boxed ? panelWidth - 4 : width;
124
+ const separator = dim(" · ");
125
+ const workspace = theme.bold(theme.fg("text", project));
126
+ const modelLabel = model
127
+ ? `${width >= 44 ? dim(`${model.provider}/`) : ""}${theme.fg("text", model.id)}`
128
+ : muted("model pending");
129
+ const thinkingLabel = model?.reasoning ? `${muted("thinking ")}${accent(thinking)}` : "";
130
+ const metadata = [modelLabel, thinkingLabel].filter(Boolean).join(separator);
131
+ const line = (text = "") => `${indent}${border("│")} ${fit(text, contentWidth)} ${border("│")}`;
132
+
133
+ const hints = [
134
+ `${accent("/")} ${muted("commands")}`,
135
+ `${accent("!")} ${muted("shell")}`,
136
+ ...(model?.reasoning && thinkingKey
137
+ ? [`${accent(thinkingKey === "shift+tab" ? "⇧Tab" : thinkingKey)} ${muted("thinking")}`]
138
+ : []),
139
+ accent("/resources"),
140
+ ];
141
+ // Drop complete hints, not half a shortcut. Keep /resources available longest.
142
+ const hintWidth = expanded ? contentWidth - 4 : contentWidth;
143
+ while (hints.length > 1 && visibleWidth(hints.join(" ")) > hintWidth) {
144
+ hints.splice(hints.length - 2, 1);
145
+ }
146
+
147
+ if (expanded) {
148
+ const logoWidth = Math.max(...PI_MASCOT.map(visibleWidth));
149
+ const detailsWidth = contentWidth - logoWidth - 5;
150
+ const name = theme.bold(theme.fg("text", "Pi"));
151
+ const details = [
152
+ `${name}${" ".repeat(Math.max(1, detailsWidth - visibleWidth(name) - visibleWidth(version)))}${version}`,
153
+ "",
154
+ workspace,
155
+ modelLabel,
156
+ thinkingLabel,
157
+ "",
158
+ ];
159
+
160
+ return [
161
+ "",
162
+ `${indent}${border(`╭${"─".repeat(panelWidth - 2)}╮`)}`,
163
+ line(),
164
+ ...PI_MASCOT.map((logo, index) =>
165
+ line(
166
+ ` ${fit(theme.bold(accent(logo)), logoWidth)} ${fit(details[index] ?? "", detailsWidth)}`,
167
+ ),
168
+ ),
169
+ line(),
170
+ line(` ${hints.join(" ")}`),
171
+ line(),
172
+ `${indent}${border(`╰${"─".repeat(panelWidth - 2)}╯`)}`,
173
+ "",
174
+ ];
175
+ }
176
+
177
+ const session = `${workspace}${separator}${metadata}`;
178
+ const lines: string[] = [];
179
+ if (visibleWidth(session) <= contentWidth) {
180
+ lines.push(session);
181
+ } else {
182
+ lines.push(workspace);
183
+ if (visibleWidth(metadata) <= contentWidth) lines.push(metadata);
184
+ else lines.push(modelLabel, ...(thinkingLabel ? [thinkingLabel] : []));
185
+ }
186
+ if (contentWidth >= visibleWidth("/resources")) {
187
+ if (boxed) lines.push("");
188
+ lines.push(hints.join(" "));
189
+ }
190
+
191
+ if (!boxed) return ["", ...[title, ...lines].map((line) => fit(line, width)), ""];
192
+
193
+ const label = truncateToWidth(title, panelWidth - 5, "…");
194
+ return [
195
+ "",
196
+ `${indent}${border("╭─ ")}${label}${border(` ${"─".repeat(panelWidth - 5 - visibleWidth(label))}╮`)}`,
197
+ ...lines.map(line),
198
+ `${indent}${border(`╰${"─".repeat(panelWidth - 2)}╯`)}`,
199
+ "",
200
+ ];
201
+ }
202
+
203
+ function renderResources(snapshot: ResourceSnapshot, theme: Theme, width: number): string[] {
204
+ if (width < 6) return [fit("Resources", width)];
205
+
206
+ const panelWidth = Math.min(86, width);
207
+ const contentWidth = panelWidth - 2;
208
+ const border = (text: string) => theme.fg("borderAccent", text);
209
+ const heading = (text: string) => theme.fg("mdHeading", theme.bold(text));
210
+ const muted = (text: string) => theme.fg("muted", text);
211
+ const dim = (text: string) => theme.fg("dim", text);
212
+ const accent = (text: string) => theme.fg("accent", text);
213
+ const line = (text = "") => `${border("│")}${fit(text, contentWidth)}${border("│")}`;
214
+ const lines = [
215
+ border(`╭${"─".repeat(panelWidth - 2)}╮`),
216
+ line(`${accent("π")} ${heading("Resources")}`),
217
+ line(dim("Detected context, skill commands, and extensions")),
218
+ line(),
219
+ ];
220
+
221
+ const sections: Array<[string, string[], string]> = [
222
+ ["Context", snapshot.context, "✓"],
223
+ ["Skills", snapshot.skills, "•"],
224
+ ["Extensions", snapshot.extensions, "•"],
225
+ ];
226
+ for (const [label, items, marker] of sections) {
227
+ lines.push(line(heading(`${label} · ${items.length}`)));
228
+ const entries = wrapTextWithAnsi(
229
+ items.length > 0 ? items.join(", ") : "none detected",
230
+ Math.max(1, contentWidth - 4),
231
+ );
232
+ for (const [index, entry] of entries.entries()) {
233
+ const prefix = index === 0 ? ` ${marker} ` : " ";
234
+ lines.push(line(`${index === 0 ? accent(prefix) : dim(prefix)}${muted(entry)}`));
235
+ }
236
+ lines.push(line());
237
+ }
238
+
239
+ lines.push(line(dim("Press Esc, Enter, or q to close")));
240
+ lines.push(border(`╰${"─".repeat(panelWidth - 2)}╯`));
241
+ return lines;
242
+ }
243
+
244
+ export default function (pi: ExtensionAPI) {
245
+ pi.on("session_start", (_event, ctx) => {
246
+ if (ctx.mode !== "tui") return;
247
+
248
+ ctx.ui.setHeader((_tui, theme) => ({
249
+ render: (width: number) =>
250
+ renderHeader({
251
+ theme,
252
+ width,
253
+ project: basename(ctx.cwd) || "workspace",
254
+ model: ctx.model,
255
+ thinking: pi.getThinkingLevel(),
256
+ thinkingKey: keyText("app.thinking.cycle"),
257
+ }),
258
+ invalidate() {},
259
+ }));
260
+ });
261
+
262
+ pi.registerCommand("resources", {
263
+ description: "Show detected context, skill commands, and extensions",
264
+ handler: async (_args, ctx) => {
265
+ if (ctx.mode !== "tui") return;
266
+ const resources = await collectResources(pi, ctx);
267
+
268
+ await ctx.ui.custom<void>(
269
+ (tui, theme, _keybindings, done) => ({
270
+ render: (width: number) => renderResources(resources, theme, width),
271
+ handleInput: (data: string) => {
272
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.enter) || data === "q") {
273
+ done();
274
+ }
275
+ tui.requestRender();
276
+ },
277
+ invalidate() {},
278
+ }),
279
+ {
280
+ overlay: true,
281
+ overlayOptions: {
282
+ anchor: "center",
283
+ width: "70%",
284
+ minWidth: 46,
285
+ maxHeight: "80%",
286
+ margin: 2,
287
+ },
288
+ },
289
+ );
290
+ },
291
+ });
292
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Git Interceptor
3
+ *
4
+ * Two guards for agent-driven git commands:
5
+ *
6
+ * 1. Editor hang prevention — Sets GIT_EDITOR, GIT_SEQUENCE_EDITOR to `true`
7
+ * (no-op) and GIT_MERGE_AUTOEDIT to `no` so git never spawns an interactive
8
+ * editor (nvim, vim, etc.) that would hang the bash process.
9
+ *
10
+ * 2. Hook bypass prevention — Blocks any command containing `--no-verify` so
11
+ * the agent cannot circumvent git hooks (pre-commit, commit-msg, etc.).
12
+ * The agent should fix hook failures or ask the human for help instead.
13
+ */
14
+
15
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
+ import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
17
+
18
+ const GIT_ENV_PREFIX = "export GIT_EDITOR=true GIT_SEQUENCE_EDITOR=true GIT_MERGE_AUTOEDIT=no\n";
19
+
20
+ const NO_VERIFY_RE = /--no-verify\b/;
21
+
22
+ const BLOCK_REASON =
23
+ "BLOCKED: --no-verify is not allowed. Git hooks exist for a reason. " +
24
+ "Do not attempt to bypass them. Instead: fix the underlying issue that " +
25
+ "is causing the hook to fail, or ask the user for help.";
26
+
27
+ export default function (pi: ExtensionAPI) {
28
+ pi.on("tool_call", (event) => {
29
+ if (!isToolCallEventType("bash", event)) return undefined;
30
+ if (!event.input.command.includes("git")) return undefined;
31
+
32
+ if (NO_VERIFY_RE.test(event.input.command)) {
33
+ return { block: true, reason: BLOCK_REASON };
34
+ }
35
+
36
+ event.input.command = GIT_ENV_PREFIX + event.input.command;
37
+ return undefined;
38
+ });
39
+ }
@@ -0,0 +1,24 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
3
+
4
+ // Catch common dependency-install commands, including those after `cd ... &&`.
5
+ // Keep the command intact: rewriting shell pipelines can change their meaning.
6
+ const INSTALL =
7
+ /\b(?:pnpm\s+(?:install|add|i|dlx)|npm\s+(?:install|i|ci|exec)|bun\s+(?:install|add|i|x)|yarn\s+(?:install|add|dlx)|npx\b)(?=\s|$)/g;
8
+
9
+ export default function (pi: ExtensionAPI) {
10
+ pi.on("tool_call", (event) => {
11
+ if (!isToolCallEventType("bash", event)) return undefined;
12
+
13
+ const command = event.input.command;
14
+ for (const match of command.matchAll(INSTALL)) {
15
+ if (/\bsfw\s+$/.test(command.slice(0, match.index))) continue;
16
+ return {
17
+ block: true,
18
+ reason:
19
+ "Package installs must run through Socket Firewall. Retry with sfw before the package manager (for example, sfw pnpm install).",
20
+ };
21
+ }
22
+ return undefined;
23
+ });
24
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Worker Configuration Guard
3
+ *
4
+ * Blocks high-confidence attempts to manually create or modify
5
+ * worker-configuration.d.ts. That file is generated by Cloudflare Wrangler and
6
+ * should be regenerated with `wrangler types` instead of hand-edited.
7
+ *
8
+ * Bash detection intentionally favors avoiding false positives over recognizing
9
+ * arbitrary mutations hidden inside scripts or shell evaluation.
10
+ */
11
+
12
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
+ import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
14
+
15
+ const PROTECTED_FILE = "worker-configuration.d.ts";
16
+
17
+ const BLOCK_REASON =
18
+ `BLOCKED: ${PROTECTED_FILE} is generated and must not be manually ` +
19
+ "created, edited, deleted, or overwritten by an agent. Use the `read` tool " +
20
+ "to inspect it, or run `wrangler types` from the worker project to " +
21
+ "regenerate it instead.";
22
+
23
+ const SHELL_COMMAND_SEPARATOR_RE = /(?:&&|\|\||[;|\n])/;
24
+ const SHELL_OUTPUT_REDIRECTION_RE = /^\d*>>?$/;
25
+ const SHELL_DIRECT_MUTATION_COMMANDS = new Set(["rm", "tee", "touch", "truncate"]);
26
+ const SHELL_COPY_COMMANDS = new Set(["cp", "install", "mv", "rsync"]);
27
+
28
+ /** Definite shell mutation categories recognized by the worker configuration guard. */
29
+ export type WorkerConfigurationMutation =
30
+ | "copy-destination"
31
+ | "direct-file-command"
32
+ | "in-place-edit"
33
+ | "output-redirection";
34
+
35
+ function normalizeToolPath(path: string): string {
36
+ return path.replace(/^@/, "").replaceAll("\\", "/");
37
+ }
38
+
39
+ function isProtectedPath(path: unknown): boolean {
40
+ if (typeof path !== "string") return false;
41
+ const normalized = normalizeToolPath(path);
42
+ return normalized.split("/").at(-1) === PROTECTED_FILE;
43
+ }
44
+
45
+ function tokenizeShellCommand(command: string): string[] {
46
+ const tokens: string[] = [];
47
+ let token = "";
48
+ let quote: "'" | '"' | undefined;
49
+
50
+ const pushToken = () => {
51
+ if (token.length === 0) return;
52
+ tokens.push(token);
53
+ token = "";
54
+ };
55
+
56
+ for (let index = 0; index < command.length; index += 1) {
57
+ const character = command[index];
58
+ if (character === undefined) continue;
59
+
60
+ if (quote !== undefined) {
61
+ if (character === quote) quote = undefined;
62
+ else token += character;
63
+ continue;
64
+ }
65
+
66
+ if (character === "'" || character === '"') {
67
+ quote = character;
68
+ continue;
69
+ }
70
+
71
+ if (character === "\\" && command[index + 1] !== undefined) {
72
+ token += command[index + 1];
73
+ index += 1;
74
+ continue;
75
+ }
76
+
77
+ if (/\s/.test(character)) {
78
+ pushToken();
79
+ if (character === "\n") tokens.push(character);
80
+ continue;
81
+ }
82
+
83
+ if (/[;&|<>]/.test(character)) {
84
+ pushToken();
85
+ const pair = character + (command[index + 1] ?? "");
86
+ if (["&&", "||", ">>", "<<"].includes(pair)) {
87
+ tokens.push(pair);
88
+ index += 1;
89
+ } else {
90
+ tokens.push(character);
91
+ }
92
+ continue;
93
+ }
94
+
95
+ token += character;
96
+ }
97
+
98
+ pushToken();
99
+ return tokens;
100
+ }
101
+
102
+ function splitShellCommandSegments(tokens: readonly string[]): string[][] {
103
+ const segments: string[][] = [[]];
104
+ for (const token of tokens) {
105
+ if (SHELL_COMMAND_SEPARATOR_RE.test(token)) segments.push([]);
106
+ else segments.at(-1)?.push(token);
107
+ }
108
+ return segments;
109
+ }
110
+
111
+ function shellCommandName(segment: readonly string[]): string | undefined {
112
+ return segment.find((token) => !token.includes("=") && !token.startsWith("-"));
113
+ }
114
+
115
+ /** Finds a definite shell mutation targeting Wrangler's generated worker configuration file. */
116
+ export function findWorkerConfigurationMutation(
117
+ command: string,
118
+ ): WorkerConfigurationMutation | undefined {
119
+ const segments = splitShellCommandSegments(tokenizeShellCommand(command));
120
+
121
+ for (const segment of segments) {
122
+ for (let index = 0; index < segment.length - 1; index += 1) {
123
+ if (
124
+ SHELL_OUTPUT_REDIRECTION_RE.test(segment[index] ?? "") &&
125
+ isProtectedPath(segment[index + 1])
126
+ ) {
127
+ return "output-redirection";
128
+ }
129
+ }
130
+
131
+ const commandName = shellCommandName(segment);
132
+ if (commandName === undefined) continue;
133
+ const operands = segment.slice(segment.indexOf(commandName) + 1);
134
+
135
+ if (SHELL_DIRECT_MUTATION_COMMANDS.has(commandName) && operands.some(isProtectedPath)) {
136
+ return "direct-file-command";
137
+ }
138
+
139
+ if (
140
+ (commandName === "sed" || commandName === "perl") &&
141
+ operands.some(
142
+ (operand) =>
143
+ operand === "-i" || operand.startsWith("-i") || operand.startsWith("--in-place"),
144
+ ) &&
145
+ operands.some(isProtectedPath)
146
+ ) {
147
+ return "in-place-edit";
148
+ }
149
+
150
+ if (SHELL_COPY_COMMANDS.has(commandName)) {
151
+ const paths = operands.filter((operand) => !operand.startsWith("-"));
152
+ if (isProtectedPath(paths.at(-1))) return "copy-destination";
153
+ }
154
+
155
+ if (
156
+ commandName === "dd" &&
157
+ operands.some((operand) => operand.startsWith("of=") && isProtectedPath(operand.slice(3)))
158
+ ) {
159
+ return "direct-file-command";
160
+ }
161
+ }
162
+
163
+ return undefined;
164
+ }
165
+
166
+ export default function (pi: ExtensionAPI) {
167
+ pi.on("tool_call", (event, ctx) => {
168
+ if (isToolCallEventType("write", event) || isToolCallEventType("edit", event)) {
169
+ if (isProtectedPath(event.input.path)) {
170
+ if (ctx.hasUI) {
171
+ ctx.ui.notify(
172
+ `Blocked manual change to ${PROTECTED_FILE}; run wrangler types.`,
173
+ "warning",
174
+ );
175
+ }
176
+ return { block: true, reason: BLOCK_REASON };
177
+ }
178
+
179
+ return undefined;
180
+ }
181
+
182
+ if (!isToolCallEventType("bash", event)) return undefined;
183
+ if (findWorkerConfigurationMutation(event.input.command) === undefined) return undefined;
184
+
185
+ if (ctx.hasUI) {
186
+ ctx.ui.notify(`Blocked manual change to ${PROTECTED_FILE}; run wrangler types.`, "warning");
187
+ }
188
+
189
+ return { block: true, reason: BLOCK_REASON };
190
+ });
191
+ }
package/src/index.ts DELETED
@@ -1,7 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
-
3
- export default function personal(pi: ExtensionAPI) {
4
- pi.on("session_start", (_, ctx) => {
5
- ctx.ui.notify("Personal extension activated.");
6
- });
7
- }