Skip to content

Commit 7b11ff9

Browse files
authored
[ENG-2080] Vendor the Greptile CLI into the plugin (#2)
* Vendor the Greptile CLI into the plugin The commands fetched the CLI with npx, so the plugin never owned it: uninstalling left the binary, its npx cache, and the login in place, and the plugin could not say which version would run. The bundle now ships in the plugin and the Bash permission is scoped to that one file instead of Bash(npx:*), which pre-approved any npx invocation. The vendored file comes from the published npm tarball, not a local build. A local `bun scripts/bundle.ts` of 3.4.2 is NOT byte-identical to npm's 3.4.2 (10757903 vs 10694685 bytes, different sha256), so vendoring a rebuild would ship a third artifact under a version number that means something else. CI now pins the vendored file to npm's by hash. It is renamed to .mjs because the npm package carries "type": "module" in a package.json the plugin does not vendor. Without that, node walks up from the script and warns MODULE_TYPELESS_PACKAGE_JSON on every run, which would land in the --agent output the review command parses. Both commands set GREPTILE_NO_UPDATE_CHECK=1. install-method.ts infers the install from the script path; a plugin path matches no known case and falls through to 'direct', whose update command is a curl-pipe-bash installer that cannot update the plugin's copy - and updatePlanFor returns run: true for it, so `greptile update` could execute it. Upstream should learn a 'plugin' case. Adds MIT LICENSE, matching what the CLI's README and npm package declare - the repo was previously all-rights-reserved while redistributing an MIT-licensed artifact. * Exclude the vendored CLI bundle from review It is a published artifact reviewed upstream, so there is nothing to review here, and it is large enough to crowd out real findings. This does not fix `greptile review` on the commit that ADDS the bundle. The CLI caps the payload at 3 MB in preflight.ts:257, computed from the local diff before any server-side config is read, so ignorePatterns cannot apply. The cap is on the diff rather than the repo, so only this change is affected - branches cut after it do not re-include the blob. * Make the CLI Check actually guard the command files The path filter listed the vendored scripts but not the commands, so a pull request that only edited review.md or login.md - exactly where the npx and update-check regressions would reappear - never ran the job that validates them. The predicates also matched whole Markdown files rather than the fenced command. review.md carries a paragraph explaining GREPTILE_NO_UPDATE_CHECK=1, so stripping the variable from the real invocation left the check passing on the explanation. Both assertions now run against the extracted command block and require the exact invocation.
1 parent 8ff72ce commit 7b11ff9

10 files changed

Lines changed: 1104 additions & 8 deletions

File tree

.github/workflows/cli-check.yml

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
name: CLI Check
2+
3+
# The plugin vendors the Greptile CLI at plugins/greptile/scripts/greptile.mjs.
4+
# Vendoring is a manual step, so the two things that can silently go wrong are
5+
# shipping a bundle that is not the release it claims to be, and shipping one
6+
# that is not what npm publishes at all. Both are checked by running the file
7+
# and by comparing it byte-for-byte against the published tarball.
8+
#
9+
# A local rebuild of the CLI is NOT byte-identical to the npm artifact for the
10+
# same version, so the vendored file must always be extracted from `npm pack`,
11+
# never from a local `bun scripts/bundle.ts`.
12+
#
13+
# No secrets. As in mcp-check.yml, repo-controlled values are read from disk
14+
# and never interpolated into shell source with `${{ }}` — a fork pull request
15+
# controls these files.
16+
17+
on:
18+
push:
19+
branches: [main]
20+
paths:
21+
- 'plugins/greptile/scripts/**'
22+
- 'plugins/greptile/commands/**'
23+
- '.github/workflows/cli-check.yml'
24+
pull_request:
25+
paths:
26+
- 'plugins/greptile/scripts/**'
27+
- 'plugins/greptile/commands/**'
28+
- '.github/workflows/cli-check.yml'
29+
schedule:
30+
- cron: '41 8 * * *'
31+
workflow_dispatch:
32+
33+
permissions:
34+
contents: read
35+
36+
jobs:
37+
check:
38+
runs-on: ubuntu-latest
39+
timeout-minutes: 10
40+
steps:
41+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
42+
43+
- name: Recorded version is well formed
44+
run: |
45+
set -euo pipefail
46+
version=$(tr -d '[:space:]' < plugins/greptile/scripts/greptile.version)
47+
if ! printf '%s' "$version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
48+
echo "::error::plugins/greptile/scripts/greptile.version is not a plain semver string."
49+
exit 1
50+
fi
51+
printf '%s' "$version" > "$RUNNER_TEMP/version.txt"
52+
echo "Recorded version $version"
53+
54+
- name: Bundle runs and reports the recorded version
55+
run: |
56+
set -euo pipefail
57+
version=$(cat "$RUNNER_TEMP/version.txt")
58+
reported=$(node plugins/greptile/scripts/greptile.mjs --version | tr -d '[:space:]')
59+
if [ "$reported" != "$version" ]; then
60+
echo "::error::Vendored bundle reports $reported but greptile.version records $version."
61+
exit 1
62+
fi
63+
echo "Bundle reports $reported."
64+
65+
- name: Bundle is byte-identical to the published npm release
66+
run: |
67+
set -euo pipefail
68+
version=$(cat "$RUNNER_TEMP/version.txt")
69+
cd "$RUNNER_TEMP"
70+
npm pack "greptile@$version" >/dev/null
71+
tar -xzf "greptile-$version.tgz"
72+
cd "$GITHUB_WORKSPACE"
73+
published=$(shasum -a 256 "$RUNNER_TEMP/package/dist/greptile.js" | cut -d' ' -f1)
74+
vendored=$(shasum -a 256 plugins/greptile/scripts/greptile.mjs | cut -d' ' -f1)
75+
if [ "$published" != "$vendored" ]; then
76+
echo "::error::Vendored bundle does not match npm greptile@$version. published=$published vendored=$vendored. Re-vendor with: npm pack greptile@$version && tar -xzf greptile-$version.tgz && cp package/dist/greptile.js plugins/greptile/scripts/greptile.mjs"
77+
exit 1
78+
fi
79+
echo "Vendored bundle matches npm greptile@$version ($vendored)."
80+
81+
- name: Commands invoke the vendored bundle, not a fetched one
82+
run: |
83+
set -euo pipefail
84+
# Assert against the fenced command block only. Matching whole files
85+
# would fail on a prose mention of npx, and — worse — would accept a
86+
# command that dropped GREPTILE_NO_UPDATE_CHECK as long as the
87+
# paragraph explaining it survived.
88+
for f in plugins/greptile/commands/review.md plugins/greptile/commands/login.md; do
89+
block=$(awk '/^```/{fence = !fence; next} fence' "$f")
90+
if [ -z "$block" ]; then
91+
echo "::error::$f has no fenced command block to validate."
92+
exit 1
93+
fi
94+
if grep -q 'npx' <<<"$block"; then
95+
echo "::error::$f still fetches the CLI with npx. The plugin vendors it; invoke \${CLAUDE_PLUGIN_ROOT}/scripts/greptile.mjs instead."
96+
exit 1
97+
fi
98+
if ! grep -qF 'node "${CLAUDE_PLUGIN_ROOT}/scripts/greptile.mjs"' <<<"$block"; then
99+
echo "::error::$f does not invoke the vendored bundle at \${CLAUDE_PLUGIN_ROOT}/scripts/greptile.mjs."
100+
exit 1
101+
fi
102+
if ! grep -qF 'GREPTILE_NO_UPDATE_CHECK=1 node "${CLAUDE_PLUGIN_ROOT}/scripts/greptile.mjs"' <<<"$block"; then
103+
echo "::error::$f invokes the vendored bundle without GREPTILE_NO_UPDATE_CHECK=1. Without it the CLI reads its own path as a standalone install and tells the user to run an installer that cannot update the plugin's copy."
104+
exit 1
105+
fi
106+
done
107+
echo "Commands invoke the vendored bundle with the update check disabled."

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Greptile, Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ The plugin gives Claude Code two ways to work with Greptile:
1414
- the **Greptile MCP server**, for reading and resolving review results and for searching your knowledge base and coding patterns
1515
- the **Greptile CLI**, for reviewing your working branch before a pull request exists
1616

17-
Both authenticate over OAuth against your Greptile account. There is no API key to create and nothing to install.
17+
Both authenticate over OAuth against your Greptile account. There is no API key to create and nothing to install — the CLI ships with the plugin, so it needs no npm or Homebrew install, only Node on your machine.
1818

1919
See [`plugins/greptile`](./plugins/greptile) for setup, commands, and the full tool list.

greptile.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"ignorePatterns": "plugins/*/scripts/*.mjs"
3+
}

