@giuseppe.trisciuoglio/pi-jscpd-duplicates0.1.0
pi extension that keeps AI-generated code free of duplicates: an asynchronous jscpd scan runs after every file write, and the agent is asked to refactor when freshly written code duplicates existing code.
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-jscpd-duplicates
A pi extension that keeps AI-generated code free of duplicate blocks. Every time the agent writes or edits a source file, a jscpd scan of the project runs in the background; if the freshly written code duplicates existing code, the agent receives a concise refactor brief and one continuation at the end of the turn.
Why
LLMs tend to repeat themselves: a method defined twice in two files, a helper re-implemented per page, near-identical test boilerplate. Code review catches it, often after the fact. This extension catches it as the code is being written and asks the agent to refactor while the context is still warm.
How it works
write/edit ──▶ tool_result ──▶ debounce 3s ──▶ jscpd in background (npx)
│
agent finishes ──▶ agent_before_settle ──▶ waits for the result
│
any clone involving the freshly written files?
│
no ──▶ done yes ──▶ notify the user
+ one message to the agent
+ continue: true
(max 2 rounds per chain)
- Never blocks the write: jscpd runs as a separate process (
npx jscpd) that starts while the agent is still working; the result is normally ready by the time the turn ends. - Only reports new duplication: clones that involve at least one file written in the current turn. Pre-existing duplication the agent never touched is left alone.
- One consolidated report per turn (
agent_before_settle): a burst of writes and edits collapses into a single scan (debounce) and a single report. - Loop-proof: at most 2 refactor rounds per conversation chain (
agent_settledresets the counter), and if the clone set signature is identical to the previous round (the refactor changed nothing) no second round is offered. - Always notifies the user (
ui.notify), even when continuation is suppressed (turn aborted, cap reached, no-op refactor). - The JSON report is written to a temp directory outside the project so jscpd never scans its own report as a source.
Install
pi install git:github.com/giuseppe-trisciuoglio/pi-jscpd-duplicates
# or from npm:
pi install npm:@giuseppe.trisciuoglio/pi-jscpd-duplicates
# or from a local checkout:
pi install /path/to/pi-jscpd-duplicates
# or try without installing:
pi -e /path/to/pi-jscpd-duplicates/src/index.ts
npx must be available on the host; jscpd is downloaded from the npm cache on first use (a one-off cost of a few seconds).
Usage
- Automatic: no action required. Write code with the agent; if it introduces duplication, the agent receives the report and refactors.
- Manual:
/jscpd [minTokens]— full project scan (all jscpd-supported formats) with an immediate report to the agent.
Configuration
Tunables in src/index.ts (move to settings if needed):
| Constant | Default | Meaning |
|---|---|---|
MIN_TOKENS |
50 | jscpd token threshold for considering a block duplicated |
DEBOUNCE_MS |
3000 | wait after the last write/edit before launching the scan |
SCAN_TIMEOUT_MS |
90000 | scan timeout |
MAX_REFACTOR_ROUNDS |
2 | max refactor continuations per conversation chain |
Supported formats
typescript, javascript, python, java, swift, go, rust, ruby, php, csharp,
scala, c, cpp, bash, kotlin. Other jscpd formats are scanned only by the
manual /jscpd command (which does not filter by extension).
Architecture
src/
index.ts # pi integration: tool_result, agent_before_settle, agent_settled,
# session_shutdown, /jscpd command
scan.ts # pure, decoupled logic, unit-tested. Maps file extensions to
# jscpd formats, parses the JSON report, filters relevant
# clones, formats the report. The sole injected external is an
# `exec` function.
test/
scan.test.ts # vitest unit tests on scan.ts
pi-coding-agent-stub.ts # vitest alias target so src files referencing the
# pi host type resolve under the test runner
scan.ts is pure: it does not import any pi symbol, so its tests need no
host runtime. The pi-coding-agent-stub.ts exists so a future test that
imports src/index.ts still resolves @earendil-works/pi-coding-agent
under vitest's strict resolver.
Design notes
- Debounce + end-of-turn scan rather than per-file scan — agents typically write many files in a burst; per-file scans multiply cost (jscpd still scans the whole project) and produce fragmented reports. The scan runs while the turn is still in progress, and the decision is taken at the only boundary built for continuing the conversation.
agent_before_settlewithcontinue: truerather than notification alone — the goal is to make the agent refactor immediately, while the work's context is still warm; the boundary guarantees exactly one continuation.- Filter on written files + clone orientation — only the duplication the agent has introduced or touched is reported, with the freshly written file oriented first for readability.
- Round cap + clone-set signature — prevents infinite ping-pong between agent and verifier when a refactor does not reduce the duplication.
- Report under
os.tmpdir()— avoids the report being auto-included in the scan. - jscpd via
npxwith no npm dependency of the extension — keeps install zero-cost; a future release as a public npm package could replace thenpxcold start with an explicit dependency. - Double defense against non-first-party paths — jscpd's glob
--ignoredoes not reliably match dot-directories (.gitis filtered poorly by brace patterns, verified on jscpd 5.3.3: sample git hooks leaked in and was reported as "refactor target"). Beyond the explicit patterns, a post-parse segment filter drops clones whose either side includes a junk segment (.git,node_modules,dist, ...). - Header-only clone filter — jscpd counts imports too: two Java test classes importing JUnit/Mockito/Spring-Test symbols easily exceed 50 tokens, but imports are not extractable logic. For every clone the
fragmentis stripped of blank lines, comments, and header lines (package/importfor Java/Kotlin/Scala,using/namespace ...;for C#,import/from ... importfor Python, single-line imports for TS/JS) and the tokens are re-counted approximately: if fewer thanminTokensremain, the clone is dropped. Clones that include logic above the threshold survive. Applies to both the automatic mode and/jscpd. :formatsuffix normalization — jscpd sometimes appends the format to the reported file name (CHANGELOG.md:markdown); parsing strips it when it coincides with the clone's format, otherwise the path-matching in automatic mode would fail silently.
Development
npm install
npm test # typecheck + vitest unit tests
npm run typecheck # tsc --noEmit
npm run test:unit # vitest run
There is no build step: pi loads the TypeScript extension directly.
Dependency-update PRs (npm packages + GitHub Actions) are produced by Dependabot on a weekly Monday schedule, configured in .github/dependabot.yml. Dependency bumps are grouped to keep PR noise low and labelled dependencies + enhancement.
Release
- Bump
versioninpackage.jsonand add a section toCHANGELOG.md. - Commit on
main. - Tag the release:
git tag v0.X.Yand push the tag. - Create a GitHub release for the tag:
.github/workflows/publish.ymlruns typecheck + tests, verifies the tag matchespackage.json, and publishes to npm with--access public.
License
MIT — see LICENSE.