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.
Files changed (43) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +28 -18
  4. package/analyze-edits.py +232 -0
  5. package/distributions/README.md +8 -0
  6. package/distributions/mitsupi-common/README.md +11 -0
  7. package/distributions/mitsupi-common/package.json +95 -0
  8. package/distributions/mitsupi-loaded/README.md +13 -0
  9. package/distributions/mitsupi-loaded/package.json +38 -0
  10. package/{pi-extensions → extensions}/answer.ts +11 -11
  11. package/extensions/btw.ts +963 -0
  12. package/{pi-extensions → extensions}/context.ts +2 -2
  13. package/{pi-extensions → extensions}/control.ts +20 -13
  14. package/{pi-extensions → extensions}/files.ts +6 -8
  15. package/{pi-extensions → extensions}/go-to-bed.ts +2 -2
  16. package/{pi-extensions → extensions}/loop.ts +18 -11
  17. package/extensions/multi-edit.ts +871 -0
  18. package/{pi-extensions → extensions}/review.ts +254 -82
  19. package/{pi-extensions → extensions}/session-breakdown.ts +551 -74
  20. package/extensions/split-fork.ts +130 -0
  21. package/{pi-extensions → extensions}/todos.ts +41 -34
  22. package/extensions/uv.ts +123 -0
  23. package/intercepted-commands/poetry +8 -1
  24. package/intercepted-commands/python +42 -2
  25. package/intercepted-commands/python3 +42 -2
  26. package/package.json +6 -3
  27. package/skills/google-workspace/SKILL.md +53 -8
  28. package/skills/google-workspace/scripts/auth.js +106 -28
  29. package/skills/google-workspace/scripts/common.js +201 -32
  30. package/skills/google-workspace/scripts/workspace.js +64 -9
  31. package/skills/native-web-search/SKILL.md +2 -0
  32. package/skills/native-web-search/search.mjs +102 -15
  33. package/skills/pi-share/SKILL.md +5 -2
  34. package/skills/pi-share/fetch-session.mjs +46 -15
  35. package/skills/summarize/SKILL.md +4 -3
  36. package/skills/summarize/to-markdown.mjs +3 -1
  37. package/skills/web-browser/SKILL.md +6 -0
  38. package/skills/web-browser/scripts/start.js +75 -27
  39. package/pi-extensions/uv.ts +0 -33
  40. /package/{pi-extensions → extensions}/notify.ts +0 -0
  41. /package/{pi-extensions → extensions}/prompt-editor.ts +0 -0
  42. /package/{pi-extensions → extensions}/whimsical.ts +0 -0
  43. /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 = getEditorKeybindings();
401
- if (kb.matches(keyData, "selectUp")) {
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, "selectDown")) {
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, "selectConfirm")) {
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, "selectCancel")) {
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(tui: TUI, theme: Theme, todo: TodoRecord, onAction: (action: TodoOverlayAction) => void) {
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 = getEditorKeybindings();
577
- if (kb.matches(keyData, "selectCancel")) {
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, "selectConfirm")) {
595
+ if (kb.matches(keyData, "tui.select.confirm")) {
582
596
  this.onAction("work");
583
597
  return;
584
598
  }
585
- if (kb.matches(keyData, "selectUp")) {
599
+ if (kb.matches(keyData, "tui.select.up")) {
586
600
  this.scrollBy(-1);
587
601
  return;
588
602
  }
589
- if (kb.matches(keyData, "selectDown")) {
603
+ if (kb.matches(keyData, "tui.select.down")) {
590
604
  this.scrollBy(1);
591
605
  return;
592
606
  }
593
- if (kb.matches(keyData, "selectPageUp")) {
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, "selectPageDown")) {
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 pieces = [work, back];
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("expandTools", "to expand")})`)}`;
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, _kb, done) => {
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, _overlayKb, overlayDone) =>
1889
- new TodoDetailOverlayComponent(overlayTui, overlayTheme, record, overlayDone),
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);
@@ -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
- echo "Error: Use uv instead of poetry (uv init, uv add, uv sync, uv run)" >&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
- # Dispatch to uv run python
64
- exec uv run python "$@"
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
- # Dispatch to uv run python
64
- exec uv run python "$@"
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.4.0",
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
- "./pi-extensions"
19
+ "./extensions"
20
20
  ],
21
21
  "skills": [
22
22
  "./skills"
23
23
  ],
24
24
  "themes": [
25
- "./pi-themes"
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": "*",