feat: add snippet CRUD tools - #470
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7e5f431e08
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ee5e4724ff
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
ee5e472 to
4ff40ac
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4ff40ace3f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Thanks for adding the snippet toolset. I found a few API-contract issues that should be fixed before merging.
- Nested multi-file snippet content fetch can 404
getSnippetFileRawContent() encodes the file path per path segment:
filePath.split("/").map(encodeURIComponent).join("/")
For a nested snippet file like dir/policy.md, the request stays as /files/main/dir/policy.md/raw. GitLab's snippets API expects file_path to be a URL-encoded path for the /files/:ref/:file_path/raw endpoint, so nested paths can be routed incorrectly or return 404.
Suggested fix: encode the entire path with encodeURIComponent(filePath) and add a test with a nested file path such as dir/policy.md.
- URL-encoded project paths are double-encoded
getSnippetsEndpoint() accepts project_id values described as either project IDs or URL-encoded paths, but it passes the value directly through encodeURIComponent. With project_id=group%2Fproject, the endpoint becomes /projects/group%252Fproject/snippets instead of the intended project path.
Suggested fix: follow the existing project endpoint pattern and decode before re-encoding, e.g. decodeURIComponent(projectId) before getEffectiveProjectId/encodeURIComponent.
- Project snippet responses may fail schema parsing
GitLabSnippetSchema requires visibility, but the Project Snippets API response shape does not consistently document/return that field. A valid project snippet response without visibility would fail with a ZodError across list/get/create/update paths.
Suggested fix: make visibility optional or split personal/project snippet response schemas.
update_snippetdoes not expose the modern multi-file update contract
create_snippet supports files[], but update_snippet only exposes the legacy-ish file_name + content shape and converts it to a single action: "update" file. GitLab supports multi-file update actions such as create/update/delete/move with previous_path. As written, users can create multi-file snippets but cannot fully update/move/delete files through this tool.
Suggested fix: add a files[] update shape with action and previous_path support, while keeping file_name/content as a backward-compatible shortcut if desired.
Adds list_snippets, get_snippet, create_snippet, update_snippet, and delete_snippet — supporting both project-scoped and personal snippets via optional project_id. Registered under a new non-default "snippets" toolset. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
The multi-file content endpoint requires a ref to construct the API URL.
Using default_branch caused 404s on instances where the default branch
is not "main"; fetching raw_url directly also 404s because it is a web
URL, not an API endpoint.
GitLab embeds the correct ref inside each file's raw_url
(.../raw/{ref}/{path}). Parse it out and pass it to the REST API
endpoint (/snippets/{id}/files/{ref}/{path}/raw) instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getSnippetFileRawContent now takes an explicit ref string instead of a raw_url to parse. The ref-extraction logic is factored into a standalone extractSnippetRef helper, resolved once at the call site rather than once per file inside the fetch function. get_snippet also gains an optional ref parameter (matching get_file_contents) so callers can override the ref directly without relying on raw_url parsing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Nested multi-file paths now encode the full file_path as a single segment (encodeURIComponent) per GitLab's /files/:ref/:file_path/raw contract. Previously, paths like dir/policy.md kept the slash and 404'd. - getSnippetsEndpoint decodes URL-encoded project paths before re-encoding, matching the existing pattern in merge_requests/issues. Eliminates double-encoded URLs like /projects/group%252Fproject/snippets. - GitLabSnippetSchema.visibility is now optional — the project snippets API does not consistently return it and ZodError was breaking list/get/create/ update for valid responses. - update_snippet exposes the modern multi-file contract: files[] with action (create/update/delete/move) and previous_path. The file_name+content shortcut remains for backward compatibility and is mutually exclusive.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a34d885f8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
8a34d88 to
a5314e8
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a5314e83f0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
SnippetFileUpdateActionSchema had previous_path optional even though the schema description said it was required for action: "move". That left malformed move actions to fail late with a GitLab 400 instead of being caught at parse time. Adds a superRefine that surfaces the constraint as a Zod error so LLM-generated update requests get a clear, local rejection.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 68b4aa690a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
zereight
left a comment
There was a problem hiding this comment.
Thanks for the follow-up fixes. The original API-contract issues I raised are mostly addressed now: nested snippet file paths are encoded correctly, URL-encoded project paths are decoded before re-encoding, visibility is no longer required for project snippet responses, and update_snippet now exposes the modern files[] update shape. The new test/test-snippets.ts file is also wired into test:mock.
I’m refreshing the request-changes baseline because a few issues remain:
-
Project-scoped deployments still need a guard for omitted
project_id. WhenGITLAB_PROJECT_IDorGITLAB_ALLOWED_PROJECT_IDSis configured, falling back to the global/snippetsendpoint bypasses the project boundary. I replied on the existing thread with the expected project-standard behavior. -
get_snippet(include_content=true, ref=...)still requiresraw_urlbefore it checks the explicitref, so valid calls can fail when the snippet file payload haspathbut noraw_url. I replied on the existing thread with the minimal shape. -
SnippetFileUpdateActionSchemashould validate the action-specific required fields locally:file_pathfor every action,contentfor create/update,previous_pathfor move, and a non-emptyfiles[]array when provided. I replied on the existing schema thread with the matrix.
Once these are addressed, this should be close to merge-ready.
get_snippet(include_content=true) previously threw if files[0].raw_url was missing, even when the caller passed ref explicitly. Flip the guard so raw_url is only required as a fallback for inferring ref.
getSnippetsEndpoint used to fall back to the global /snippets endpoint whenever project_id was omitted, bypassing GITLAB_PROJECT_ID and GITLAB_ALLOWED_PROJECT_IDS access boundaries. Route omitted project_id through getEffectiveProjectId when either scope env is set, reusing its existing default-when-unambiguous, reject-when-ambiguous semantics. Personal snippets remain available only when no scope is configured.
SnippetFileUpdateActionSchema only enforced previous_path for 'move', so malformed actions (create/update without content, delete without file_path) and empty files[] arrays passed local validation and only failed later as GitLab 400s. Expand the superRefine with the full action matrix and require files[] to be non-empty when provided.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 79eb6d6f57
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
zereight
left a comment
There was a problem hiding this comment.
The three follow-ups from the previous review look fixed on 79eb6d6f:
- Omitted
project_idnow routes throughgetEffectiveProjectIdwhenGITLAB_PROJECT_IDorGITLAB_ALLOWED_PROJECT_IDSis set, instead of falling back to/snippets. - Multi-file
get_snippet(include_content=true)accepts an explicitrefwithout requiringfiles[].raw_url. SnippetFileUpdateActionSchemarejects malformedfiles[]actions at parse time, with tests for the action matrix.
One gap remains. In get_snippet, when files.length <= 1, content fetch always uses /raw and ignores args.ref. Multi-file snippets already honor ref via getSnippetFileRawContent, so ref behaves differently depending on file count. Callers passing include_content=true and ref on a single-file snippet still get default HEAD content.
Suggested fix: when args.ref is set and there is exactly one file, resolve the path from files[0].path or snippet.file_name and call getSnippetFileRawContent; keep /raw as the fallback when ref is omitted.
Minor follow-up (non-blocking): snippet schema descriptions say "omit for personal snippets" but do not mention scope env behavior. Other tools use "(optional when GITLAB_PROJECT_ID is set)" — worth aligning so copy-paste configs match runtime.
test/test-snippets.ts coverage is solid and the toolset wiring looks correct. Once single-file ref is handled, this should be good to merge.
| })) | ||
| ); | ||
| } else { | ||
| result.content = await getSnippetRawContent( |
There was a problem hiding this comment.
args.ref is ignored here because this branch always calls getSnippetRawContent. Use getSnippetFileRawContent when ref is provided and there is a resolvable file path (files[0].path or snippet.file_name).
Adds list_snippets, get_snippet, create_snippet, update_snippet, and delete_snippet — supporting both project-scoped and personal snippets via optional project_id. Registered under a new non-default "snippets" toolset.
It seems well covered and tested:
My first pull request here, let me know anything I missed.