@geminixiang/pi-simplify 0.1.0 → 0.1.3

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/CHANGELOG.md CHANGED
@@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.3] - 2026-09-07
10
+
11
+ ### Fixed
12
+
13
+ - Move repository-relative path validation from tool-schema regex lookaheads to runtime checks for better provider compatibility in `/simplify` and `/code-smell`.
14
+ - Reject absolute paths and parent-directory traversal using either forward slashes or backslashes.
15
+
16
+ ## [0.1.2] - 2026-07-26
17
+
18
+ ### Changed
19
+
20
+ - Move package development and trusted publishing to the `geminixiang/pi-stuff` monorepo.
21
+ - Declare Pi runtime packages as peer dependencies to follow Pi package conventions.
22
+
23
+ ## [0.1.1] - 2026-06-09
24
+
25
+ ### Added
26
+
27
+ - Add `/simplify previous` mode to review only the last commit (`HEAD~1..HEAD`), with aliases for `previous-commit`, `prev`, and `last-commit`.
28
+
9
29
  ## [0.1.0] - 2026-06-03
10
30
 
11
31
  ### Added
package/README.md CHANGED
@@ -58,7 +58,7 @@ Scans specific paths directly.
58
58
  /simplify
59
59
  ```
60
60
 
61
- Opens a preset selector. Choose uncommitted changes or a folder snapshot.
61
+ Opens a preset selector. Choose uncommitted changes, the previous commit, or a folder snapshot.
62
62
 
63
63
  Uncommitted mode analyzes all git changes and presents cleanup candidates:
64
64
 
@@ -66,6 +66,16 @@ Uncommitted mode analyzes all git changes and presents cleanup candidates:
66
66
  - **Confirm** (yellow) - delete after user confirms
67
67
  - **Review** (orange) - user should review first
68
68
 
69
+ ### Simplify Previous Commit
70
+
71
+ ```
72
+ /simplify previous
73
+ ```
74
+
75
+ Analyzes only the last commit (`git diff HEAD~1..HEAD`), even if there are uncommitted local changes.
76
+
77
+ Aliases: `/simplify previous-commit`, `/simplify prev`, `/simplify last-commit`.
78
+
69
79
  ### Simplify Folders
70
80
 
71
81
  ```
@@ -83,12 +93,13 @@ Analyzes the specified folders/files as a snapshot (not a diff), even when there
83
93
 
84
94
  ## Commands
85
95
 
86
- | Command | Purpose |
87
- | --- | --- |
88
- | `/simplify` | Delete excess after a feature: dead code, debug remnants, thin wrappers |
89
- | `/simplify folder <paths>` | Same as above but snapshot mode (no git diff needed) |
90
- | `/code-smell` | Find structural problems: coupling, complexity, error handling, performance |
91
- | `/code-smell <paths>` | Scan specific paths for code smells |
96
+ | Command | Purpose |
97
+ | -------------------------- | --------------------------------------------------------------------------- |
98
+ | `/simplify` | Delete excess after a feature: dead code, debug remnants, thin wrappers |
99
+ | `/simplify previous` | Same as above but scoped to the previous commit (`HEAD~1..HEAD`) |
100
+ | `/simplify folder <paths>` | Same as above but snapshot mode (no git diff needed) |
101
+ | `/code-smell` | Find structural problems: coupling, complexity, error handling, performance |
102
+ | `/code-smell <paths>` | Scan specific paths for code smells |
92
103
 
93
104
  ## Comparison with pi-review
94
105
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminixiang/pi-simplify",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Simplify: Review changed code for reuse, quality, and efficiency",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -9,24 +9,20 @@
9
9
  "author": "geminixiang",
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "https://github.com/geminixiang/pi-simplify"
12
+ "url": "https://github.com/geminixiang/pi-stuff.git",
13
+ "directory": "packages/pi-simplify"
13
14
  },
14
15
  "main": "index.ts",
15
- "scripts": {
16
- "lint": "oxlint",
17
- "lint:fix": "oxlint --fix",
18
- "fmt": "oxfmt",
19
- "fmt:check": "oxfmt --check"
20
- },
21
- "dependencies": {
22
- "@earendil-works/pi-coding-agent": ">=0.74.0",
23
- "@earendil-works/pi-tui": ">=0.74.0",
24
- "typebox": "^1.1.38"
25
- },
26
16
  "devDependencies": {
27
- "oxfmt": "^0.45.0",
28
- "oxlint": "^1.60.0",
29
- "typescript": "^6.0.2"
17
+ "@earendil-works/pi-coding-agent": "^0.82.1",
18
+ "@earendil-works/pi-tui": "^0.82.1",
19
+ "typebox": "^1.1.38",
20
+ "typescript": "7.0.2"
21
+ },
22
+ "peerDependencies": {
23
+ "@earendil-works/pi-coding-agent": "*",
24
+ "@earendil-works/pi-tui": "*",
25
+ "typebox": "*"
30
26
  },
