pi-mono-all 1.0.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 (161) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENCE.md +7 -0
  3. package/node_modules/pi-common/package.json +22 -0
  4. package/node_modules/pi-common/src/auth-config.ts +290 -0
  5. package/node_modules/pi-common/src/auth.ts +63 -0
  6. package/node_modules/pi-common/src/cache.ts +60 -0
  7. package/node_modules/pi-common/src/errors.ts +47 -0
  8. package/node_modules/pi-common/src/http-client.ts +118 -0
  9. package/node_modules/pi-common/src/index.ts +7 -0
  10. package/node_modules/pi-common/src/rate-limiter.ts +32 -0
  11. package/node_modules/pi-common/src/tool-result.ts +27 -0
  12. package/node_modules/pi-mono-ask-user-question/CHANGELOG.md +185 -0
  13. package/node_modules/pi-mono-ask-user-question/README.md +226 -0
  14. package/node_modules/pi-mono-ask-user-question/index.ts +923 -0
  15. package/node_modules/pi-mono-ask-user-question/package.json +29 -0
  16. package/node_modules/pi-mono-auto-fix/CHANGELOG.md +59 -0
  17. package/node_modules/pi-mono-auto-fix/README.md +77 -0
  18. package/node_modules/pi-mono-auto-fix/index.ts +488 -0
  19. package/node_modules/pi-mono-auto-fix/package.json +23 -0
  20. package/node_modules/pi-mono-btw/CHANGELOG.md +180 -0
  21. package/node_modules/pi-mono-btw/README.md +24 -0
  22. package/node_modules/pi-mono-btw/index.ts +499 -0
  23. package/node_modules/pi-mono-btw/package.json +29 -0
  24. package/node_modules/pi-mono-clear/CHANGELOG.md +180 -0
  25. package/node_modules/pi-mono-clear/README.md +40 -0
  26. package/node_modules/pi-mono-clear/index.ts +45 -0
  27. package/node_modules/pi-mono-clear/package.json +29 -0
  28. package/node_modules/pi-mono-context/CHANGELOG.md +12 -0
  29. package/node_modules/pi-mono-context/README.md +74 -0
  30. package/node_modules/pi-mono-context/index.ts +641 -0
  31. package/node_modules/pi-mono-context/package.json +29 -0
  32. package/node_modules/pi-mono-context-guard/CHANGELOG.md +195 -0
  33. package/node_modules/pi-mono-context-guard/README.md +81 -0
  34. package/node_modules/pi-mono-context-guard/index.ts +212 -0
  35. package/node_modules/pi-mono-context-guard/package.json +23 -0
  36. package/node_modules/pi-mono-figma/CHANGELOG.md +59 -0
  37. package/node_modules/pi-mono-figma/README.md +236 -0
  38. package/node_modules/pi-mono-figma/__tests__/code-connect.test.ts +32 -0
  39. package/node_modules/pi-mono-figma/__tests__/figma-assets.test.ts +38 -0
  40. package/node_modules/pi-mono-figma/__tests__/figma-component-hints.test.ts +23 -0
  41. package/node_modules/pi-mono-figma/__tests__/figma-implementation-layout.test.ts +47 -0
  42. package/node_modules/pi-mono-figma/__tests__/figma-search.test.ts +51 -0
  43. package/node_modules/pi-mono-figma/__tests__/figma-summarizer.test.ts +65 -0
  44. package/node_modules/pi-mono-figma/__tests__/fixtures/complex-auto-layout.json +115 -0
  45. package/node_modules/pi-mono-figma/__tests__/fixtures/component-instance.json +50 -0
  46. package/node_modules/pi-mono-figma/__tests__/fixtures/hidden-and-vectors.json +28 -0
  47. package/node_modules/pi-mono-figma/__tests__/fixtures/variables-and-styles.json +40 -0
  48. package/node_modules/pi-mono-figma/docs/live-selection-bridge.md +16 -0
  49. package/node_modules/pi-mono-figma/index.ts +6 -0
  50. package/node_modules/pi-mono-figma/package.json +33 -0
  51. package/node_modules/pi-mono-figma/skills/figma/SKILL.md +143 -0
  52. package/node_modules/pi-mono-figma/src/code-connect.ts +110 -0
  53. package/node_modules/pi-mono-figma/src/figma-assets.ts +146 -0
  54. package/node_modules/pi-mono-figma/src/figma-cache.ts +6 -0
  55. package/node_modules/pi-mono-figma/src/figma-client.ts +471 -0
  56. package/node_modules/pi-mono-figma/src/figma-component-hints.ts +87 -0
  57. package/node_modules/pi-mono-figma/src/figma-implementation.ts +264 -0
  58. package/node_modules/pi-mono-figma/src/figma-schemas.ts +139 -0
  59. package/node_modules/pi-mono-figma/src/figma-search.ts +195 -0
  60. package/node_modules/pi-mono-figma/src/figma-summarizer.ts +673 -0
  61. package/node_modules/pi-mono-figma/src/figma-tokens.ts +57 -0
  62. package/node_modules/pi-mono-figma/src/figma-tools.ts +352 -0
  63. package/node_modules/pi-mono-linear/CHANGELOG.md +44 -0
  64. package/node_modules/pi-mono-linear/README.md +159 -0
  65. package/node_modules/pi-mono-linear/index.ts +6 -0
  66. package/node_modules/pi-mono-linear/package.json +30 -0
  67. package/node_modules/pi-mono-linear/skills/linear/SKILL.md +107 -0
  68. package/node_modules/pi-mono-linear/src/linear-client.ts +339 -0
  69. package/node_modules/pi-mono-linear/src/linear-queries.ts +101 -0
  70. package/node_modules/pi-mono-linear/src/linear-schemas.ts +90 -0
  71. package/node_modules/pi-mono-linear/src/linear-tools.ts +362 -0
  72. package/node_modules/pi-mono-loop/CHANGELOG.md +163 -0
  73. package/node_modules/pi-mono-loop/README.md +54 -0
  74. package/node_modules/pi-mono-loop/index.ts +291 -0
  75. package/node_modules/pi-mono-loop/package.json +26 -0
  76. package/node_modules/pi-mono-multi-edit/CHANGELOG.md +232 -0
  77. package/node_modules/pi-mono-multi-edit/README.md +244 -0
  78. package/node_modules/pi-mono-multi-edit/__tests__/classic.test.ts +277 -0
  79. package/node_modules/pi-mono-multi-edit/__tests__/diff.test.ts +77 -0
  80. package/node_modules/pi-mono-multi-edit/__tests__/patch.test.ts +287 -0
  81. package/node_modules/pi-mono-multi-edit/benchmark-edits.ts +966 -0
  82. package/node_modules/pi-mono-multi-edit/classic.ts +435 -0
  83. package/node_modules/pi-mono-multi-edit/diff.ts +143 -0
  84. package/node_modules/pi-mono-multi-edit/index.ts +266 -0
  85. package/node_modules/pi-mono-multi-edit/package.json +37 -0
  86. package/node_modules/pi-mono-multi-edit/patch.ts +463 -0
  87. package/node_modules/pi-mono-multi-edit/types.ts +53 -0
  88. package/node_modules/pi-mono-multi-edit/workspace.ts +85 -0
  89. package/node_modules/pi-mono-review/CHANGELOG.md +190 -0
  90. package/node_modules/pi-mono-review/README.md +30 -0
  91. package/node_modules/pi-mono-review/common.ts +930 -0
  92. package/node_modules/pi-mono-review/index.ts +8 -0
  93. package/node_modules/pi-mono-review/package.json +29 -0
  94. package/node_modules/pi-mono-review/review-tui.ts +194 -0
  95. package/node_modules/pi-mono-review/review.ts +119 -0
  96. package/node_modules/pi-mono-review/reviewer.ts +339 -0
  97. package/node_modules/pi-mono-sentinel/CHANGELOG.md +158 -0
  98. package/node_modules/pi-mono-sentinel/README.md +87 -0
  99. package/node_modules/pi-mono-sentinel/__tests__/output-scanner.test.ts +109 -0
  100. package/node_modules/pi-mono-sentinel/__tests__/permissions.test.ts +202 -0
  101. package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +59 -0
  102. package/node_modules/pi-mono-sentinel/guards/execution-tracker.ts +281 -0
  103. package/node_modules/pi-mono-sentinel/guards/output-scanner.ts +232 -0
  104. package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +170 -0
  105. package/node_modules/pi-mono-sentinel/index.ts +43 -0
  106. package/node_modules/pi-mono-sentinel/package.json +26 -0
  107. package/node_modules/pi-mono-sentinel/patterns/permissions.ts +175 -0
  108. package/node_modules/pi-mono-sentinel/patterns/read-targets.ts +104 -0
  109. package/node_modules/pi-mono-sentinel/patterns/secrets.ts +143 -0
  110. package/node_modules/pi-mono-sentinel/session.ts +95 -0
  111. package/node_modules/pi-mono-sentinel/specs/2026/04/sentinel/001-permission-gate.md +145 -0
  112. package/node_modules/pi-mono-sentinel/types.ts +39 -0
  113. package/node_modules/pi-mono-sentinel/whitelist.ts +86 -0
  114. package/node_modules/pi-mono-simplify/CHANGELOG.md +163 -0
  115. package/node_modules/pi-mono-simplify/README.md +56 -0
  116. package/node_modules/pi-mono-simplify/index.ts +78 -0
  117. package/node_modules/pi-mono-simplify/package.json +29 -0
  118. package/node_modules/pi-mono-status-line/CHANGELOG.md +180 -0
  119. package/node_modules/pi-mono-status-line/README.md +96 -0
  120. package/node_modules/pi-mono-status-line/basic.ts +89 -0
  121. package/node_modules/pi-mono-status-line/expert.ts +689 -0
  122. package/node_modules/pi-mono-status-line/index.ts +54 -0
  123. package/node_modules/pi-mono-status-line/package.json +29 -0
  124. package/node_modules/pi-mono-team-mode/CHANGELOG.md +278 -0
  125. package/node_modules/pi-mono-team-mode/README.md +246 -0
  126. package/node_modules/pi-mono-team-mode/__tests__/agent-manager-transient.test.ts +75 -0
  127. package/node_modules/pi-mono-team-mode/__tests__/delegation-manager.test.ts +118 -0
  128. package/node_modules/pi-mono-team-mode/__tests__/formatters.test.ts +104 -0
  129. package/node_modules/pi-mono-team-mode/__tests__/model-config.test.ts +272 -0
  130. package/node_modules/pi-mono-team-mode/__tests__/notification-box.test.ts +34 -0
  131. package/node_modules/pi-mono-team-mode/__tests__/parallel-utils.test.ts +32 -0
  132. package/node_modules/pi-mono-team-mode/__tests__/pi-stream-parser.test.ts +64 -0
  133. package/node_modules/pi-mono-team-mode/__tests__/prompts.test.ts +106 -0
  134. package/node_modules/pi-mono-team-mode/__tests__/store.test.ts +164 -0
  135. package/node_modules/pi-mono-team-mode/__tests__/tasks.test.ts +267 -0
  136. package/node_modules/pi-mono-team-mode/__tests__/teammate-specs.test.ts +114 -0
  137. package/node_modules/pi-mono-team-mode/__tests__/widget.test.ts +41 -0
  138. package/node_modules/pi-mono-team-mode/__tests__/worktree.test.ts +78 -0
  139. package/node_modules/pi-mono-team-mode/core/chain-utils.ts +90 -0
  140. package/node_modules/pi-mono-team-mode/core/fs-utils.ts +44 -0
  141. package/node_modules/pi-mono-team-mode/core/model-config.ts +432 -0
  142. package/node_modules/pi-mono-team-mode/core/parallel-utils.ts +48 -0
  143. package/node_modules/pi-mono-team-mode/core/prompts.ts +158 -0
  144. package/node_modules/pi-mono-team-mode/core/store.ts +156 -0
  145. package/node_modules/pi-mono-team-mode/core/tasks.ts +99 -0
  146. package/node_modules/pi-mono-team-mode/core/teammate-specs.ts +124 -0
  147. package/node_modules/pi-mono-team-mode/core/types.ts +160 -0
  148. package/node_modules/pi-mono-team-mode/index.ts +825 -0
  149. package/node_modules/pi-mono-team-mode/managers/agent-manager.ts +654 -0
  150. package/node_modules/pi-mono-team-mode/managers/delegation-manager.ts +211 -0
  151. package/node_modules/pi-mono-team-mode/managers/task-manager.ts +238 -0
  152. package/node_modules/pi-mono-team-mode/managers/team-manager.ts +59 -0
  153. package/node_modules/pi-mono-team-mode/package.json +33 -0
  154. package/node_modules/pi-mono-team-mode/runtime/pi-stream-parser.ts +194 -0
  155. package/node_modules/pi-mono-team-mode/runtime/subprocess.ts +183 -0
  156. package/node_modules/pi-mono-team-mode/runtime/transient-session.ts +196 -0
  157. package/node_modules/pi-mono-team-mode/runtime/worktree.ts +90 -0
  158. package/node_modules/pi-mono-team-mode/ui/formatters.ts +149 -0
  159. package/node_modules/pi-mono-team-mode/ui/notification-box.ts +55 -0
  160. package/node_modules/pi-mono-team-mode/ui/widget.ts +94 -0
  161. package/package.json +76 -0
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "pi-mono-ask-user-question",
3
+ "version": "1.7.3",
4
+ "description": "Pi extension for asking users structured interactive questions",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension"
8
+ ],
9
+ "peerDependencies": {
10
+ "@mariozechner/pi-ai": "*",
11
+ "@mariozechner/pi-coding-agent": "*",
12
+ "@mariozechner/pi-tui": "*",
13
+ "@sinclair/typebox": "*"
14
+ },
15
+ "pi": {
16
+ "extensions": [
17
+ "./index.ts"
18
+ ]
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/emanuelcasco/pi-mono-extensions.git",
23
+ "directory": "extensions/ask-user-question"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/emanuelcasco/pi-mono-extensions/issues"
27
+ },
28
+ "homepage": "https://github.com/emanuelcasco/pi-mono-extensions#readme"
29
+ }
@@ -0,0 +1,59 @@
1
+ # pi-mono-auto-fix
2
+
3
+ ## 0.3.0
4
+
5
+ ### Added
6
+
7
+ - **ESLint version-aware dispatch with config detection**: Auto-fix now determines the project's ESLint version and config format before running.
8
+ - Walks from the file's directory up to `package.json` to find the project root.
9
+ - Detects flat (`eslint.config.*`) vs. legacy (`.eslintrc.*`) configs.
10
+ - Reads the installed ESLint major version from `node_modules/eslint/package.json`.
11
+ | Local ESLint | Config Format | Action |
12
+ | --- | --- | --- |
13
+ | v8 | Flat config | **Skip** — incompatible |
14
+ | v9+ | Legacy config | Use local + `ESLINT_USE_FLAT_CONFIG=false` |
15
+ | v8 | Legacy config | Use local binary directly |
16
+ | v9+ | Flat config | Use local binary directly |
17
+ | None | Legacy config | `npx --yes eslint@8` |
18
+ | None | Flat config | `npx --yes eslint@9` |
19
+ | None | No config | **Skip** — avoids injecting rules on projects that don't use ESLint |
20
+
21
+ ### Changed
22
+
23
+ - `resolveSpawnTarget` now returns `{ command, spawnCwd, env? }` so fixers can pass environment overrides (e.g. `ESLINT_USE_FLAT_CONFIG`).
24
+ - `runFixer` branches labelled `"eslint"` through the new `resolveEslintCommand` resolver.
25
+
26
+ ### Fixed
27
+
28
+ - Tightened `spawn` invocation typing for the release package without changing runtime behavior.
29
+
30
+ ## 0.2.2
31
+
32
+ ### Fixed
33
+
34
+ - **eslint failing on every file in projects pinned to v8**: The `0.2.1` neutral-cwd fix made `npx eslint` resolve from `/tmp`, which auto-installs the latest ESLint (v10+) and ignores the project's pinned version and config. Projects on ESLint v8 with `.eslintrc.*` then failed with "ESLint couldn't find an eslint.config.(js|mjs|cjs) file." on every file. Auto-fix now walks up from each file to find a project-local `node_modules/.bin/<tool>` and execs it directly with the project root as cwd, falling back to neutral-cwd npx only when no local install exists.
35
+
36
+ ## 0.2.0
37
+
38
+ ### Fixed
39
+
40
+ - **Auto-install dependencies**: Fixers now use `npx` (without `--no-install`) for eslint/prettier and `uvx` for ruff, auto-installing if missing — no manual install required.
41
+ - **False failure reporting with eslint**: `eslint --fix` exits non-zero when it finds unfixable issues, even if it successfully fixed others in the same pass. Failure detection now checks mtime (file actually changed) instead of exit code, so successful fixes are no longer reported as failures.
42
+ - **npx broken in pnpm monorepos**: `npx` delegates to pnpm when `package.json` declares `"packageManager": "pnpm"`, but `pnpm exec` doesn't auto-install like npx does. Fixers now use a neutral cwd (`/tmp`) for npx commands to bypass project-level toolchain interference.
43
+
44
+ ### Changed
45
+
46
+ - Python fixer swapped from `black` (global install required) to `uvx ruff check --fix && uvx ruff format` (auto-installs via PyPI, faster).
47
+
48
+ ## 0.1.0
49
+
50
+ ### Minor Changes
51
+
52
+ ### New Extension: auto-fix
53
+
54
+ End-of-turn formatter/linter dispatcher. Subscribes to `tool_result` (for `edit` / `write`) and the `context-guard:file-modified` event bus to collect every path written during a turn, then on `agent_end` routes each path to a language-appropriate fixer (eslint / black / prettier by default) and runs them in parallel. Re-emits `context-guard:file-modified` for files whose mtime actually changed so downstream read caches evict.
55
+
56
+ - Configurable via `~/.pi/agent/auto-fix.json` (fixers, ignore patterns, timeout, concurrency)
57
+ - Disable with `PI_AUTO_FIX=0`
58
+ - Silent stdout/stderr; single summary notification per flush
59
+ - Paths outside `ctx.cwd` and deleted files are skipped automatically
@@ -0,0 +1,77 @@
1
+ # pi-mono-auto-fix
2
+
3
+ End-of-turn formatter/linter dispatcher for pi. Collects every file written during a turn and applies language-appropriate fixers (eslint, black, prettier, …) in one batch once the agent stops talking.
4
+
5
+ ## What it does
6
+
7
+ - Subscribes to `tool_result` for the built-in `edit` and `write` tools, plus the `context-guard:file-modified` event that `multi-edit` and other writers emit.
8
+ - Buffers absolute paths of touched files in a per-turn `Set`.
9
+ - On `agent_end`, groups paths by matching fixer, runs each fixer once per group (parallel up to `concurrency`).
10
+ - After each run, re-emits `context-guard:file-modified` for any file whose mtime actually changed, so downstream read caches evict.
11
+ - Emits a single notification at the end (`auto-fix: N/M files updated`).
12
+
13
+ Fixers are invoked silently — stdout/stderr are swallowed. Failures are reported in the summary notification but never surfaced into the LLM context.
14
+
15
+ ## Built-in fixer rules
16
+
17
+ | Extensions | Command |
18
+ | ------------------------------------------------- | -------------------------------------------------------------- |
19
+ | `.ts .tsx .js .jsx .mjs .cjs` | `npx --no-install eslint --fix --no-error-on-unmatched-pattern {files}` |
20
+ | `.py` | `black -q {files}` |
21
+ | `.json .md .yml .yaml .css .scss .html` | `npx --no-install prettier --write --log-level=warn {files}` |
22
+
23
+ `{files}` is replaced with shell-quoted, space-separated absolute paths. If the token is missing, files are appended to the end of the command.
24
+
25
+ Commands run with `shell: true`, `cwd = ctx.cwd`, and a per-invocation timeout (default 60s).
26
+
27
+ ## Configuration
28
+
29
+ Resolution order (first hit wins):
30
+
31
+ 1. `PI_AUTO_FIX=0` → extension is fully disabled (no listeners registered)
32
+ 2. `~/.pi/agent/auto-fix.json`
33
+ 3. built-in defaults
34
+
35
+ Example `~/.pi/agent/auto-fix.json`:
36
+
37
+ ```json
38
+ {
39
+ "enabled": true,
40
+ "timeoutMs": 90000,
41
+ "concurrency": 2,
42
+ "ignore": ["node_modules/", "dist/", ".git/", "vendor/"],
43
+ "fixers": [
44
+ {
45
+ "label": "biome",
46
+ "extensions": [".ts", ".tsx", ".js", ".jsx"],
47
+ "command": "npx --no-install biome check --write --no-errors-on-unmatched {files}"
48
+ },
49
+ {
50
+ "label": "ruff",
51
+ "extensions": [".py"],
52
+ "command": "ruff check --fix {files} && ruff format {files}"
53
+ }
54
+ ]
55
+ }
56
+ ```
57
+
58
+ All fields are optional; anything omitted falls back to the built-in default.
59
+
60
+ ## Install
61
+
62
+ ```bash
63
+ pi install npm:pi-mono-auto-fix
64
+ ```
65
+
66
+ Or load directly for testing:
67
+
68
+ ```bash
69
+ pi -e /path/to/pi-extensions/extensions/auto-fix/index.ts
70
+ ```
71
+
72
+ ## Notes
73
+
74
+ - Paths outside `ctx.cwd` are skipped for safety.
75
+ - Paths matching any substring in `ignore` are skipped.
76
+ - Files deleted during the turn are skipped (existence is re-checked at flush time).
77
+ - The mtime diff catches fixers that are no-ops on already-clean files, so the summary reflects real changes rather than just invocations.
@@ -0,0 +1,488 @@
1
+ /**
2
+ * auto-fix — end-of-turn formatter/linter dispatcher.
3
+ *
4
+ * Collects every file written during a turn (via `edit` / `write` tool
5
+ * results and `context-guard:file-modified` events), then on `agent_end`
6
+ * dispatches each file to a language-appropriate fixer command (eslint,
7
+ * black, prettier, etc.). Fixes are applied silently; the user is only
8
+ * notified of the final summary.
9
+ *
10
+ * Config resolution order (first hit wins):
11
+ * 1. PI_AUTO_FIX=0 → extension is disabled entirely
12
+ * 2. ~/.pi/agent/auto-fix.json
13
+ * 3. built-in defaults (see DEFAULT_FIXERS below)
14
+ */
15
+
16
+ import type {
17
+ ExtensionAPI,
18
+ ExtensionContext,
19
+ } from "@mariozechner/pi-coding-agent";
20
+ import { spawn } from "node:child_process";
21
+ import { existsSync, readFileSync, statSync } from "node:fs";
22
+ import { homedir } from "node:os";
23
+ import { dirname, extname, isAbsolute, relative, resolve } from "node:path";
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Config
27
+ // ---------------------------------------------------------------------------
28
+
29
+ interface FixerRule {
30
+ /** File extensions this rule matches (include leading dot, lowercase). */
31
+ extensions: string[];
32
+ /** Shell command; `{files}` is replaced with space-separated, quoted paths. */
33
+ command: string;
34
+ /** Optional human-readable label used in notifications. */
35
+ label?: string;
36
+ }
37
+
38
+ interface Config {
39
+ enabled: boolean;
40
+ fixers: FixerRule[];
41
+ /** Glob-ish substring ignore patterns applied to the relative path. */
42
+ ignore: string[];
43
+ /** Per-fixer timeout in ms. */
44
+ timeoutMs: number;
45
+ /** Max parallel fixer invocations. */
46
+ concurrency: number;
47
+ }
48
+
49
+ const DEFAULT_FIXERS: FixerRule[] = [
50
+ {
51
+ label: "eslint",
52
+ extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
53
+ command: "npx eslint --fix --no-error-on-unmatched-pattern {files}",
54
+ },
55
+ {
56
+ label: "ruff",
57
+ extensions: [".py"],
58
+ command:
59
+ "uvx ruff check --fix --quiet {files} && uvx ruff format --quiet {files}",
60
+ },
61
+ {
62
+ label: "prettier",
63
+ extensions: [".json", ".md", ".yml", ".yaml", ".css", ".scss", ".html"],
64
+ command: "npx prettier --write --log-level=warn {files}",
65
+ },
66
+ ];
67
+
68
+ const DEFAULT_CONFIG: Config = {
69
+ enabled: true,
70
+ fixers: DEFAULT_FIXERS,
71
+ ignore: ["node_modules/", "dist/", "build/", ".git/", ".next/", "coverage/"],
72
+ timeoutMs: 60_000,
73
+ concurrency: 3,
74
+ };
75
+
76
+ const CONFIG_PATH = `${homedir()}/.pi/agent/auto-fix.json`;
77
+
78
+ function loadConfig(): Config {
79
+ if (process.env.PI_AUTO_FIX === "0") {
80
+ return { ...DEFAULT_CONFIG, enabled: false };
81
+ }
82
+ if (!existsSync(CONFIG_PATH)) return DEFAULT_CONFIG;
83
+ try {
84
+ const parsed = JSON.parse(
85
+ readFileSync(CONFIG_PATH, "utf-8"),
86
+ ) as Partial<Config>;
87
+ return {
88
+ enabled: parsed.enabled ?? DEFAULT_CONFIG.enabled,
89
+ fixers: parsed.fixers ?? DEFAULT_CONFIG.fixers,
90
+ ignore: parsed.ignore ?? DEFAULT_CONFIG.ignore,
91
+ timeoutMs: parsed.timeoutMs ?? DEFAULT_CONFIG.timeoutMs,
92
+ concurrency: parsed.concurrency ?? DEFAULT_CONFIG.concurrency,
93
+ };
94
+ } catch {
95
+ return DEFAULT_CONFIG;
96
+ }
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Helpers
101
+ // ---------------------------------------------------------------------------
102
+
103
+ function shellQuote(s: string): string {
104
+ return `'${s.replace(/'/g, "'\\''")}'`;
105
+ }
106
+
107
+ function matchFixer(
108
+ absPath: string,
109
+ fixers: FixerRule[],
110
+ ): FixerRule | undefined {
111
+ const ext = extname(absPath).toLowerCase();
112
+ if (!ext) return undefined;
113
+ return fixers.find((f) => f.extensions.includes(ext));
114
+ }
115
+
116
+ function isIgnored(relPath: string, ignore: string[]): boolean {
117
+ return ignore.some((p) => relPath.includes(p));
118
+ }
119
+
120
+ function mtimeSafe(path: string): number {
121
+ try {
122
+ return statSync(path).mtimeMs;
123
+ } catch {
124
+ return -1;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Walk up from `startDir` until a directory containing `node_modules/.bin/<tool>`
130
+ * is found. Returns `{ binPath, projectRoot }` or undefined.
131
+ */
132
+ function findLocalBin(
133
+ startDir: string,
134
+ tool: string,
135
+ ): { binPath: string; projectRoot: string } | undefined {
136
+ let dir = startDir;
137
+ while (true) {
138
+ const binPath = `${dir}/node_modules/.bin/${tool}`;
139
+ if (existsSync(binPath)) return { binPath, projectRoot: dir };
140
+ const parent = dirname(dir);
141
+ if (parent === dir) return undefined;
142
+ dir = parent;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Walk up from `startDir` until a `package.json` is found.
148
+ */
149
+ function findProjectRoot(startDir: string): string | undefined {
150
+ let dir = startDir;
151
+ while (true) {
152
+ if (existsSync(`${dir}/package.json`)) return dir;
153
+ const parent = dirname(dir);
154
+ if (parent === dir) return undefined;
155
+ dir = parent;
156
+ }
157
+ }
158
+
159
+ const ESLINT_FLAT_CONFIG_FILES = [
160
+ "eslint.config.mjs",
161
+ "eslint.config.js",
162
+ "eslint.config.cjs",
163
+ "eslint.config.ts",
164
+ "eslint.config.mts",
165
+ "eslint.config.cts",
166
+ ];
167
+
168
+ const ESLINT_LEGACY_CONFIG_FILES = [
169
+ ".eslintrc.cjs",
170
+ ".eslintrc.mjs",
171
+ ".eslintrc.js",
172
+ ".eslintrc.json",
173
+ ".eslintrc.yaml",
174
+ ".eslintrc.yml",
175
+ ".eslintrc",
176
+ ];
177
+
178
+ function detectEslintConfigType(projectRoot: string): "flat" | "legacy" | "none" {
179
+ for (const f of ESLINT_FLAT_CONFIG_FILES) {
180
+ if (existsSync(`${projectRoot}/${f}`)) return "flat";
181
+ }
182
+ for (const f of ESLINT_LEGACY_CONFIG_FILES) {
183
+ if (existsSync(`${projectRoot}/${f}`)) return "legacy";
184
+ }
185
+ return "none";
186
+ }
187
+
188
+ function getEslintVersion(projectRoot: string): number | null {
189
+ const pkgPath = `${projectRoot}/node_modules/eslint/package.json`;
190
+ if (!existsSync(pkgPath)) return null;
191
+ try {
192
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
193
+ const major = parseInt(pkg.version.split(".")[0], 10);
194
+ return isNaN(major) ? null : major;
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ interface SpawnTarget {
201
+ command: string | null;
202
+ spawnCwd: string;
203
+ env?: Record<string, string>;
204
+ }
205
+
206
+ /**
207
+ * ESLint-specific resolver that avoids version / config mismatches.
208
+ *
209
+ * Logic:
210
+ * 1. Local ESLint installed?
211
+ * a. v9+ + legacy config → use local + ESLINT_USE_FLAT_CONFIG=false
212
+ * b. v8 + flat config → skip (incompatible)
213
+ * c. otherwise → use local binary directly
214
+ * 2. No local ESLint?
215
+ * a. legacy config → `npx --yes eslint@8` (pin v8)
216
+ * b. flat config → `npx --yes eslint@9` (pin v9+)
217
+ * c. no config → skip
218
+ */
219
+ function resolveEslintCommand(
220
+ commandTemplate: string,
221
+ files: string[],
222
+ cwd: string,
223
+ ): SpawnTarget {
224
+ const firstFile = files[0];
225
+ const fileDir = firstFile ? dirname(firstFile) : cwd;
226
+ const projectRoot = findProjectRoot(fileDir) ?? cwd;
227
+
228
+ const local = findLocalBin(fileDir, "eslint");
229
+ const configType = detectEslintConfigType(projectRoot);
230
+ const filesArg = files.map(shellQuote).join(" ");
231
+
232
+ // Strip the binary invocation and {files} placeholder to keep only flags.
233
+ const extraArgs = commandTemplate
234
+ .replace(/^npx\s+/, "")
235
+ .replace(/^(?:[.\\/][\S]+[\\/])?eslint(?:\.exe)?\b\s*/, "")
236
+ .replace("{files}", "")
237
+ .trim();
238
+
239
+ // CASE 1: Local ESLint installed — honour project's pinned version.
240
+ if (local) {
241
+ const version = getEslintVersion(local.projectRoot);
242
+
243
+ // ESLint v9+ on a legacy-config project: force legacy mode so the v9
244
+ // binary can still read .eslintrc.* files.
245
+ if (version !== null && version >= 9 && configType === "legacy") {
246
+ return {
247
+ command: `${shellQuote(local.binPath)} ${extraArgs} ${filesArg}`.trim(),
248
+ spawnCwd: local.projectRoot,
249
+ env: { ESLINT_USE_FLAT_CONFIG: "false" },
250
+ };
251
+ }
252
+
253
+ // ESLint v8 on a flat-config project: incompatible — skip.
254
+ if (version !== null && version < 9 && configType === "flat") {
255
+ return { command: null, spawnCwd: local.projectRoot };
256
+ }
257
+
258
+ return {
259
+ command: `${shellQuote(local.binPath)} ${extraArgs} ${filesArg}`.trim(),
260
+ spawnCwd: local.projectRoot,
261
+ };
262
+ }
263
+
264
+ // CASE 2: No local ESLint — pin a version compatible with the project's
265
+ // config format so we never install latest v9 on a legacy-config repo.
266
+ if (configType === "legacy") {
267
+ return {
268
+ command: `npx --yes eslint@8 ${extraArgs} ${filesArg}`.trim(),
269
+ spawnCwd: projectRoot,
270
+ };
271
+ }
272
+
273
+ if (configType === "flat") {
274
+ return {
275
+ command: `npx --yes eslint@9 ${extraArgs} ${filesArg}`.trim(),
276
+ spawnCwd: projectRoot,
277
+ };
278
+ }
279
+
280
+ // CASE 3: No ESLint config found — skip to avoid injecting unwanted rules.
281
+ return { command: null, spawnCwd: projectRoot };
282
+ }
283
+
284
+ /**
285
+ * If `command` starts with `npx <tool>`, attempt to resolve a project-local
286
+ * binary by walking up from the first file. On hit, rewrite the command to
287
+ * exec the local bin directly and return the project root as cwd. On miss,
288
+ * fall back to `/tmp` so npx can fetch the latest from the registry without
289
+ * being delegated through pnpm.
290
+ */
291
+ function resolveSpawnTarget(
292
+ command: string,
293
+ files: string[],
294
+ cwd: string,
295
+ ): SpawnTarget {
296
+ const npxMatch = command.match(/^npx\s+(\S+)/);
297
+ if (!npxMatch) return { command, spawnCwd: cwd };
298
+
299
+ const tool = npxMatch[1];
300
+ const firstFile = files[0];
301
+ if (firstFile) {
302
+ const local = findLocalBin(dirname(firstFile), tool);
303
+ if (local) {
304
+ return {
305
+ command: command.replace(/^npx\s+\S+/, shellQuote(local.binPath)),
306
+ spawnCwd: local.projectRoot,
307
+ };
308
+ }
309
+ }
310
+ // No local install — neutral cwd avoids pnpm delegating npx.
311
+ return { command, spawnCwd: "/tmp" };
312
+ }
313
+
314
+ async function runFixer(
315
+ rule: FixerRule,
316
+ files: string[],
317
+ cwd: string,
318
+ timeoutMs: number,
319
+ ): Promise<{ ok: boolean; stderr: string }> {
320
+ const filesArg = files.map(shellQuote).join(" ");
321
+
322
+ let resolved: SpawnTarget;
323
+
324
+ if (rule.label === "eslint") {
325
+ // ESLint gets version-aware command resolution so we don't install a
326
+ // global v9 on a legacy v8-configured project (or vice-versa).
327
+ resolved = resolveEslintCommand(rule.command, files, cwd);
328
+ } else {
329
+ const rawCommand = rule.command.includes("{files}")
330
+ ? rule.command.replace("{files}", filesArg)
331
+ : `${rule.command} ${filesArg}`;
332
+ // Prefer the project's locally installed binary when available so we
333
+ // honor the pinned version + config. Only fall back to neutral-cwd npx
334
+ // when no local install is found, which sidesteps pnpm's delegation of
335
+ // npx without auto-install.
336
+ resolved = resolveSpawnTarget(rawCommand, files, cwd);
337
+ }
338
+
339
+ const command = resolved.command;
340
+ if (command === null) {
341
+ return {
342
+ ok: true,
343
+ stderr: `Skipped ${rule.label ?? rule.command}: no compatible target found.`,
344
+ };
345
+ }
346
+
347
+ return new Promise((resolvePromise) => {
348
+ const child = spawn(command, [], {
349
+ cwd: resolved.spawnCwd,
350
+ shell: true,
351
+ env: { ...process.env, ...resolved.env },
352
+ });
353
+ let stderr = "";
354
+ const timer = setTimeout(() => child.kill("SIGTERM"), timeoutMs);
355
+ child.stderr?.on("data", (chunk: Buffer) => {
356
+ stderr += chunk.toString();
357
+ });
358
+ child.on("error", () => {
359
+ clearTimeout(timer);
360
+ resolvePromise({ ok: false, stderr });
361
+ });
362
+ child.on("close", (code: number | null) => {
363
+ clearTimeout(timer);
364
+ resolvePromise({ ok: code === 0, stderr });
365
+ });
366
+ });
367
+ }
368
+
369
+ async function runWithConcurrency<T>(
370
+ items: T[],
371
+ limit: number,
372
+ worker: (item: T) => Promise<void>,
373
+ ): Promise<void> {
374
+ const queue = [...items];
375
+ const workers = Array.from(
376
+ { length: Math.min(limit, queue.length) },
377
+ async () => {
378
+ while (queue.length) {
379
+ const item = queue.shift();
380
+ if (item === undefined) return;
381
+ await worker(item);
382
+ }
383
+ },
384
+ );
385
+ await Promise.all(workers);
386
+ }
387
+
388
+ // ---------------------------------------------------------------------------
389
+ // Extension
390
+ // ---------------------------------------------------------------------------
391
+
392
+ export default function (pi: ExtensionAPI): void {
393
+ const cfg = loadConfig();
394
+ if (!cfg.enabled) return;
395
+
396
+ /** Absolute paths written during the current turn. */
397
+ const pending = new Set<string>();
398
+
399
+ function collect(rawPath: string, cwd: string): void {
400
+ if (!rawPath) return;
401
+ const absolutePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
402
+ const rel = relative(cwd, absolutePath);
403
+ if (rel.startsWith("..")) return; // outside cwd — skip
404
+ if (isIgnored(rel, cfg.ignore)) return;
405
+ pending.add(absolutePath);
406
+ }
407
+
408
+ // Reset between agent runs (turn boundary for pi's purposes).
409
+ pi.on("agent_start", () => {
410
+ pending.clear();
411
+ });
412
+
413
+ // Collector 1: direct edit/write tool results.
414
+ pi.on("tool_result", async (event, ctx) => {
415
+ if (event.isError) return;
416
+ if (event.toolName !== "edit" && event.toolName !== "write") return;
417
+ const rawPath = (event.input as { path?: string }).path;
418
+ if (rawPath) collect(rawPath, ctx.cwd);
419
+ });
420
+
421
+ // End-of-turn flush.
422
+ pi.on("agent_end", async (_event, ctx) => {
423
+ if (!pending.size) return;
424
+ const paths = [...pending];
425
+ pending.clear();
426
+
427
+ await flush(paths, ctx);
428
+ });
429
+
430
+ async function flush(paths: string[], ctx: ExtensionContext): Promise<void> {
431
+ // Filter to existing files and group by fixer rule.
432
+ const groups = new Map<FixerRule, string[]>();
433
+ for (const p of paths) {
434
+ if (!existsSync(p)) continue;
435
+ const rule = matchFixer(p, cfg.fixers);
436
+ if (!rule) continue;
437
+ const bucket = groups.get(rule) ?? [];
438
+ bucket.push(p);
439
+ groups.set(rule, bucket);
440
+ }
441
+ if (!groups.size) return;
442
+
443
+ let changed = 0;
444
+ const failures: string[] = [];
445
+ const jobs = [...groups.entries()];
446
+
447
+ await runWithConcurrency(jobs, cfg.concurrency, async ([rule, files]) => {
448
+ // Snapshot mtimes *before* running this fixer group.
449
+ const groupBefore = new Map(files.map((p) => [p, mtimeSafe(p)]));
450
+
451
+ const result = await runFixer(rule, files, ctx.cwd, cfg.timeoutMs);
452
+
453
+ // Count how many files this fixer actually changed.
454
+ const groupChanged = files.filter(
455
+ (p) => mtimeSafe(p) !== groupBefore.get(p),
456
+ ).length;
457
+ changed += groupChanged;
458
+
459
+ // Only count as failure if the tool failed AND no files changed.
460
+ // Some tools (eslint --fix) exit non-zero even when they fix things.
461
+ if (!result.ok && groupChanged === 0) {
462
+ failures.push(
463
+ `${rule.label ?? rule.command.split(" ")[0]} (${files.length} file${files.length === 1 ? "" : "s"})`,
464
+ );
465
+ }
466
+
467
+ // Re-emit file-modified for anything the fixer actually rewrote so
468
+ // context-guard evicts its stale read cache.
469
+ for (const p of files) {
470
+ if (mtimeSafe(p) !== groupBefore.get(p)) {
471
+ pi.events.emit("context-guard:file-modified", { path: p });
472
+ }
473
+ }
474
+ });
475
+
476
+ const total = [...groups.values()].reduce((n, b) => n + b.length, 0);
477
+ if (changed > 0 || failures.length) {
478
+ const parts: string[] = [
479
+ `auto-fix: ${changed}/${total} file${total === 1 ? "" : "s"} updated`,
480
+ ];
481
+ if (failures.length) parts.push(`failed: ${failures.join(", ")}`);
482
+ ctx.ui.notify(
483
+ `[auto-fix] ${parts.join(" — ")}`,
484
+ failures.length ? "warning" : "info",
485
+ );
486
+ }
487
+ }
488
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "pi-mono-auto-fix",
3
+ "version": "0.3.0",
4
+ "description": "Pi extension that runs language-appropriate fixers (eslint, black, prettier, ...) on files touched during a turn",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension"
8
+ ],
9
+ "peerDependencies": {
10
+ "@mariozechner/pi-coding-agent": "*",
11
+ "@sinclair/typebox": "*"
12
+ },
13
+ "pi": {
14
+ "extensions": [
15
+ "./index.ts"
16
+ ]
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/emanuelcasco/pi-mono-extensions.git",
21
+ "directory": "extensions/auto-fix"
22
+ }
23
+ }