@geminixiang/pi-simplify 0.0.2 → 0.0.4
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/.github/workflows/publish.yml +2 -2
- package/README.md +70 -0
- package/index.ts +721 -64
- package/package.json +32 -21
- package/tsconfig.json +11 -0
|
@@ -19,7 +19,7 @@ jobs:
|
|
|
19
19
|
|
|
20
20
|
- uses: actions/setup-node@v4
|
|
21
21
|
with:
|
|
22
|
-
node-version: "
|
|
22
|
+
node-version: "24"
|
|
23
23
|
registry-url: "https://registry.npmjs.org"
|
|
24
24
|
|
|
25
25
|
- name: Install dependencies
|
|
@@ -37,7 +37,7 @@ jobs:
|
|
|
37
37
|
|
|
38
38
|
- uses: actions/setup-node@v4
|
|
39
39
|
with:
|
|
40
|
-
node-version: "
|
|
40
|
+
node-version: "24"
|
|
41
41
|
registry-url: "https://registry.npmjs.org"
|
|
42
42
|
|
|
43
43
|
- name: Update npm
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# pi-simplify
|
|
2
|
+
|
|
3
|
+
A [pi coding agent](https://github.com/mariozechner/pi) extension that cleans up leftover code after feature implementation.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
After implementing a feature, your code often accumulates:
|
|
8
|
+
|
|
9
|
+
- **Dead code** - unused exports, orphaned files, zombie variables
|
|
10
|
+
- **Debug remnants** - console.log, debugger statements, temp flags
|
|
11
|
+
- **Commented-out code** - old logic left in comments
|
|
12
|
+
- **Over-engineering** - "might use later" abstractions never used
|
|
13
|
+
- **Duplicate logic** - repeated if-else blocks doing the same thing
|
|
14
|
+
|
|
15
|
+
`/simplify` finds these and removes them.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pi install npm:@geminixiang/pi-simplify
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Full Simplify
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
/simplify
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Analyzes all git changes and presents cleanup candidates:
|
|
32
|
+
|
|
33
|
+
- **Safe** (green) - auto-selected, will be deleted
|
|
34
|
+
- **Confirm** (yellow) - delete after user confirms
|
|
35
|
+
- **Review** (orange) - user should review first
|
|
36
|
+
|
|
37
|
+
### Quick Simplify
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
/simplify-quick
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Only removes the obviously safe stuff:
|
|
44
|
+
|
|
45
|
+
- `console.log` / `console.warn` / `console.error`
|
|
46
|
+
- `debugger` statements
|
|
47
|
+
- Unused imports
|
|
48
|
+
- Empty catch blocks
|
|
49
|
+
|
|
50
|
+
No confirmation needed - just does it.
|
|
51
|
+
|
|
52
|
+
### With Focus
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
/simplify focus on the utils folder
|
|
56
|
+
/simplify focus on removing debug code
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Comparison with pi-review
|
|
60
|
+
|
|
61
|
+
| | pi-review | pi-simplify |
|
|
62
|
+
| ------------ | --------------------------- | --------------------- |
|
|
63
|
+
| **Goal** | Find problems | Delete excess |
|
|
64
|
+
| **Attitude** | Conservative (marks issues) | Active (removes junk) |
|
|
65
|
+
| **Output** | Findings list | Deletion plan |
|
|
66
|
+
| **Trigger** | Manual review | Post-feature cleanup |
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
package/index.ts
CHANGED
|
@@ -1,89 +1,746 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-simplify: Code Cleanup Extension for pi
|
|
3
|
+
*
|
|
4
|
+
* Removes leftover code after feature implementation:
|
|
5
|
+
* - Dead code (unused exports, orphaned files)
|
|
6
|
+
* - Debug remnants (console.log, debugger, temp flags)
|
|
7
|
+
* - Commented-out code
|
|
8
|
+
* - Over-engineering ("might use later" abstractions)
|
|
9
|
+
* - Duplicate logic
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { writeFileSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
1
16
|
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
17
|
+
import { matchesKey, Key, truncateToWidth, type SelectItem } from "@mariozechner/pi-tui";
|
|
18
|
+
|
|
19
|
+
type SimplifyResult = {
|
|
20
|
+
file: string;
|
|
21
|
+
lines: string;
|
|
22
|
+
reason: string;
|
|
23
|
+
risk: "safe" | "confirm" | "review";
|
|
24
|
+
};
|
|
2
25
|
|
|
3
|
-
const
|
|
4
|
-
const DELIVERY_FOLLOW_UP = "followUp";
|
|
5
|
-
const NOTIFY_REVIEW_STARTED = "Simplify review started";
|
|
6
|
-
const NOTIFY_REVIEW_QUEUED = "Simplify review queued as a follow-up";
|
|
26
|
+
const SIMPLIFY_PROMPT = `# Simplify: Clean Up Leftover Code
|
|
7
27
|
|
|
8
|
-
|
|
28
|
+
You are a code cleanup assistant. Your job is to identify and remove unnecessary code left over after feature implementation.
|
|
9
29
|
|
|
10
|
-
|
|
30
|
+
## What to Find
|
|
11
31
|
|
|
12
|
-
|
|
32
|
+
### 1. Dead Code (Safe to Delete)
|
|
33
|
+
- **Unused exports**: Functions/classes defined but never imported elsewhere
|
|
34
|
+
- **Orphan files**: Files created but never referenced
|
|
35
|
+
- **Zombie variables**: Variables assigned but never used
|
|
36
|
+
- **Empty blocks**: try/catch/if blocks with no logic
|
|
13
37
|
|
|
14
|
-
|
|
38
|
+
### 2. Debug Remnants (Safe to Delete)
|
|
39
|
+
- console.log, console.warn, console.error statements
|
|
40
|
+
- debugger statements
|
|
41
|
+
- Temporary feature flags (e.g., \`ENABLE_DEBUG\`, \`DEBUG_MODE\`)
|
|
42
|
+
- Temporary todo comments that are now implemented
|
|
15
43
|
|
|
16
|
-
|
|
44
|
+
### 3. Commented-out Code (Review Before Deleting)
|
|
45
|
+
- Old logic left in comments
|
|
46
|
+
- Disabled features (commented out rather than deleted)
|
|
47
|
+
- Copy-pasted templates never customized
|
|
17
48
|
|
|
18
|
-
|
|
49
|
+
### 4. Over-engineering (Confirm Before Deleting)
|
|
50
|
+
- Abstractions created "for future use" but never used
|
|
51
|
+
- Helper functions with single call sites (could be inlined)
|
|
52
|
+
- Layers of indirection that add no value
|
|
19
53
|
|
|
20
|
-
###
|
|
54
|
+
### 5. Duplicate Logic (Confirm Before Deleting)
|
|
55
|
+
- Repeated if-else blocks doing the same thing
|
|
56
|
+
- Copy-paste code with minor variations
|
|
57
|
+
- Duplicate utility functions
|
|
21
58
|
|
|
22
|
-
|
|
59
|
+
## How to Analyze
|
|
23
60
|
|
|
24
|
-
1.
|
|
25
|
-
2.
|
|
26
|
-
|
|
61
|
+
1. Run \`git diff\` to see what changed
|
|
62
|
+
2. For each change, determine if it's:
|
|
63
|
+
- **Essential**: Required for the feature to work
|
|
64
|
+
- **Residual**: Left over from development/debugging
|
|
65
|
+
- **Legacy**: Old code not touched by this change
|
|
27
66
|
|
|
28
|
-
|
|
67
|
+
3. For each residual item:
|
|
68
|
+
- Identify exact file and line(s)
|
|
69
|
+
- Assess risk level
|
|
70
|
+
- Provide clear reason for removal
|
|
29
71
|
|
|
30
|
-
|
|
72
|
+
## Risk Levels
|
|
31
73
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
4. **Leaky abstractions**: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries
|
|
36
|
-
5. **Stringly-typed code**: using raw strings where constants, enums (string unions), or branded types already exist in the codebase
|
|
37
|
-
6. **Unnecessary JSX nesting**: wrapper Boxes/elements that add no layout value — check if inner component props (flexShrink, alignItems, etc.) already provide the needed behavior
|
|
38
|
-
7. **Unnecessary comments**: comments explaining WHAT the code does (well-named identifiers already do that), narrating the change, or referencing the task/caller — delete; keep only non-obvious WHY (hidden constraints, subtle invariants, workarounds)
|
|
74
|
+
- **safe**: Definitely can be deleted (unused, debug code)
|
|
75
|
+
- **confirm**: Delete after user confirms (over-engineered, duplicates)
|
|
76
|
+
- **review**: User should review before action (commented code, ambiguous)
|
|
39
77
|
|
|
40
|
-
|
|
78
|
+
## Rules
|
|
41
79
|
|
|
42
|
-
|
|
80
|
+
1. When in doubt, mark as "confirm" or "review" - don't delete without consent
|
|
81
|
+
2. For inline candidates, show the alternative
|
|
82
|
+
3. Don't flag necessary code just because it's simple
|
|
83
|
+
4. Respect existing abstraction boundaries
|
|
84
|
+
5. Be especially careful with:
|
|
85
|
+
- Error handling code
|
|
86
|
+
- Security-related logic
|
|
87
|
+
- Code that looks "unused" but is called via reflection/eval
|
|
88
|
+
- Database migration files
|
|
43
89
|
|
|
44
|
-
|
|
45
|
-
2. **Missed concurrency**: independent operations run sequentially when they could run in parallel
|
|
46
|
-
3. **Hot-path bloat**: new blocking work added to startup or per-request/per-render hot paths
|
|
47
|
-
4. **Recurring no-op updates**: state/store updates inside polling loops, intervals, or event handlers that fire unconditionally — add a change-detection guard so downstream consumers aren't notified when nothing changed. Also: if a wrapper function takes an updater/reducer callback, verify it honors same-reference returns (or whatever the "no change" signal is) — otherwise callers' early-return no-ops are silently defeated
|
|
48
|
-
5. **Unnecessary existence checks**: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error
|
|
49
|
-
6. **Memory**: unbounded data structures, missing cleanup, event listener leaks
|
|
50
|
-
7. **Overly broad operations**: reading entire files when only a portion is needed, loading all items when filtering for one
|
|
90
|
+
## Output Format (REQUIRED)
|
|
51
91
|
|
|
52
|
-
|
|
92
|
+
You may write prose analysis first, but you MUST end your response with a single fenced JSON block containing ALL candidates. This JSON is parsed programmatically — it must be valid JSON and use this exact shape:
|
|
93
|
+
|
|
94
|
+
\`\`\`json
|
|
95
|
+
{
|
|
96
|
+
"candidates": [
|
|
97
|
+
{
|
|
98
|
+
"risk": "safe",
|
|
99
|
+
"file": "path/to/file.ext",
|
|
100
|
+
"lines": "12-15",
|
|
101
|
+
"type": "debug",
|
|
102
|
+
"reason": "Why this should be removed",
|
|
103
|
+
"action": "delete"
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
\`\`\`
|
|
53
108
|
|
|
54
|
-
|
|
109
|
+
Field notes:
|
|
110
|
+
- \`risk\` — one of: "safe", "confirm", "review"
|
|
111
|
+
- \`file\` — repository-relative path, no backticks, no markdown
|
|
112
|
+
- \`lines\` — line number or range ("42" or "42-57"); empty string if unknown
|
|
113
|
+
- \`type\` — one of: "dead-code", "debug", "commented", "over-eng", "duplicate"
|
|
114
|
+
- \`reason\` — single plain-text sentence
|
|
115
|
+
- \`action\` — one of: "delete", "inline", "confirm"
|
|
55
116
|
|
|
56
|
-
|
|
117
|
+
If there are no candidates, output \`{"candidates": []}\`. Do NOT output anything after the closing \`\`\` fence.
|
|
57
118
|
`;
|
|
58
119
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
120
|
+
export default function simplifyExtension(pi: ExtensionAPI) {
|
|
121
|
+
// Accumulate assistant text across the whole turn (multiple messages when
|
|
122
|
+
// the AI makes tool calls). Overwriting would drop the JSON block if it
|
|
123
|
+
// appears in an earlier message and a later one adds a short confirmation.
|
|
124
|
+
let assistantTextBuffer = "";
|
|
125
|
+
pi.on("message_end", (event) => {
|
|
126
|
+
const msg = event.message;
|
|
127
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.content)) return;
|
|
128
|
+
const text = (msg.content as { type: string; text?: string }[])
|
|
129
|
+
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
130
|
+
.map((b) => b.text as string)
|
|
131
|
+
.join("\n");
|
|
132
|
+
if (text) assistantTextBuffer += (assistantTextBuffer ? "\n" : "") + text;
|
|
133
|
+
});
|
|
64
134
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
135
|
+
const RISK_VALUES = new Set(["safe", "confirm", "review"]);
|
|
136
|
+
|
|
137
|
+
function normalizeRisk(value: unknown): "safe" | "confirm" | "review" {
|
|
138
|
+
const v = String(value ?? "")
|
|
139
|
+
.toLowerCase()
|
|
140
|
+
.trim();
|
|
141
|
+
return RISK_VALUES.has(v) ? (v as "safe" | "confirm" | "review") : "review";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function stripMarkdown(text: string): string {
|
|
145
|
+
return text.replace(/[`*_]/g, "").trim();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Try to parse a JSON candidates block. The prompt requires this format,
|
|
150
|
+
* but we tolerate the AI wrapping it in ```json ... ``` or ``` ... ```,
|
|
151
|
+
* and also handle the case where it embeds the JSON inline without fences.
|
|
152
|
+
* Returns null only when no valid JSON with a `candidates` array is found —
|
|
153
|
+
* a valid empty list returns [] (which means "the AI found nothing").
|
|
154
|
+
*/
|
|
155
|
+
function parseCandidatesJSON(response: string): SimplifyResult[] | null {
|
|
156
|
+
const fenceRe = /```(?:json)?\s*\n([\s\S]*?)\n```/gi;
|
|
157
|
+
const blocks: string[] = [];
|
|
158
|
+
let m: RegExpExecArray | null;
|
|
159
|
+
while ((m = fenceRe.exec(response)) !== null) blocks.push(m[1]);
|
|
160
|
+
|
|
161
|
+
const bareMatch = response.match(/\{[\s\S]*"candidates"[\s\S]*\}/);
|
|
162
|
+
if (bareMatch) blocks.push(bareMatch[0]);
|
|
163
|
+
|
|
164
|
+
// Walk newest-first: the contract puts the JSON at the end of the response.
|
|
165
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(blocks[i]);
|
|
168
|
+
const list = parsed?.candidates;
|
|
169
|
+
if (!Array.isArray(list)) continue;
|
|
170
|
+
const out: SimplifyResult[] = [];
|
|
171
|
+
for (const item of list) {
|
|
172
|
+
if (!item || typeof item !== "object") continue;
|
|
173
|
+
const file = stripMarkdown(String(item.file ?? item.location ?? ""));
|
|
174
|
+
if (!file) continue;
|
|
175
|
+
const reason = stripMarkdown(String(item.reason ?? item.description ?? ""));
|
|
176
|
+
const action = item.action ? ` [${stripMarkdown(String(item.action))}]` : "";
|
|
177
|
+
out.push({
|
|
178
|
+
file,
|
|
179
|
+
lines: stripMarkdown(String(item.lines ?? "")),
|
|
180
|
+
reason: reason ? reason + action : action.trim() || "(no reason provided)",
|
|
181
|
+
risk: normalizeRisk(item.risk),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// A valid candidates array (even empty) is a successful parse
|
|
185
|
+
return out;
|
|
186
|
+
} catch {
|
|
187
|
+
// try next block
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Tolerant markdown parser — fallback for responses that don't emit the JSON block.
|
|
195
|
+
* Accepts many header/bullet variations that LLMs produce in practice.
|
|
196
|
+
*/
|
|
197
|
+
function parseCandidatesMarkdown(response: string): SimplifyResult[] {
|
|
198
|
+
const candidates: SimplifyResult[] = [];
|
|
199
|
+
const lines = response.split("\n");
|
|
200
|
+
|
|
201
|
+
// Match `[safe]` / `[confirm]` / `[review]` anywhere on a "header-ish" line.
|
|
202
|
+
// A line counts as a header if the risk tag is preceded only by markdown noise:
|
|
203
|
+
// whitespace, #, *, -, digits, dots, parens, colons, backticks, underscores.
|
|
204
|
+
const HEADER_PREFIX = /^[\s#*\-–—•▸→0-9.)\]:`_]*$/;
|
|
205
|
+
const RISK_TAG = /\[(safe|confirm|review)\]/i;
|
|
206
|
+
|
|
207
|
+
let current: (Partial<SimplifyResult> & { _reason?: string; _action?: string }) | null = null;
|
|
208
|
+
|
|
209
|
+
const commit = () => {
|
|
210
|
+
if (!current) return;
|
|
211
|
+
if (!current.risk || !current.file) {
|
|
212
|
+
current = null;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const reason = current._reason?.trim() || current.lines?.trim() || "(no reason provided)";
|
|
216
|
+
const action = current._action ? ` [${current._action.trim()}]` : "";
|
|
217
|
+
candidates.push({
|
|
218
|
+
file: current.file,
|
|
219
|
+
lines: current.lines || "",
|
|
220
|
+
reason: reason + action,
|
|
221
|
+
risk: current.risk as "safe" | "confirm" | "review",
|
|
222
|
+
});
|
|
223
|
+
current = null;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
for (const rawLine of lines) {
|
|
227
|
+
const line = rawLine;
|
|
228
|
+
const tagMatch = line.match(RISK_TAG);
|
|
229
|
+
if (tagMatch) {
|
|
230
|
+
const prefix = line.slice(0, line.indexOf(tagMatch[0]));
|
|
231
|
+
if (HEADER_PREFIX.test(prefix)) {
|
|
232
|
+
// New candidate — commit the previous one
|
|
233
|
+
commit();
|
|
234
|
+
const risk = normalizeRisk(tagMatch[1]);
|
|
235
|
+
// Everything after the tag on this line
|
|
236
|
+
const rest = stripMarkdown(
|
|
237
|
+
line.slice(line.indexOf(tagMatch[0]) + tagMatch[0].length).replace(/^[\s\-–—:]+/, ""),
|
|
238
|
+
);
|
|
239
|
+
// Try to extract a file path (with optional :line or :line-line)
|
|
240
|
+
const fileMatch = rest.match(/([^\s()`*,]+?\.[a-zA-Z0-9]+)(?::(\d+(?:-\d+)?))?/);
|
|
241
|
+
current = { risk };
|
|
242
|
+
if (fileMatch) {
|
|
243
|
+
current.file = fileMatch[1];
|
|
244
|
+
current.lines = fileMatch[2] || "";
|
|
245
|
+
// Remaining text becomes a description/reason fallback
|
|
246
|
+
const desc = rest
|
|
247
|
+
.replace(fileMatch[0], "")
|
|
248
|
+
.replace(/^[\s\-–—:]+/, "")
|
|
249
|
+
.trim();
|
|
250
|
+
if (desc) current.lines = current.lines ? `${current.lines}: ${desc}` : desc;
|
|
251
|
+
} else {
|
|
252
|
+
// No recognizable file on header — treat rest as description
|
|
253
|
+
current.lines = rest;
|
|
254
|
+
}
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!current) continue;
|
|
260
|
+
|
|
261
|
+
// Field extraction: tolerate `- Key:`, `* Key:`, `**Key:**`, or bare `Key:`.
|
|
262
|
+
const fieldMatch = line.match(
|
|
263
|
+
/^\s*(?:[-*•]\s*)?\*{0,2}(File|Location|Path|Reason|Why|Action|Lines?)\*{0,2}\s*:?\s*[:\-–—]?\s*(.+?)\s*$/i,
|
|
264
|
+
);
|
|
265
|
+
if (fieldMatch) {
|
|
266
|
+
const key = fieldMatch[1].toLowerCase();
|
|
267
|
+
const value = stripMarkdown(fieldMatch[2]);
|
|
268
|
+
if (key === "file" || key === "location" || key === "path") {
|
|
269
|
+
// Split `path.ts:12-15` into file + lines if current.file is still missing/weak
|
|
270
|
+
const withLine = value.match(/^(.+?)(?::(\d+(?:-\d+)?))?$/);
|
|
271
|
+
if (withLine) {
|
|
272
|
+
current.file = current.file || withLine[1];
|
|
273
|
+
if (withLine[2]) current.lines = withLine[2];
|
|
274
|
+
} else {
|
|
275
|
+
current.file = current.file || value;
|
|
276
|
+
}
|
|
277
|
+
} else if (key === "reason" || key === "why") {
|
|
278
|
+
current._reason = value;
|
|
279
|
+
} else if (key === "action") {
|
|
280
|
+
current._action = value;
|
|
281
|
+
} else if (key === "line" || key === "lines") {
|
|
282
|
+
current.lines = value;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
commit();
|
|
288
|
+
return candidates;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Write raw AI output to a debug file so the user can see what the model actually returned. */
|
|
292
|
+
function dumpDebug(response: string): string {
|
|
293
|
+
const path = join(tmpdir(), `pi-simplify-debug-${Date.now()}.md`);
|
|
294
|
+
try {
|
|
295
|
+
writeFileSync(path, response, "utf8");
|
|
296
|
+
return path;
|
|
297
|
+
} catch {
|
|
298
|
+
return "";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
type ParseOutcome = { kind: "ok"; candidates: SimplifyResult[] } | { kind: "unparseable" };
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Parse the AI's response to extract cleanup candidates.
|
|
306
|
+
* - "ok" with non-empty list → show selector
|
|
307
|
+
* - "ok" with empty list → AI legitimately found nothing
|
|
308
|
+
* - "unparseable" → AI output didn't match any known format; dump for debug
|
|
309
|
+
*/
|
|
310
|
+
function parseCandidates(response: string): ParseOutcome {
|
|
311
|
+
const json = parseCandidatesJSON(response);
|
|
312
|
+
if (json !== null) return { kind: "ok", candidates: json };
|
|
313
|
+
const md = parseCandidatesMarkdown(response);
|
|
314
|
+
if (md.length > 0) return { kind: "ok", candidates: md };
|
|
315
|
+
return { kind: "unparseable" };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Show selection dialog for candidates with proper multi-select support
|
|
320
|
+
*/
|
|
321
|
+
async function showCandidateSelector(
|
|
322
|
+
ctx: ExtensionCommandContext,
|
|
323
|
+
candidates: SimplifyResult[],
|
|
324
|
+
): Promise<SimplifyResult[]> {
|
|
325
|
+
if (candidates.length === 0) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Categorize by risk level for display
|
|
330
|
+
const safeCandidates = candidates.filter((c) => c.risk === "safe");
|
|
331
|
+
const confirmCandidates = candidates.filter((c) => c.risk === "confirm");
|
|
332
|
+
const reviewCandidates = candidates.filter((c) => c.risk === "review");
|
|
333
|
+
|
|
334
|
+
// Build display items with section headers
|
|
335
|
+
const displayItems: SelectItem[] = [];
|
|
336
|
+
const selectableItems: { index: number; candidate: SimplifyResult }[] = [];
|
|
337
|
+
let globalIndex = 0;
|
|
338
|
+
|
|
339
|
+
if (safeCandidates.length > 0) {
|
|
340
|
+
displayItems.push({
|
|
341
|
+
value: "__section__safe__",
|
|
342
|
+
label: `── Safe to delete (${safeCandidates.length}) ──`,
|
|
343
|
+
description: "",
|
|
344
|
+
});
|
|
345
|
+
globalIndex++;
|
|
346
|
+
for (const c of safeCandidates) {
|
|
347
|
+
displayItems.push({
|
|
348
|
+
value: JSON.stringify(c),
|
|
349
|
+
label: `${c.file} - ${c.reason}`,
|
|
350
|
+
description: "Will be deleted",
|
|
351
|
+
});
|
|
352
|
+
selectableItems.push({ index: globalIndex, candidate: c });
|
|
353
|
+
globalIndex++;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (confirmCandidates.length > 0) {
|
|
358
|
+
displayItems.push({
|
|
359
|
+
value: "__section__confirm__",
|
|
360
|
+
label: `── Needs confirmation (${confirmCandidates.length}) ──`,
|
|
361
|
+
description: "",
|
|
362
|
+
});
|
|
363
|
+
globalIndex++;
|
|
364
|
+
for (const c of confirmCandidates) {
|
|
365
|
+
displayItems.push({
|
|
366
|
+
value: JSON.stringify(c),
|
|
367
|
+
label: `${c.file} - ${c.reason}`,
|
|
368
|
+
description: "Select to delete",
|
|
369
|
+
});
|
|
370
|
+
selectableItems.push({ index: globalIndex, candidate: c });
|
|
371
|
+
globalIndex++;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (reviewCandidates.length > 0) {
|
|
376
|
+
displayItems.push({
|
|
377
|
+
value: "__section__review__",
|
|
378
|
+
label: `── Needs review (${reviewCandidates.length}) ──`,
|
|
379
|
+
description: "",
|
|
380
|
+
});
|
|
381
|
+
globalIndex++;
|
|
382
|
+
for (const c of reviewCandidates) {
|
|
383
|
+
displayItems.push({
|
|
384
|
+
value: JSON.stringify(c),
|
|
385
|
+
label: `${c.file} - ${c.reason}`,
|
|
386
|
+
description: "Review before deleting",
|
|
387
|
+
});
|
|
388
|
+
selectableItems.push({ index: globalIndex, candidate: c });
|
|
389
|
+
globalIndex++;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Track selected selectable items (by their selectableItems index)
|
|
394
|
+
const selectedSet = new Set<number>();
|
|
395
|
+
|
|
396
|
+
// Pre-select all safe items (indices 0 to safeCandidates.length - 1 in selectableItems)
|
|
397
|
+
for (let i = 0; i < safeCandidates.length; i++) {
|
|
398
|
+
selectedSet.add(i);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const maxVisible = 12;
|
|
402
|
+
let scrollOffset = 0;
|
|
403
|
+
let cursorPos = 0;
|
|
404
|
+
|
|
405
|
+
const result = await ctx.ui.custom<SimplifyResult[]>((tui, theme, _keybindings, done) => {
|
|
406
|
+
// Render the multi-select list manually
|
|
407
|
+
const renderList = (width: number): string[] => {
|
|
408
|
+
const lines: string[] = [];
|
|
409
|
+
|
|
410
|
+
// Calculate which items are visible
|
|
411
|
+
const visibleItems: { selectableIdx: number | null; item: SelectItem }[] = [];
|
|
412
|
+
for (let i = 0; i < displayItems.length; i++) {
|
|
413
|
+
const item = displayItems[i];
|
|
414
|
+
if (item.value.startsWith("__section__")) {
|
|
415
|
+
visibleItems.push({ selectableIdx: null, item });
|
|
416
|
+
} else {
|
|
417
|
+
const selectableInfo = selectableItems.find((si) => si.index === i);
|
|
418
|
+
if (selectableInfo) {
|
|
419
|
+
const selectableIdx = selectableItems.indexOf(selectableInfo);
|
|
420
|
+
visibleItems.push({ selectableIdx, item });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Show visible range with scroll
|
|
426
|
+
const startIdx = Math.max(0, scrollOffset);
|
|
427
|
+
const endIdx = Math.min(visibleItems.length, startIdx + maxVisible);
|
|
428
|
+
|
|
429
|
+
for (let vIdx = startIdx; vIdx < endIdx; vIdx++) {
|
|
430
|
+
const { selectableIdx, item } = visibleItems[vIdx];
|
|
431
|
+
|
|
432
|
+
if (item.value.startsWith("__section__")) {
|
|
433
|
+
// Section header
|
|
434
|
+
lines.push(theme.fg("accent", item.label));
|
|
435
|
+
} else {
|
|
436
|
+
// Selectable item
|
|
437
|
+
const isCursor = selectableIdx === cursorPos;
|
|
438
|
+
const isSelected = selectableIdx !== null && selectedSet.has(selectableIdx);
|
|
439
|
+
const prefix = isCursor ? theme.fg("accent", "> ") : " ";
|
|
440
|
+
const checkbox = isSelected ? theme.fg("success", "[x]") : theme.fg("dim", "[ ]");
|
|
441
|
+
const truncatedLabel = truncateToWidth(item.label, width - 6);
|
|
442
|
+
const line = `${prefix}${checkbox} ${truncatedLabel}`;
|
|
443
|
+
lines.push(isCursor ? theme.fg("accent", line) : line);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Scroll indicator
|
|
448
|
+
if (visibleItems.length > maxVisible) {
|
|
449
|
+
const scrollInfo = ` (${startIdx + 1}-${endIdx}/${visibleItems.length})`;
|
|
450
|
+
lines.push(theme.fg("dim", scrollInfo));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return lines;
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
render(width: number) {
|
|
458
|
+
const lines: string[] = [];
|
|
459
|
+
lines.push(theme.bold("Simplify: Select items to remove"));
|
|
460
|
+
lines.push(
|
|
461
|
+
theme.fg(
|
|
462
|
+
"muted",
|
|
463
|
+
`Found ${candidates.length} candidates (${safeCandidates.length} safe, ${confirmCandidates.length} confirm, ${reviewCandidates.length} review)`,
|
|
464
|
+
),
|
|
465
|
+
);
|
|
466
|
+
lines.push("");
|
|
467
|
+
lines.push(...renderList(width));
|
|
468
|
+
lines.push("");
|
|
469
|
+
lines.push(
|
|
470
|
+
theme.fg(
|
|
471
|
+
"muted",
|
|
472
|
+
`Selected: ${selectedSet.size} • ↑↓ navigate • space toggle • a select all • enter confirm • esc cancel`,
|
|
473
|
+
),
|
|
474
|
+
);
|
|
475
|
+
return lines;
|
|
476
|
+
},
|
|
477
|
+
invalidate() {},
|
|
478
|
+
handleInput(data: string) {
|
|
479
|
+
if (matchesKey(data, Key.enter)) {
|
|
480
|
+
const results = Array.from(selectedSet).map((i) => selectableItems[i].candidate);
|
|
481
|
+
done(results);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (matchesKey(data, Key.escape)) {
|
|
485
|
+
done([]);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (matchesKey(data, Key.space)) {
|
|
489
|
+
// Toggle current selection
|
|
490
|
+
if (selectedSet.has(cursorPos)) {
|
|
491
|
+
selectedSet.delete(cursorPos);
|
|
492
|
+
} else {
|
|
493
|
+
selectedSet.add(cursorPos);
|
|
494
|
+
}
|
|
495
|
+
tui.requestRender();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (matchesKey(data, "a")) {
|
|
499
|
+
// Select/deselect all
|
|
500
|
+
if (selectedSet.size === selectableItems.length) {
|
|
501
|
+
selectedSet.clear();
|
|
502
|
+
} else {
|
|
503
|
+
for (let i = 0; i < selectableItems.length; i++) {
|
|
504
|
+
selectedSet.add(i);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
tui.requestRender();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Navigation
|
|
512
|
+
if (matchesKey(data, Key.down)) {
|
|
513
|
+
cursorPos = Math.min(selectableItems.length - 1, cursorPos + 1);
|
|
514
|
+
// Ensure cursor is within visible window
|
|
515
|
+
const displayIdx = selectableItems[cursorPos].index;
|
|
516
|
+
if (displayIdx >= scrollOffset + maxVisible) {
|
|
517
|
+
scrollOffset = displayIdx - maxVisible + 1;
|
|
518
|
+
}
|
|
519
|
+
tui.requestRender();
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (matchesKey(data, Key.up)) {
|
|
523
|
+
cursorPos = Math.max(0, cursorPos - 1);
|
|
524
|
+
// Ensure cursor is within visible window
|
|
525
|
+
const displayIdx = selectableItems[cursorPos].index;
|
|
526
|
+
if (displayIdx < scrollOffset) {
|
|
527
|
+
// Scroll up to show the item, and include section header if it's just above
|
|
528
|
+
scrollOffset = Math.max(0, displayIdx - 1);
|
|
529
|
+
}
|
|
530
|
+
tui.requestRender();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (matchesKey(data, Key.home)) {
|
|
534
|
+
cursorPos = 0;
|
|
535
|
+
scrollOffset = 0;
|
|
536
|
+
tui.requestRender();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (matchesKey(data, Key.end)) {
|
|
540
|
+
cursorPos = selectableItems.length - 1;
|
|
541
|
+
const lastDisplayIdx = selectableItems[cursorPos].index;
|
|
542
|
+
scrollOffset = Math.max(0, lastDisplayIdx - maxVisible + 1);
|
|
543
|
+
tui.requestRender();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
return result ?? [];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Execute the cleanup by sending deletion commands
|
|
555
|
+
*/
|
|
556
|
+
async function executeCleanup(
|
|
557
|
+
ctx: ExtensionCommandContext,
|
|
558
|
+
selected: SimplifyResult[],
|
|
559
|
+
): Promise<void> {
|
|
560
|
+
if (selected.length === 0) {
|
|
561
|
+
ctx.ui.notify("No items selected for cleanup", "info");
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const safeItems = selected.filter((c) => c.risk === "safe");
|
|
566
|
+
const confirmItems = selected.filter((c) => c.risk === "confirm");
|
|
567
|
+
|
|
568
|
+
// Build cleanup prompt
|
|
569
|
+
const cleanupPrompt = `# Cleanup Instructions
|
|
570
|
+
|
|
571
|
+
Delete the following code:
|
|
572
|
+
|
|
573
|
+
${selected.map((c) => `- ${c.file}: ${c.reason}`).join("\n")}
|
|
574
|
+
|
|
575
|
+
For each item:
|
|
576
|
+
1. Read the file to find the exact location
|
|
577
|
+
2. Remove only the specified code (not surrounding code unless instructed)
|
|
578
|
+
3. If the removal affects other code, stop and report the issue
|
|
579
|
+
4. After all deletions, verify the code still works by running any existing tests
|
|
580
|
+
|
|
581
|
+
Report:
|
|
582
|
+
- What was deleted
|
|
583
|
+
- Any issues encountered
|
|
584
|
+
- Files that may need further attention`;
|
|
585
|
+
|
|
586
|
+
ctx.ui.notify(
|
|
587
|
+
`Starting cleanup of ${selected.length} items (${safeItems.length} safe, ${confirmItems.length} confirmed)`,
|
|
588
|
+
"info",
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
pi.sendUserMessage(cleanupPrompt);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Main command handler
|
|
596
|
+
*/
|
|
597
|
+
pi.registerCommand("simplify", {
|
|
598
|
+
description: "Clean up leftover code (dead code, debug remnants, over-engineering)",
|
|
599
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
600
|
+
if (!ctx.hasUI) {
|
|
601
|
+
ctx.ui.notify("Simplify requires interactive mode", "error");
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (!ctx.isIdle()) {
|
|
606
|
+
ctx.ui.notify(
|
|
607
|
+
"Cannot run simplify while agent is busy. Wait for current task to complete.",
|
|
608
|
+
"warning",
|
|
609
|
+
);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Check if in git repo
|
|
614
|
+
const { code } = await pi.exec("git", ["rev-parse", "--git-dir"]);
|
|
615
|
+
if (code !== 0) {
|
|
616
|
+
ctx.ui.notify("Not a git repository", "error");
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Check if there are changes
|
|
621
|
+
const { stdout: gitStatus } = await pi.exec("git", ["status", "--porcelain"]);
|
|
622
|
+
if (!gitStatus.trim()) {
|
|
623
|
+
ctx.ui.notify("No git changes found. Simplify reviews uncommitted changes.", "info");
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
ctx.ui.notify("Analyzing code for cleanup candidates...", "info");
|
|
628
|
+
|
|
629
|
+
// Build prompt with optional focus
|
|
630
|
+
let fullPrompt = SIMPLIFY_PROMPT;
|
|
631
|
+
if (args.trim()) {
|
|
632
|
+
fullPrompt += `\n\n## Additional Focus\n\n${args.trim()}`;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Reset capture buffer, then send prompt and wait.
|
|
636
|
+
// message_end events append to assistantTextBuffer during the turn.
|
|
637
|
+
//
|
|
638
|
+
// Race condition fix: pi.sendUserMessage() queues the message, but
|
|
639
|
+
// ctx.waitForIdle() resolves immediately if the agent is still in idle.
|
|
640
|
+
// We must wait for the agent to transition out of idle before waiting.
|
|
641
|
+
assistantTextBuffer = "";
|
|
642
|
+
let resolveTurnStarted: () => void;
|
|
643
|
+
const turnStarted = new Promise<void>((r) => {
|
|
644
|
+
resolveTurnStarted = r;
|
|
645
|
+
});
|
|
646
|
+
pi.on("turn_start", () => resolveTurnStarted!());
|
|
647
|
+
pi.sendUserMessage(fullPrompt);
|
|
648
|
+
await turnStarted;
|
|
649
|
+
await ctx.waitForIdle();
|
|
650
|
+
const analysisText = assistantTextBuffer;
|
|
651
|
+
|
|
652
|
+
if (!analysisText) {
|
|
653
|
+
ctx.ui.notify("Could not read analysis — no response captured.", "error");
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const outcome = parseCandidates(analysisText);
|
|
658
|
+
|
|
659
|
+
if (outcome.kind === "unparseable") {
|
|
660
|
+
const debugPath = dumpDebug(analysisText);
|
|
661
|
+
const suffix = debugPath ? ` Raw response saved to ${debugPath}` : "";
|
|
662
|
+
ctx.ui.notify(
|
|
663
|
+
`Could not parse cleanup candidates — the model did not return the expected JSON block.${suffix}`,
|
|
664
|
+
"warning",
|
|
665
|
+
);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const candidates = outcome.candidates;
|
|
670
|
+
if (candidates.length === 0) {
|
|
671
|
+
ctx.ui.notify("No cleanup candidates found!", "info");
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
ctx.ui.notify(`Found ${candidates.length} candidates. Select items to remove...`, "info");
|
|
676
|
+
|
|
677
|
+
// Show selector
|
|
678
|
+
const selected = await showCandidateSelector(ctx, candidates);
|
|
679
|
+
|
|
680
|
+
if (!selected || selected.length === 0) {
|
|
681
|
+
ctx.ui.notify("Cleanup cancelled", "info");
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Execute cleanup
|
|
686
|
+
await executeCleanup(ctx, selected);
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Quick simplify - auto-clean safe items only
|
|
692
|
+
*/
|
|
693
|
+
pi.registerCommand("simplify-quick", {
|
|
694
|
+
description: "Quick cleanup of obviously safe items (debug code, unused exports)",
|
|
695
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
696
|
+
if (!ctx.hasUI) {
|
|
697
|
+
ctx.ui.notify("Simplify requires interactive mode", "error");
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (!ctx.isIdle()) {
|
|
702
|
+
ctx.ui.notify("Agent is busy. Try again after current task.", "warning");
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const quickPrompt = `# Quick Cleanup
|
|
707
|
+
|
|
708
|
+
Find and delete ONLY the most obviously safe cleanup items:
|
|
709
|
+
|
|
710
|
+
1. \`console.log\`, \`console.warn\`, \`console.error\` statements
|
|
711
|
+
2. \`debugger\` statements
|
|
712
|
+
3. Unused \`import\` statements (imported but never used)
|
|
713
|
+
4. Empty \`catch\` blocks (just \`catch (e) {}\`)
|
|
714
|
+
5. Unused \`const\`/\`let\`/\`var\` declarations
|
|
715
|
+
|
|
716
|
+
DO NOT delete:
|
|
717
|
+
- Commented-out code
|
|
718
|
+
- Over-engineered abstractions
|
|
719
|
+
- Duplicate logic
|
|
720
|
+
- Anything that might be needed
|
|
721
|
+
|
|
722
|
+
For each item found:
|
|
723
|
+
1. Confirm it's truly unused by searching for references
|
|
724
|
+
2. Delete only the specific lines
|
|
725
|
+
3. Report what was deleted
|
|
726
|
+
|
|
727
|
+
Return a summary of what was deleted.`;
|
|
728
|
+
|
|
729
|
+
ctx.ui.notify("Running quick cleanup...", "info");
|
|
730
|
+
pi.sendUserMessage(quickPrompt, { deliverAs: "followUp" });
|
|
731
|
+
},
|
|
732
|
+
});
|
|
81
733
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
734
|
+
/**
|
|
735
|
+
* Status indicator on startup
|
|
736
|
+
*/
|
|
737
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
738
|
+
const { code } = await pi.exec("git", ["rev-parse", "--git-dir"]);
|
|
739
|
+
if (code === 0) {
|
|
740
|
+
ctx.ui.setStatus(
|
|
741
|
+
"simplify",
|
|
742
|
+
`${ctx.ui.theme.fg("accent", "simplify")} ${ctx.ui.theme.fg("muted", "ready (try /simplify)")}`,
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
});
|
|
89
746
|
}
|
package/package.json
CHANGED
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
2
|
+
"name": "@geminixiang/pi-simplify",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "Simplify: Clean up leftover code after feature implementation",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package"
|
|
7
|
+
],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"author": "geminixiang",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/geminixiang/pi-simplify"
|
|
13
|
+
},
|
|
14
|
+
"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
|
+
"@mariozechner/pi-coding-agent": "^0.65.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"oxfmt": "^0.45.0",
|
|
26
|
+
"oxlint": "^1.60.0",
|
|
27
|
+
"typescript": "^6.0.2"
|
|
28
|
+
},
|
|
29
|
+
"pi": {
|
|
30
|
+
"extensions": [
|
|
31
|
+
"./index.ts"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
23
34
|
}
|