mitsupi 1.4.0 → 1.6.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/AGENTS.md +1 -1
- package/CHANGELOG.md +24 -0
- package/README.md +28 -18
- package/analyze-edits.py +232 -0
- package/distributions/README.md +8 -0
- package/distributions/mitsupi-common/README.md +11 -0
- package/distributions/mitsupi-common/package.json +95 -0
- package/distributions/mitsupi-loaded/README.md +13 -0
- package/distributions/mitsupi-loaded/package.json +38 -0
- package/{pi-extensions → extensions}/answer.ts +11 -11
- package/extensions/btw.ts +963 -0
- package/{pi-extensions → extensions}/context.ts +2 -2
- package/{pi-extensions → extensions}/control.ts +20 -13
- package/{pi-extensions → extensions}/files.ts +6 -8
- package/{pi-extensions → extensions}/go-to-bed.ts +2 -2
- package/{pi-extensions → extensions}/loop.ts +18 -11
- package/extensions/multi-edit.ts +871 -0
- package/{pi-extensions → extensions}/review.ts +254 -82
- package/{pi-extensions → extensions}/session-breakdown.ts +551 -74
- package/extensions/split-fork.ts +130 -0
- package/{pi-extensions → extensions}/todos.ts +41 -34
- package/extensions/uv.ts +123 -0
- package/intercepted-commands/poetry +8 -1
- package/intercepted-commands/python +42 -2
- package/intercepted-commands/python3 +42 -2
- package/package.json +6 -3
- package/skills/google-workspace/SKILL.md +53 -8
- package/skills/google-workspace/scripts/auth.js +106 -28
- package/skills/google-workspace/scripts/common.js +201 -32
- package/skills/google-workspace/scripts/workspace.js +64 -9
- package/skills/native-web-search/SKILL.md +2 -0
- package/skills/native-web-search/search.mjs +102 -15
- package/skills/pi-share/SKILL.md +5 -2
- package/skills/pi-share/fetch-session.mjs +46 -15
- package/skills/summarize/SKILL.md +4 -3
- package/skills/summarize/to-markdown.mjs +3 -1
- package/skills/web-browser/SKILL.md +6 -0
- package/skills/web-browser/scripts/start.js +75 -27
- package/pi-extensions/uv.ts +0 -33
- /package/{pi-extensions → extensions}/notify.ts +0 -0
- /package/{pi-extensions → extensions}/prompt-editor.ts +0 -0
- /package/{pi-extensions → extensions}/whimsical.ts +0 -0
- /package/{pi-themes → themes}/nightowl.json +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { existsSync, promises as fs } from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
const GHOSTTY_SPLIT_SCRIPT = `on run argv
|
|
7
|
+
set targetCwd to item 1 of argv
|
|
8
|
+
set startupInput to item 2 of argv
|
|
9
|
+
tell application "Ghostty"
|
|
10
|
+
set cfg to new surface configuration
|
|
11
|
+
set initial working directory of cfg to targetCwd
|
|
12
|
+
set initial input of cfg to startupInput
|
|
13
|
+
if (count of windows) > 0 then
|
|
14
|
+
try
|
|
15
|
+
set frontWindow to front window
|
|
16
|
+
set targetTerminal to focused terminal of selected tab of frontWindow
|
|
17
|
+
split targetTerminal direction right with configuration cfg
|
|
18
|
+
on error
|
|
19
|
+
new window with configuration cfg
|
|
20
|
+
end try
|
|
21
|
+
else
|
|
22
|
+
new window with configuration cfg
|
|
23
|
+
end if
|
|
24
|
+
activate
|
|
25
|
+
end tell
|
|
26
|
+
end run`;
|
|
27
|
+
|
|
28
|
+
function shellQuote(value: string): string {
|
|
29
|
+
if (value.length === 0) return "''";
|
|
30
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getPiInvocationParts(): string[] {
|
|
34
|
+
const currentScript = process.argv[1];
|
|
35
|
+
if (currentScript && existsSync(currentScript)) {
|
|
36
|
+
return [process.execPath, currentScript];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const execName = path.basename(process.execPath).toLowerCase();
|
|
40
|
+
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
41
|
+
if (!isGenericRuntime) {
|
|
42
|
+
return [process.execPath];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return ["pi"];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildPiStartupInput(sessionFile: string | undefined, prompt: string): string {
|
|
49
|
+
const commandParts = [...getPiInvocationParts()];
|
|
50
|
+
|
|
51
|
+
if (sessionFile) {
|
|
52
|
+
commandParts.push("--session", sessionFile);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (prompt.length > 0) {
|
|
56
|
+
commandParts.push("--", prompt);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return `${commandParts.map(shellQuote).join(" ")}\n`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function createForkedSession(ctx: ExtensionCommandContext): Promise<string | undefined> {
|
|
63
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
64
|
+
if (!sessionFile) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const sessionDir = path.dirname(sessionFile);
|
|
69
|
+
const branchEntries = ctx.sessionManager.getBranch();
|
|
70
|
+
const currentHeader = ctx.sessionManager.getHeader();
|
|
71
|
+
|
|
72
|
+
const timestamp = new Date().toISOString();
|
|
73
|
+
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
74
|
+
const newSessionId = randomUUID();
|
|
75
|
+
const newSessionFile = path.join(sessionDir, `${fileTimestamp}_${newSessionId}.jsonl`);
|
|
76
|
+
|
|
77
|
+
const newHeader = {
|
|
78
|
+
type: "session",
|
|
79
|
+
version: currentHeader?.version ?? 3,
|
|
80
|
+
id: newSessionId,
|
|
81
|
+
timestamp,
|
|
82
|
+
cwd: currentHeader?.cwd ?? ctx.cwd,
|
|
83
|
+
parentSession: sessionFile,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const lines = [JSON.stringify(newHeader), ...branchEntries.map((entry) => JSON.stringify(entry))].join("\n") + "\n";
|
|
87
|
+
|
|
88
|
+
await fs.mkdir(sessionDir, { recursive: true });
|
|
89
|
+
await fs.writeFile(newSessionFile, lines, "utf8");
|
|
90
|
+
|
|
91
|
+
return newSessionFile;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default function (pi: ExtensionAPI): void {
|
|
95
|
+
pi.registerCommand("split-fork", {
|
|
96
|
+
description: "Fork this session into a new pi process in a right-hand Ghostty split. Usage: /split-fork [optional prompt]",
|
|
97
|
+
handler: async (args, ctx) => {
|
|
98
|
+
if (process.platform !== "darwin") {
|
|
99
|
+
ctx.ui.notify("/split-fork currently requires macOS (Ghostty AppleScript).", "warning");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const wasBusy = !ctx.isIdle();
|
|
104
|
+
const prompt = args.trim();
|
|
105
|
+
const forkedSessionFile = await createForkedSession(ctx);
|
|
106
|
+
const startupInput = buildPiStartupInput(forkedSessionFile, prompt);
|
|
107
|
+
|
|
108
|
+
const result = await pi.exec("osascript", ["-e", GHOSTTY_SPLIT_SCRIPT, "--", ctx.cwd, startupInput]);
|
|
109
|
+
if (result.code !== 0) {
|
|
110
|
+
const reason = result.stderr?.trim() || result.stdout?.trim() || "unknown osascript error";
|
|
111
|
+
ctx.ui.notify(`Failed to launch Ghostty split: ${reason}`, "error");
|
|
112
|
+
if (forkedSessionFile) {
|
|
113
|
+
ctx.ui.notify(`Forked session was created: ${forkedSessionFile}`, "info");
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (forkedSessionFile) {
|
|
119
|
+
const fileName = path.basename(forkedSessionFile);
|
|
120
|
+
const suffix = prompt ? " and sent prompt" : "";
|
|
121
|
+
ctx.ui.notify(`Forked to ${fileName} in a new Ghostty split${suffix}.`, "info");
|
|
122
|
+
if (wasBusy) {
|
|
123
|
+
ctx.ui.notify("Forked from current committed state (in-flight turn continues in original session).", "info");
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
ctx.ui.notify("Opened a new Ghostty split (no persisted session to fork).", "warning");
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -48,7 +48,6 @@ import {
|
|
|
48
48
|
Text,
|
|
49
49
|
TUI,
|
|
50
50
|
fuzzyMatch,
|
|
51
|
-
getEditorKeybindings,
|
|
52
51
|
matchesKey,
|
|
53
52
|
truncateToWidth,
|
|
54
53
|
visibleWidth,
|
|
@@ -90,6 +89,10 @@ interface TodoSettings {
|
|
|
90
89
|
gcDays: number;
|
|
91
90
|
}
|
|
92
91
|
|
|
92
|
+
type KeybindingMatcher = {
|
|
93
|
+
matches: (keyData: string, keybindingId: string) => boolean;
|
|
94
|
+
};
|
|
95
|
+
|
|
93
96
|
const TodoParams = Type.Object({
|
|
94
97
|
action: StringEnum([
|
|
95
98
|
"list",
|
|
@@ -253,6 +256,7 @@ class TodoSelectorComponent extends Container implements Focusable {
|
|
|
253
256
|
private onCancelCallback: () => void;
|
|
254
257
|
private tui: TUI;
|
|
255
258
|
private theme: Theme;
|
|
259
|
+
private keybindings: KeybindingMatcher;
|
|
256
260
|
private headerText: Text;
|
|
257
261
|
private hintText: Text;
|
|
258
262
|
private currentSessionId?: string;
|
|
@@ -269,6 +273,7 @@ class TodoSelectorComponent extends Container implements Focusable {
|
|
|
269
273
|
constructor(
|
|
270
274
|
tui: TUI,
|
|
271
275
|
theme: Theme,
|
|
276
|
+
keybindings: KeybindingMatcher,
|
|
272
277
|
todos: TodoFrontMatter[],
|
|
273
278
|
onSelect: (todo: TodoFrontMatter) => void,
|
|
274
279
|
onCancel: () => void,
|
|
@@ -279,6 +284,7 @@ class TodoSelectorComponent extends Container implements Focusable {
|
|
|
279
284
|
super();
|
|
280
285
|
this.tui = tui;
|
|
281
286
|
this.theme = theme;
|
|
287
|
+
this.keybindings = keybindings;
|
|
282
288
|
this.currentSessionId = currentSessionId;
|
|
283
289
|
this.allTodos = todos;
|
|
284
290
|
this.filteredTodos = todos;
|
|
@@ -397,25 +403,25 @@ class TodoSelectorComponent extends Container implements Focusable {
|
|
|
397
403
|
}
|
|
398
404
|
|
|
399
405
|
handleInput(keyData: string): void {
|
|
400
|
-
const kb =
|
|
401
|
-
if (kb.matches(keyData, "
|
|
406
|
+
const kb = this.keybindings;
|
|
407
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
402
408
|
if (this.filteredTodos.length === 0) return;
|
|
403
409
|
this.selectedIndex = this.selectedIndex === 0 ? this.filteredTodos.length - 1 : this.selectedIndex - 1;
|
|
404
410
|
this.updateList();
|
|
405
411
|
return;
|
|
406
412
|
}
|
|
407
|
-
if (kb.matches(keyData, "
|
|
413
|
+
if (kb.matches(keyData, "tui.select.down")) {
|
|
408
414
|
if (this.filteredTodos.length === 0) return;
|
|
409
415
|
this.selectedIndex = this.selectedIndex === this.filteredTodos.length - 1 ? 0 : this.selectedIndex + 1;
|
|
410
416
|
this.updateList();
|
|
411
417
|
return;
|
|
412
418
|
}
|
|
413
|
-
if (kb.matches(keyData, "
|
|
419
|
+
if (kb.matches(keyData, "tui.select.confirm")) {
|
|
414
420
|
const selected = this.filteredTodos[this.selectedIndex];
|
|
415
421
|
if (selected) this.onSelectCallback(selected);
|
|
416
422
|
return;
|
|
417
423
|
}
|
|
418
|
-
if (kb.matches(keyData, "
|
|
424
|
+
if (kb.matches(keyData, "tui.select.cancel")) {
|
|
419
425
|
this.onCancelCallback();
|
|
420
426
|
return;
|
|
421
427
|
}
|
|
@@ -558,10 +564,18 @@ class TodoDetailOverlayComponent {
|
|
|
558
564
|
private viewHeight = 0;
|
|
559
565
|
private totalLines = 0;
|
|
560
566
|
private onAction: (action: TodoOverlayAction) => void;
|
|
567
|
+
private keybindings: KeybindingMatcher;
|
|
561
568
|
|
|
562
|
-
constructor(
|
|
569
|
+
constructor(
|
|
570
|
+
tui: TUI,
|
|
571
|
+
theme: Theme,
|
|
572
|
+
keybindings: KeybindingMatcher,
|
|
573
|
+
todo: TodoRecord,
|
|
574
|
+
onAction: (action: TodoOverlayAction) => void,
|
|
575
|
+
) {
|
|
563
576
|
this.tui = tui;
|
|
564
577
|
this.theme = theme;
|
|
578
|
+
this.keybindings = keybindings;
|
|
565
579
|
this.todo = todo;
|
|
566
580
|
this.onAction = onAction;
|
|
567
581
|
this.markdown = new Markdown(this.getMarkdownText(), 1, 0, getMarkdownTheme());
|
|
@@ -573,28 +587,28 @@ class TodoDetailOverlayComponent {
|
|
|
573
587
|
}
|
|
574
588
|
|
|
575
589
|
handleInput(keyData: string): void {
|
|
576
|
-
const kb =
|
|
577
|
-
if (kb.matches(keyData, "
|
|
590
|
+
const kb = this.keybindings;
|
|
591
|
+
if (kb.matches(keyData, "tui.select.cancel")) {
|
|
578
592
|
this.onAction("back");
|
|
579
593
|
return;
|
|
580
594
|
}
|
|
581
|
-
if (kb.matches(keyData, "
|
|
595
|
+
if (kb.matches(keyData, "tui.select.confirm")) {
|
|
582
596
|
this.onAction("work");
|
|
583
597
|
return;
|
|
584
598
|
}
|
|
585
|
-
if (kb.matches(keyData, "
|
|
599
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
586
600
|
this.scrollBy(-1);
|
|
587
601
|
return;
|
|
588
602
|
}
|
|
589
|
-
if (kb.matches(keyData, "
|
|
603
|
+
if (kb.matches(keyData, "tui.select.down")) {
|
|
590
604
|
this.scrollBy(1);
|
|
591
605
|
return;
|
|
592
606
|
}
|
|
593
|
-
if (kb.matches(keyData, "
|
|
607
|
+
if (kb.matches(keyData, "tui.select.pageUp") || matchesKey(keyData, Key.left)) {
|
|
594
608
|
this.scrollBy(-this.viewHeight || -1);
|
|
595
609
|
return;
|
|
596
610
|
}
|
|
597
|
-
if (kb.matches(keyData, "
|
|
611
|
+
if (kb.matches(keyData, "tui.select.pageDown") || matchesKey(keyData, Key.right)) {
|
|
598
612
|
this.scrollBy(this.viewHeight || 1);
|
|
599
613
|
return;
|
|
600
614
|
}
|
|
@@ -685,7 +699,8 @@ class TodoDetailOverlayComponent {
|
|
|
685
699
|
private buildActionLine(width: number): string {
|
|
686
700
|
const work = this.theme.fg("accent", "enter") + this.theme.fg("muted", " work on todo");
|
|
687
701
|
const back = this.theme.fg("dim", "esc back");
|
|
688
|
-
const
|
|
702
|
+
const nav = this.theme.fg("dim", "↑/↓: move. ←/→: page.");
|
|
703
|
+
const pieces = [work, back, nav];
|
|
689
704
|
|
|
690
705
|
let line = pieces.join(this.theme.fg("muted", " • "));
|
|
691
706
|
if (this.totalLines > this.viewHeight) {
|
|
@@ -1256,7 +1271,7 @@ function renderTodoDetail(theme: Theme, todo: TodoRecord, expanded: boolean): st
|
|
|
1256
1271
|
}
|
|
1257
1272
|
|
|
1258
1273
|
function appendExpandHint(theme: Theme, text: string): string {
|
|
1259
|
-
return `${text}\n${theme.fg("dim", `(${keyHint("
|
|
1274
|
+
return `${text}\n${theme.fg("dim", `(${keyHint("app.tools.expand", "to expand")})`)}`;
|
|
1260
1275
|
}
|
|
1261
1276
|
|
|
1262
1277
|
async function ensureTodoExists(filePath: string, id: string): Promise<TodoRecord | null> {
|
|
@@ -1784,21 +1799,6 @@ export default function todosExtension(pi: ExtensionAPI) {
|
|
|
1784
1799
|
|
|
1785
1800
|
pi.registerCommand("todos", {
|
|
1786
1801
|
description: "List todos from .pi/todos",
|
|
1787
|
-
getArgumentCompletions: (argumentPrefix: string) => {
|
|
1788
|
-
const todos = listTodosSync(getTodosDir(process.cwd()));
|
|
1789
|
-
if (!todos.length) return null;
|
|
1790
|
-
const matches = filterTodos(todos, argumentPrefix);
|
|
1791
|
-
if (!matches.length) return null;
|
|
1792
|
-
return matches.map((todo) => {
|
|
1793
|
-
const title = todo.title || "(untitled)";
|
|
1794
|
-
const tags = todo.tags.length ? ` • ${todo.tags.join(", ")}` : "";
|
|
1795
|
-
return {
|
|
1796
|
-
value: title,
|
|
1797
|
-
label: `${formatTodoId(todo.id)} ${title}`,
|
|
1798
|
-
description: `${todo.status || "open"}${tags}`,
|
|
1799
|
-
};
|
|
1800
|
-
});
|
|
1801
|
-
},
|
|
1802
1802
|
handler: async (args, ctx) => {
|
|
1803
1803
|
const todosDir = getTodosDir(ctx.cwd);
|
|
1804
1804
|
const todos = await listTodos(todosDir);
|
|
@@ -1813,7 +1813,7 @@ export default function todosExtension(pi: ExtensionAPI) {
|
|
|
1813
1813
|
|
|
1814
1814
|
let nextPrompt: string | null = null;
|
|
1815
1815
|
let rootTui: TUI | null = null;
|
|
1816
|
-
await ctx.ui.custom<void>((tui, theme,
|
|
1816
|
+
await ctx.ui.custom<void>((tui, theme, keybindings, done) => {
|
|
1817
1817
|
rootTui = tui;
|
|
1818
1818
|
let selector: TodoSelectorComponent | null = null;
|
|
1819
1819
|
let actionMenu: TodoActionMenuComponent | null = null;
|
|
@@ -1885,8 +1885,14 @@ export default function todosExtension(pi: ExtensionAPI) {
|
|
|
1885
1885
|
|
|
1886
1886
|
const openTodoOverlay = async (record: TodoRecord): Promise<TodoOverlayAction> => {
|
|
1887
1887
|
const action = await ctx.ui.custom<TodoOverlayAction>(
|
|
1888
|
-
(overlayTui, overlayTheme,
|
|
1889
|
-
new TodoDetailOverlayComponent(
|
|
1888
|
+
(overlayTui, overlayTheme, overlayKeybindings, overlayDone) =>
|
|
1889
|
+
new TodoDetailOverlayComponent(
|
|
1890
|
+
overlayTui,
|
|
1891
|
+
overlayTheme,
|
|
1892
|
+
overlayKeybindings,
|
|
1893
|
+
record,
|
|
1894
|
+
overlayDone,
|
|
1895
|
+
),
|
|
1890
1896
|
{
|
|
1891
1897
|
overlay: true,
|
|
1892
1898
|
overlayOptions: { width: "80%", maxHeight: "80%", anchor: "center" },
|
|
@@ -2022,6 +2028,7 @@ export default function todosExtension(pi: ExtensionAPI) {
|
|
|
2022
2028
|
selector = new TodoSelectorComponent(
|
|
2023
2029
|
tui,
|
|
2024
2030
|
theme,
|
|
2031
|
+
keybindings,
|
|
2025
2032
|
todos,
|
|
2026
2033
|
(todo) => {
|
|
2027
2034
|
void handleSelect(todo);
|
package/extensions/uv.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UV Extension - Redirects Python tooling to uv equivalents
|
|
3
|
+
*
|
|
4
|
+
* This extension wraps the bash tool to prepend intercepted-commands to PATH,
|
|
5
|
+
* which contains shim scripts that intercept common Python tooling commands
|
|
6
|
+
* and redirect agents to use uv instead.
|
|
7
|
+
*
|
|
8
|
+
* Intercepted commands:
|
|
9
|
+
* - pip/pip3: Blocked with suggestions to use `uv add` or `uv run --with`
|
|
10
|
+
* - poetry: Blocked with uv equivalents (uv init, uv add, uv sync, uv run)
|
|
11
|
+
* - python/python3: Redirected through `uv run` to a real interpreter path,
|
|
12
|
+
* with special handling to block `python -m pip`, `python -m venv`, and
|
|
13
|
+
* `python -m py_compile`
|
|
14
|
+
*
|
|
15
|
+
* The shim scripts are located in the intercepted-commands directory and
|
|
16
|
+
* provide helpful error messages with the equivalent uv commands.
|
|
17
|
+
*
|
|
18
|
+
* Note: PATH shims are bypassable via explicit interpreter paths
|
|
19
|
+
* (for example `.venv/bin/python`). To close that gap, this extension also
|
|
20
|
+
* blocks disallowed invocations at bash spawn time.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
24
|
+
import { createBashTool } from "@mariozechner/pi-coding-agent";
|
|
25
|
+
import { dirname, join } from "path";
|
|
26
|
+
import { fileURLToPath } from "url";
|
|
27
|
+
|
|
28
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const interceptedCommandsPath = join(__dirname, "..", "intercepted-commands");
|
|
30
|
+
|
|
31
|
+
function getBlockedCommandMessage(command: string): string | null {
|
|
32
|
+
// Match commands at the start of a shell segment (start/newline/; /&& /|| /|)
|
|
33
|
+
const pipCommandPattern = /(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?pip\s*(?:$|\s)/m;
|
|
34
|
+
const pip3CommandPattern = /(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?pip3\s*(?:$|\s)/m;
|
|
35
|
+
const poetryCommandPattern = /(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?poetry\s*(?:$|\s)/m;
|
|
36
|
+
|
|
37
|
+
// Match python invocations including explicit paths like .venv/bin/python
|
|
38
|
+
// and .venv/bin/python3.12.
|
|
39
|
+
const pythonPipPattern =
|
|
40
|
+
/(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?python(?:3(?:\.\d+)?)?\b[^\n;|&]*(?:\s-m\s*pip\b|\s-mpip\b)/m;
|
|
41
|
+
const pythonVenvPattern =
|
|
42
|
+
/(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?python(?:3(?:\.\d+)?)?\b[^\n;|&]*(?:\s-m\s*venv\b|\s-mvenv\b)/m;
|
|
43
|
+
const pythonPyCompilePattern =
|
|
44
|
+
/(?:^|\n|[;|&]{1,2})\s*(?:\S+\/)?python(?:3(?:\.\d+)?)?\b[^\n;|&]*(?:\s-m\s*py_compile\b|\s-mpy_compile\b)/m;
|
|
45
|
+
|
|
46
|
+
if (pipCommandPattern.test(command)) {
|
|
47
|
+
return [
|
|
48
|
+
"Error: pip is disabled. Use uv instead:",
|
|
49
|
+
"",
|
|
50
|
+
" To install a package for a script: uv run --with PACKAGE python script.py",
|
|
51
|
+
" To add a dependency to the project: uv add PACKAGE",
|
|
52
|
+
"",
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (pip3CommandPattern.test(command)) {
|
|
57
|
+
return [
|
|
58
|
+
"Error: pip3 is disabled. Use uv instead:",
|
|
59
|
+
"",
|
|
60
|
+
" To install a package for a script: uv run --with PACKAGE python script.py",
|
|
61
|
+
" To add a dependency to the project: uv add PACKAGE",
|
|
62
|
+
"",
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (poetryCommandPattern.test(command)) {
|
|
67
|
+
return [
|
|
68
|
+
"Error: poetry is disabled. Use uv instead:",
|
|
69
|
+
"",
|
|
70
|
+
" To initialize a project: uv init",
|
|
71
|
+
" To add a dependency: uv add PACKAGE",
|
|
72
|
+
" To sync dependencies: uv sync",
|
|
73
|
+
" To run commands: uv run COMMAND",
|
|
74
|
+
"",
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (pythonPipPattern.test(command)) {
|
|
79
|
+
return [
|
|
80
|
+
"Error: 'python -m pip' is disabled. Use uv instead:",
|
|
81
|
+
"",
|
|
82
|
+
" To install a package for a script: uv run --with PACKAGE python script.py",
|
|
83
|
+
" To add a dependency to the project: uv add PACKAGE",
|
|
84
|
+
"",
|
|
85
|
+
].join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (pythonVenvPattern.test(command)) {
|
|
89
|
+
return [
|
|
90
|
+
"Error: 'python -m venv' is disabled. Use uv instead:",
|
|
91
|
+
"",
|
|
92
|
+
" To create a virtual environment: uv venv",
|
|
93
|
+
"",
|
|
94
|
+
].join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (pythonPyCompilePattern.test(command)) {
|
|
98
|
+
return [
|
|
99
|
+
"Error: 'python -m py_compile' is disabled because it writes .pyc files to __pycache__.",
|
|
100
|
+
"",
|
|
101
|
+
" To verify syntax without bytecode output: uv run python -m ast path/to/file.py >/dev/null",
|
|
102
|
+
"",
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default function (pi: ExtensionAPI) {
|
|
110
|
+
const cwd = process.cwd();
|
|
111
|
+
const bashTool = createBashTool(cwd, {
|
|
112
|
+
commandPrefix: `export PATH="${interceptedCommandsPath}:$PATH"`,
|
|
113
|
+
spawnHook: (ctx) => {
|
|
114
|
+
const blockedMessage = getBlockedCommandMessage(ctx.command);
|
|
115
|
+
if (blockedMessage) {
|
|
116
|
+
throw new Error(blockedMessage);
|
|
117
|
+
}
|
|
118
|
+
return ctx;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
pi.registerTool(bashTool);
|
|
123
|
+
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
echo "Error: poetry is disabled. Use uv instead:" >&2
|
|
4
|
+
echo "" >&2
|
|
5
|
+
echo " To initialize a project: uv init" >&2
|
|
6
|
+
echo " To add a dependency: uv add PACKAGE" >&2
|
|
7
|
+
echo " To sync dependencies: uv sync" >&2
|
|
8
|
+
echo " To run commands: uv run COMMAND" >&2
|
|
9
|
+
echo "" >&2
|
|
3
10
|
exit 1
|
|
@@ -60,5 +60,45 @@ for arg in "$@"; do
|
|
|
60
60
|
prev="$arg"
|
|
61
61
|
done
|
|
62
62
|
|
|
63
|
-
#
|
|
64
|
-
|
|
63
|
+
# Resolve a Python interpreter for uv. Prefer uv-managed Python to match
|
|
64
|
+
# `uv run python` defaults, while still avoiding shim recursion.
|
|
65
|
+
resolve_uv_python() {
|
|
66
|
+
local shim_dir candidate candidate_dir
|
|
67
|
+
shim_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
68
|
+
|
|
69
|
+
candidate="$(uv python find --managed-python 2>/dev/null || true)"
|
|
70
|
+
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
|
71
|
+
candidate_dir="$(cd "$(dirname "$candidate")" && pwd)"
|
|
72
|
+
if [ "$candidate_dir" != "$shim_dir" ]; then
|
|
73
|
+
echo "$candidate"
|
|
74
|
+
return 0
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
while IFS= read -r candidate; do
|
|
79
|
+
[ -n "$candidate" ] || continue
|
|
80
|
+
candidate_dir="$(cd "$(dirname "$candidate")" && pwd)"
|
|
81
|
+
[ "$candidate_dir" = "$shim_dir" ] && continue
|
|
82
|
+
echo "$candidate"
|
|
83
|
+
return 0
|
|
84
|
+
done < <(type -aP python3 2>/dev/null)
|
|
85
|
+
|
|
86
|
+
while IFS= read -r candidate; do
|
|
87
|
+
[ -n "$candidate" ] || continue
|
|
88
|
+
candidate_dir="$(cd "$(dirname "$candidate")" && pwd)"
|
|
89
|
+
[ "$candidate_dir" = "$shim_dir" ] && continue
|
|
90
|
+
echo "$candidate"
|
|
91
|
+
return 0
|
|
92
|
+
done < <(type -aP python 2>/dev/null)
|
|
93
|
+
|
|
94
|
+
return 1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
UV_PYTHON="$(resolve_uv_python)"
|
|
98
|
+
if [ -z "$UV_PYTHON" ]; then
|
|
99
|
+
echo "Error: Unable to locate a Python interpreter outside intercepted-commands." >&2
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Dispatch through uv with an explicit interpreter path to avoid recursion.
|
|
104
|
+
exec uv run --python "$UV_PYTHON" python "$@"
|
|
@@ -60,5 +60,45 @@ for arg in "$@"; do
|
|
|
60
60
|
prev="$arg"
|
|
61
61
|
done
|
|
62
62
|
|
|
63
|
-
#
|
|
64
|
-
|
|
63
|
+
# Resolve a Python interpreter for uv. Prefer uv-managed Python to match
|
|
64
|
+
# `uv run python` defaults, while still avoiding shim recursion.
|
|
65
|
+
resolve_uv_python() {
|
|
66
|
+
local shim_dir candidate candidate_dir
|
|
67
|
+
shim_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
68
|
+
|
|
69
|
+
candidate="$(uv python find --managed-python 2>/dev/null || true)"
|
|
70
|
+
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
|
71
|
+
candidate_dir="$(cd "$(dirname "$candidate")" && pwd)"
|
|
72
|
+
if [ "$candidate_dir" != "$shim_dir" ]; then
|
|
73
|
+
echo "$candidate"
|
|
74
|
+
return 0
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
while IFS= read -r candidate; do
|
|
79
|
+
[ -n "$candidate" ] || continue
|
|
80
|
+
candidate_dir="$(cd "$(dirname "$candidate")" && pwd)"
|
|
81
|
+
[ "$candidate_dir" = "$shim_dir" ] && continue
|
|
82
|
+
echo "$candidate"
|
|
83
|
+
return 0
|
|
84
|
+
done < <(type -aP python3 2>/dev/null)
|
|
85
|
+
|
|
86
|
+
while IFS= read -r candidate; do
|
|
87
|
+
[ -n "$candidate" ] || continue
|
|
88
|
+
candidate_dir="$(cd "$(dirname "$candidate")" && pwd)"
|
|
89
|
+
[ "$candidate_dir" = "$shim_dir" ] && continue
|
|
90
|
+
echo "$candidate"
|
|
91
|
+
return 0
|
|
92
|
+
done < <(type -aP python 2>/dev/null)
|
|
93
|
+
|
|
94
|
+
return 1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
UV_PYTHON="$(resolve_uv_python)"
|
|
98
|
+
if [ -z "$UV_PYTHON" ]; then
|
|
99
|
+
echo "Error: Unable to locate a Python interpreter outside intercepted-commands." >&2
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Dispatch through uv with an explicit interpreter path to avoid recursion.
|
|
104
|
+
exec uv run --python "$UV_PYTHON" python "$@"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mitsupi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Armin's pi coding agent commands, skills, extensions, and themes",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,18 +16,21 @@
|
|
|
16
16
|
],
|
|
17
17
|
"pi": {
|
|
18
18
|
"extensions": [
|
|
19
|
-
"./
|
|
19
|
+
"./extensions"
|
|
20
20
|
],
|
|
21
21
|
"skills": [
|
|
22
22
|
"./skills"
|
|
23
23
|
],
|
|
24
24
|
"themes": [
|
|
25
|
-
"./
|
|
25
|
+
"./themes"
|
|
26
26
|
],
|
|
27
27
|
"prompts": [
|
|
28
28
|
"./commands"
|
|
29
29
|
]
|
|
30
30
|
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"diff": "^8.0.2"
|
|
33
|
+
},
|
|
31
34
|
"peerDependencies": {
|
|
32
35
|
"@mariozechner/pi-coding-agent": "*",
|
|
33
36
|
"@mariozechner/pi-ai": "*",
|