31
27
  "pi": {
32
28
  "extensions": [
@@ -9,6 +9,7 @@ import {
9
9
  import { Container, SelectList, Text, type SelectItem } from "@earendil-works/pi-tui";
10
10
  import { Type } from "typebox";
11
11
 
12
+ import { assertRepositoryRelativePaths } from "../repository-path.js";
12
13
  import { runProgrammaticChecks } from "./checks.js";
13
14
  import { CODE_SMELL_PROMPT } from "./prompt.js";
14
15
  import { showFindingSelector } from "./selector.js";
@@ -164,7 +165,10 @@ function registerFindingsTool(
164
165
  category: categorySchema,
165
166
  severity: severitySchema,
166
167
  confidence: confidenceSchema,
167
- file: Type.String({ minLength: 1, pattern: "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$" }),
168
+ file: Type.String({
169
+ description: "Repository-relative path without '..' segments",
170
+ minLength: 1,
171
+ }),
168
172
  lines: Type.String(),
169
173
  smell: Type.String({ minLength: 1 }),
170
174
  evidence: Type.String({ minLength: 1 }),
@@ -176,6 +180,8 @@ function registerFindingsTool(
176
180
  }),
177
181
  async execute(_toolCallId, params) {
178
182
  const findings: CodeSmellFinding[] = params.findings;
183
+ assertRepositoryRelativePaths(findings.map((finding) => finding.file));
184
+
179
185
  const resolve = state.getResolver();
180
186
  if (!resolve) {
181
187
  return {
@@ -0,0 +1,19 @@
1
+ export function isRepositoryRelativePath(file: string): boolean {
2
+ const normalized = file.replaceAll("\\", "/");
3
+
4
+ return (
5
+ file.length > 0 &&
6
+ !normalized.startsWith("/") &&
7
+ !/^[A-Za-z]:\//.test(normalized) &&
8
+ !normalized.split("/").includes("..")
9
+ );
10
+ }
11
+
12
+ export function assertRepositoryRelativePaths(files: string[]): void {
13
+ const invalidFiles = files.filter((file) => !isRepositoryRelativePath(file));
14
+ if (invalidFiles.length === 0) return;
15
+
16
+ throw new Error(
17
+ `Expected repository-relative file paths without '..' segments; received: ${invalidFiles.join(", ")}`,
18
+ );
19
+ }
@@ -8,6 +8,7 @@ import {
8
8
  import { Container, SelectList, Text, type SelectItem } from "@earendil-works/pi-tui";
9
9
  import { Type } from "typebox";
10
10
 
11
+ import { assertRepositoryRelativePaths } from "../repository-path.js";
11
12
  import { SIMPLIFY_PROMPT } from "./prompt.js";
12
13
  import { showCandidateSelector } from "./selector.js";
13
14
  import type { SimplifyResult } from "./types.js";
@@ -29,7 +30,10 @@ const actionSchema = Type.Union([
29
30
  Type.Literal("parallelize"),
30
31
  ]);
31
32
 
32
- type SimplifyTarget = { type: "uncommitted" } | { type: "folder"; paths: string[] };
33
+ type SimplifyTarget =
34
+ | { type: "uncommitted" }
35
+ | { type: "previous-commit" }
36
+ | { type: "folder"; paths: string[] };
33
37
  type ParsedArgs = {
34
38
  target: SimplifyTarget | null;
35
39
  additionalFocus?: string;
@@ -38,6 +42,11 @@ type ParsedArgs = {
38
42
 
39
43
  const SIMPLIFY_PRESETS = [
40
44
  { value: "uncommitted", label: "Simplify uncommitted changes", description: "" },
45
+ {
46
+ value: "previous-commit",
47
+ label: "Simplify previous commit",
48
+ description: "(diff: HEAD~1..HEAD)",
49
+ },
41
50
  { value: "folder", label: "Simplify a folder (or more)", description: "(snapshot, not diff)" },
42
51
  ] as const;
43
52
 
@@ -55,14 +64,25 @@ function parseArgs(args: string): ParsedArgs {
55
64
  if (!trimmed) return { target: null };
56
65
 
57
66
  const [subcommand, ...rest] = trimmed.split(/\s+/);
58
- if (subcommand?.toLowerCase() !== "folder") {
59
- return { target: { type: "uncommitted" }, additionalFocus: trimmed };
67
+ const normalizedSubcommand = subcommand?.toLowerCase();
68
+
69
+ if (normalizedSubcommand === "folder") {
70
+ const paths = parseSimplifyPaths(rest.join(" "));
71
+ if (paths.length === 0)
72
+ return { target: null, error: "Usage: /simplify folder <path> [path...]" };
73
+ return { target: { type: "folder", paths } };
74
+ }
75
+
76
+ if (
77
+ normalizedSubcommand === "previous" ||
78
+ normalizedSubcommand === "previous-commit" ||
79
+ normalizedSubcommand === "prev" ||
80
+ normalizedSubcommand === "last-commit"
81
+ ) {
82
+ return { target: { type: "previous-commit" }, additionalFocus: rest.join(" ") || undefined };
60
83
  }
61
84
 
62
- const paths = parseSimplifyPaths(rest.join(" "));
63
- if (paths.length === 0)
64
- return { target: null, error: "Usage: /simplify folder <path> [path...]" };
65
- return { target: { type: "folder", paths } };
85
+ return { target: { type: "uncommitted" }, additionalFocus: trimmed };
66
86
  }
67
87
 
68
88
  async function hasUncommittedChanges(pi: ExtensionAPI): Promise<boolean> {
@@ -70,6 +90,11 @@ async function hasUncommittedChanges(pi: ExtensionAPI): Promise<boolean> {
70
90
  return code === 0 && stdout.trim().length > 0;
71
91
  }
72
92
 
93
+ async function hasPreviousCommit(pi: ExtensionAPI): Promise<boolean> {
94
+ const { code } = await pi.exec("git", ["rev-parse", "--verify", "HEAD~1"]);
95
+ return code === 0;
96
+ }
97
+
73
98
  function buildSimplifyPrompt(target: SimplifyTarget, additionalFocus?: string): string {
74
99
  let fullPrompt = SIMPLIFY_PROMPT;
75
100
 
@@ -77,6 +102,11 @@ function buildSimplifyPrompt(target: SimplifyTarget, additionalFocus?: string):
77
102
  fullPrompt += `\n\n## Scope Override: Folder Snapshot\n\nReview only the code in these repository-relative paths: ${target.paths.join(", ")}\n\nThis is a snapshot review, not a git diff review. Read files directly under these paths instead of relying on \`git diff\`. Only return candidates whose \`file\` is inside one of these paths.`;
78
103
  }
79
104
 
105
+ if (target.type === "previous-commit") {
106
+ fullPrompt +=
107
+ "\n\n## Scope Override: Previous Commit\n\nReview the previous commit only. Run `git diff HEAD~1..HEAD` to see what changed in the last commit, and treat that diff as the review scope. Do not use uncommitted changes as the analysis target. Only return candidates caused by the previous commit's diff.";
108
+ }
109
+
80
110
  if (additionalFocus?.trim()) fullPrompt += `\n\n## Additional Focus\n\n${additionalFocus.trim()}`;
81
111
  return fullPrompt;
82
112
  }
@@ -95,9 +125,8 @@ function registerSimplifyCandidatesTool(
95
125
  category: categorySchema,
96
126
  risk: riskSchema,
97
127
  file: Type.String({
98
- description: "Repository-relative path",
128
+ description: "Repository-relative path without '..' segments",
99
129
  minLength: 1,
100
- pattern: "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$",
101
130
  }),
102
131
  lines: Type.String({ description: "Line number or range, or empty string if unknown" }),
103
132
  rootIssue: Type.String({
@@ -133,6 +162,8 @@ function registerSimplifyCandidatesTool(
133
162
  parameters,
134
163
  async execute(_toolCallId, params) {
135
164
  const candidates: SimplifyResult[] = params.candidates;
165
+ assertRepositoryRelativePaths(candidates.map((candidate) => candidate.file));
166
+
136
167
  const resolve = state.getResolver();
137
168
  if (!resolve) {
138
169
  return {
@@ -243,6 +274,7 @@ async function showSimplifyTargetSelector(
243
274
 
244
275
  if (!result) return null;
245
276
  if (result === "uncommitted") return { type: "uncommitted" };
277
+ if (result === "previous-commit") return { type: "previous-commit" };
246
278
 
247
279
  const target = await showFolderInput(ctx);
248
280
  if (target) return target;
@@ -354,8 +386,20 @@ export function registerSimplifyWorkflow(pi: ExtensionAPI) {
354
386
  return;
355
387
  }
356
388
 
389
+ if (target.type === "previous-commit" && !(await hasPreviousCommit(pi))) {
390
+ ctx.ui.notify(
391
+ "No previous commit found. Need at least two commits to use this mode.",
392
+ "info",
393
+ );
394
+ return;
395
+ }
396
+
357
397
  const targetHint =
358
- target.type === "folder" ? `folders: ${target.paths.join(", ")}` : "uncommitted changes";
398
+ target.type === "folder"
399
+ ? `folders: ${target.paths.join(", ")}`
400
+ : target.type === "previous-commit"
401
+ ? "previous commit"
402
+ : "uncommitted changes";
359
403
  ctx.ui.notify(`Analyzing ${targetHint} for review findings...`, "info");
360
404
 
361
405
  const fullPrompt = buildSimplifyPrompt(target, parsed.additionalFocus);
@@ -1,56 +0,0 @@
1
- name: CI / Publish to npm
2
-
3
- on:
4
- release:
5
- types: [published]
6
- push:
7
- branches: [main]
8
- workflow_dispatch:
9
-
10
- permissions:
11
- id-token: write
12
- contents: read
13
-
14
- jobs:
15
- ci:
16
- runs-on: ubuntu-latest
17
- steps:
18
- - uses: actions/checkout@v4
19
-
20
- - uses: actions/setup-node@v4
21
- with:
22
- node-version: "24"
23
- registry-url: "https://registry.npmjs.org"
24
-
25
- - name: Install dependencies
26
- run: npm ci
27
-
28
- - name: Type check
29
- run: npx tsc --noEmit --skipLibCheck || echo "No TypeScript, skipping type check"
30
-
31
- publish:
32
- needs: ci
33
- runs-on: ubuntu-latest
34
- if: github.event_name == 'release'
35
- steps:
36
- - uses: actions/checkout@v4
37
-
38
- - uses: actions/setup-node@v4
39
- with:
40
- node-version: "24"
41
- registry-url: "https://registry.npmjs.org"
42
-
43
- - name: Update npm
44
- run: npm install -g npm@latest
45
-
46
- - name: Install dependencies
47
- run: npm ci
48
-
49
- - name: Publish to npm
50
- run: |
51
- VERSION=$(node -p "require('./package.json').version")
52
- if [[ "$VERSION" == *-* ]]; then
53
- npm publish --provenance --access public --tag beta
54
- else
55
- npm publish --provenance --access public
56
- fi
@@ -1,146 +0,0 @@
1
- ---
2
- name: release
3
- description: Prepare and publish a new pi-simplify release. Use when bumping the version, committing and pushing it, and creating a GitHub release in the style of earlier pi-simplify releases.
4
- ---
5
-
6
- # pi-simplify Release
7
-
8
- Repo defaults:
9
-
10
- - branch: `main`
11
- - remote: `origin`
12
- - repo: `geminixiang/pi-simplify`
13
- - npm package: `@geminixiang/pi-simplify`
14
- - use `package.json` version for npm/package files, and `v<version>` for the git tag and GitHub release title
15
- - versions with `-alpha.`, `-beta.`, or `-rc.` are prereleases by default
16
-
17
- ## Version rules
18
-
19
- Examples:
20
-
21
- - stable package version: `0.2.1`, `0.3.0`, `1.0.0` → release tag/title: `v0.2.1`, `v0.3.0`, `v1.0.0`
22
- - prerelease package version: `0.2.0-beta.8`, `0.3.0-rc.1`, `1.0.0-alpha.2` → release tag/title: `v0.2.0-beta.8`, `v0.3.0-rc.1`, `v1.0.0-alpha.2`
23
-
24
- Guidance:
25
-
26
- - `patch` = bugfix / small maintenance
27
- - `minor` = backward-compatible features
28
- - `major` = breaking changes
29
- - prerelease numbers increase within the same line, e.g. `beta.8 -> beta.9`
30
- - promote prerelease to stable by dropping the suffix, e.g. `0.2.0-beta.8 -> 0.2.0`
31
-
32
- ## Flow
33
-
34
- ### 1. Check state
35
-
36
- ```bash
37
- git status --short
38
- git branch --show-current
39
- git remote -v
40
- git tag --list | tail -20
41
- ```
42
-
43
- Read `package.json` and `package-lock.json`. If unrelated files are modified, ask before committing.
44
-
45
- ### 2. Sync version files
46
-
47
- Preferred:
48
-
49
- ```bash
50
- npm version <version> --no-git-tag-version
51
- ```
52
-
53
- Use this to sync `package.json` and `package-lock.json` without creating an automatic commit or tag. If the user already edited `package.json`, just verify `package-lock.json` matches.
54
-
55
- ### 3. Update CHANGELOG
56
-
57
- `CHANGELOG.md` follows Keep a Changelog with `### Added / Changed / Fixed / Removed / Security / Performance / Tests` subsections. The newest release sits at the top under an `## [Unreleased]` placeholder.
58
-
59
- Gather user-visible changes since the previous tag:
60
-
61
- ```bash
62
- git log --pretty=format:'%h %s' <previous-tag>..HEAD | grep -v "^[a-f0-9]* chore: bump version"
63
- ```
64
-
65
- Then in `CHANGELOG.md`:
66
-
67
- - Keep `## [Unreleased]` at the top as an empty placeholder.
68
- - Insert a new section `## [<version>] - <YYYY-MM-DD>` (use today's date for stable, omit the date for prereleases to match the existing style).
69
- - Group entries by subsection; one bullet per user-visible change, imperative voice, no commit hashes.
70
- - Skip pure internal refactors that don't change behavior unless they affect contributors (then list under `### Changed`).
71
-
72
- If you generated draft release notes in step 5 first, copy the same wording into CHANGELOG — they should match.
73
-
74
- ### 4. Commit and push
75
-
76
- Stage version files and CHANGELOG together — the bump and the changelog entry belong in the same commit.
77
-
78
- ```bash
79
- git add package.json package-lock.json CHANGELOG.md
80
- git commit -m "chore: bump version to <version>"
81
- git push origin main
82
- ```
83
-
84
- ### 5. Draft release notes
85
-
86
- Use the previous GitHub release as the style reference.
87
-
88
- ```bash
89
- gh release list --repo geminixiang/pi-simplify --limit 10
90
- gh release view <previous-tag> --repo geminixiang/pi-simplify --json tagName,name,body,url,publishedAt
91
- git log --pretty=format:'%h %s' <previous-tag>..HEAD
92
- git diff --stat <previous-tag>..HEAD
93
- ```
94
-
95
- Write concise notes focused on user-visible changes, usually with:
96
-
97
- - `## What's changed`
98
- - `### Highlights`
99
- - `### Notable changes`
100
- - `### Docs and maintenance`
101
- - `### Verification`
102
-
103
- Write notes to `/tmp/pi-simplify-release-<version>.md`. Keep them consistent with the CHANGELOG entry from step 3. Use `v<previous-version>...v<version>` in compare links.
104
-
105
- ### 6. Create or update release
106
-
107
- Prerelease:
108
-
109
- ```bash
110
- gh release create v<version> \
111
- --repo geminixiang/pi-simplify \
112
- --target main \
113
- --title v<version> \
114
- --notes-file /tmp/pi-simplify-release-<version>.md \
115
- --prerelease
116
- ```
117
-
118
- Stable:
119
-
120
- ```bash
121
- gh release create v<version> \
122
- --repo geminixiang/pi-simplify \
123
- --target main \
124
- --title v<version> \
125
- --notes-file /tmp/pi-simplify-release-<version>.md
126
- ```
127
-
128
- If it already exists, use `gh release edit v<version> ...` and keep prerelease/stable intent consistent.
129
-
130
- ## Report back
131
-
132
- Return:
133
-
134
- - released version
135
- - stable or prerelease
136
- - version-bump commit hash
137
- - push status
138
- - release URL
139
-
140
- ## Guardrails
141
-
142
- - Always use `geminixiang/pi-simplify`.
143
- - Infer stable vs prerelease from the version, or ask.
144
- - Do not include raw commit hashes in release notes unless requested.
145
- - If hooks fail during commit, fix or report before retrying.
146
- - Never publish a release without a corresponding CHANGELOG entry — the version bump commit must include the new CHANGELOG section.