plugins/greptile/.claude-plugin/plugin.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"name": "greptile",
33
"description": "AI code review agent for GitHub and GitLab. View and resolve Greptile's PR review comments, run reviews on your working branch, and search your organization's knowledge base and coding patterns, directly from Claude Code.",
4-
"version": "1.0.0",
4+
"version": "1.1.0",
5+
"license": "MIT",
56
"author": {
67
"name": "Greptile",
78
"url": "https://greptile.com"

plugins/greptile/README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Nothing to install and no API key to create.
1313

1414
**MCP server.** Open the `/mcp` menu and authenticate **greptile**. Your browser opens [auth.greptile.com](https://auth.greptile.com); sign in and approve access. Claude Code stores and refreshes the tokens.
1515

16-
**CLI.** Run `/greptile:login` once. This fetches the CLI with `npx` and signs it in through the same OAuth provider. Requires Node 22 or newer.
16+
**CLI.** Run `/greptile:login` once and sign in through the same OAuth provider. The CLI ships with this plugin — there is nothing to fetch and no npm or Homebrew install — but it is a Node program, so Node must be on your machine.
1717

1818
The two sign-ins are separate: same Greptile account, same OAuth provider, but the CLI keeps its own credentials in `~/.greptile/auth.json` while Claude Code keeps the MCP tokens in its own store. Use whichever surface you need; you only have to sign in to that one.
1919

@@ -54,6 +54,19 @@ The two sign-ins are separate: same Greptile account, same OAuth provider, but t
5454
- "What issues did Greptile find on PR #123?"
5555
- "Search our knowledge base for how authentication works in this repo"
5656

57+
## Bundled CLI
58+
59+
`scripts/greptile.mjs` is the Greptile CLI, vendored from the published npm package
60+
`greptile` (its `dist/greptile.js`, renamed only so Node reads it as ESM without a
61+
sibling `package.json`). `scripts/greptile.version` records which release it is, and
62+
CI verifies the file byte-for-byte against that version's npm tarball, so the copy
63+
running here is the same one npm serves.
64+
65+
Because the CLI ships with the plugin, it updates with the plugin — not through
66+
`greptile update`, `npm`, or `brew`. Any separate `greptile` you have installed is
67+
untouched and unused by these commands, though both share your login at
68+
`~/.greptile/auth.json`.
69+
5770
## Documentation
5871

5972
See [greptile.com/docs/mcp-v2/overview](https://www.greptile.com/docs/mcp-v2/overview).

plugins/greptile/commands/login.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
allowed-tools: Bash(npx:*)
2+
allowed-tools: Bash(node ${CLAUDE_PLUGIN_ROOT}/scripts/greptile.mjs:*)
33
argument-hint: ""
44
description: Sign the Greptile CLI in to your Greptile account
55
---
@@ -10,7 +10,7 @@ Tell the user a browser window will open and that they need to finish signing
1010
in there, then run:
1111

1212
```
13-
npx -y greptile@latest login
13+
GREPTILE_NO_UPDATE_CHECK=1 node "${CLAUDE_PLUGIN_ROOT}/scripts/greptile.mjs" login
1414
```
1515

1616
Give the Bash call a **600000 ms timeout**. The CLI waits up to ten minutes for

plugins/greptile/commands/review.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
allowed-tools: Bash(npx:*), mcp__plugin_greptile_greptile__get_code_review, mcp__plugin_greptile_greptile__list_code_reviews
2+
allowed-tools: Bash(node ${CLAUDE_PLUGIN_ROOT}/scripts/greptile.mjs:*), mcp__plugin_greptile_greptile__get_code_review, mcp__plugin_greptile_greptile__list_code_reviews
33
argument-hint: [base branch] [what to focus on]
44
description: Review the current branch with Greptile
55
---
@@ -17,13 +17,18 @@ Build the command from `$ARGUMENTS`, if the user supplied any:
1717
instructions and `$(...)` would stop the run at a permission prompt.
1818

1919
```
20-
npx -y greptile@latest review --agent
20+
GREPTILE_NO_UPDATE_CHECK=1 node "${CLAUDE_PLUGIN_ROOT}/scripts/greptile.mjs" review --agent
2121
```
2222

2323
Always pass `--agent`; it selects plain output intended for AI agents.
2424

25+
`GREPTILE_NO_UPDATE_CHECK=1` is required, not cosmetic. The CLI infers how it was
26+
installed from its own path; a copy running from inside this plugin looks like a
27+
standalone install, so the notifier would tell the user to re-run an installer
28+
that cannot update the plugin's copy. Updates arrive with the plugin instead.
29+
2530
Give the Bash call a **600000 ms timeout**. A review commonly runs longer than
26-
the two-minute default, and the first invocation also has to fetch the CLI.
31+
the two-minute default.
2732

2833
If the run reports that no one is signed in, it prints `not signed in. Set
2934
GREPTILE_API_KEY or run 'greptile login --api-key'`. Prefer OAuth over an API

plugins/greptile/scripts/greptile.mjs

Lines changed: 945 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.4.2

0 commit comments

Comments
 (0)