v0.2: wrap every new endpoint family from feat/workspace-projects-browser#2
v0.2: wrap every new endpoint family from feat/workspace-projects-browser#2claudio-michel[bot] wants to merge 3 commits into
Conversation
…ts-browser
Pure additive release. No deletions. Existing v0.1 scripts + skills work
unchanged; this layers wrappers on top of them for every capability that
the workspace-projects-browser branch introduces.
10 new skills:
* /grep-search — search past jobs by status / question text
* /grep-jobs — quick recent-jobs listing
* /grep-resume — resume a paused job (with optional steering)
* /grep-stop — pause/cancel a running check; soft-delete result
* /grep-apps — list slidedeck/podcast/narrative artifacts
* /grep-inputs — attach/remove per-job input files (multipart)
* /grep-defaults — manage per-user default context files (multipart)
* /grep-workspace — browse Pierre-backed workspace tree, files,
commit history, diffs
* /grep-projects — create + manage SOP-driven projects (POST SOP.md
to projects/<name>/, then run with --project=...)
* /grep-experts — initialize, save, and train custom research
experts (config + knowledge base via documents)
scripts/grep-api.js extended:
* apiMultipart() helper for multipart uploads.
* 20+ new helper functions covering every endpoint family, each with a
CLI subcommand (run `node scripts/grep-api.js help` for the full list).
* searchJobs() — client-side filter over /api/v1/research, since the
backend has no full-text search yet.
* submitResearch / runResearch extended to accept all new optional
fields on ResearchJobInput: --project, --expert, --language,
--from-date, --to-date, --additional-thesis, --website,
--custom-skills, --custom-mcp-tools, --skip-clarification,
--action-mode, --output-type.
Light updates to 5 existing skills (advanced-flags sections):
* /research, /quick-research, /ultra-research now document the new
optional flags (project / expert / language / etc.).
* /grep-status points users at /grep-search for filtered listings.
* /grep-skill-creator notes that custom skills surface via
--custom-skills on research submit.
Untouched:
* scripts/auth.js, scripts/billing.js — no changes.
* All 8 existing skills' core flows — unchanged.
* bin/install.js — no functional change; symlink loop picks up the
new skill dirs automatically (verified: 18 skills symlinked).
CHANGELOG.md added (didn't exist on main).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rning
Five fixes from the /simplify pass:
1. Session caching — getValidToken() previously hit
~/.grep/session.json on every HTTP request. The poll loop in
runResearch() invokes api() ~30+ times per long job; that was 30+
disk reads. Cache _session in-process; saveSession() refreshes it.
2. printResult() helper — consolidates 26 callsites of
`console.log(JSON.stringify(result, null, 2))`.
3. encodeRemotePath() helper — collapses two duplicate
`path.split('/').map(encodeURIComponent).join('/')` instances in
deleteInput() and deleteDefault().
4. buildFileParts() helper — collapses three duplicate `files.map(p
=> { const f = readFile(p); return { name: 'files', ... } })` blocks
in attachInputs(), projectUpload(), expertTrain().
5. expertSave() now warns to stderr when the config file isn't valid
JSON before falling through to "send raw for server-side YAML
parsing". Previously this silently swallowed parse errors, leading
to opaque downstream failures if the YAML was also malformed.
Also:
* readFile() drops the TOCTOU `fs.existsSync` pre-check; just try/catch
the read.
* Three migration-history comments deleted ("unchanged from v0.1",
"New fields from feat/workspace-projects-browser", "---- new in
v0.2 ----"). They narrated the PR, not invariants.
Net +27 lines for ~50 lines of duplication eliminated and one
silent-failure bug fixed. No behavior changes for users.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| console.log(JSON.stringify(result, null, 2)); | ||
| if (options.project) body.project = options.project; | ||
| if (options.expert) body.expert_id = options.expert; | ||
| if (options.language) body.language = options.language; |
There was a problem hiding this comment.
[BUG] --language flag has no effect — wrong field name sent to backend
The backend's ResearchJobInput field is response_language (in shared_grep_router.py and grep_router.py). The wrapper sends body.language = options.language, which Pydantic silently drops because the model doesn't declare extra=forbid. Result: the --language=es flag is documented in help text, the README, and /research, but the backend never sees it and the report comes back in English.
Fix: rename the JSON field, not the CLI flag:
if (options.language) body.response_language = options.language;This is the most user-visible bug in the PR — every i18n flow goes through this path. Same in buildSubmitBody. Also worth a smoke test like run --language=es "hola" after the fix.
|
|
||
| async function expertInit(expertName) { | ||
| if (!expertName) throw new Error('expert_name required'); | ||
| const result = await api('POST', '/grep/code-storage/workspace/experts/init', { expert_name: expertName }); |
There was a problem hiding this comment.
[BUG] expert:init sends the wrong field name — backend will return 422
The backend route POST /grep/code-storage/workspace/experts/init is bound to:
class InitExpertRequest(_BaseModel):
folder_name: strThe wrapper sends { expert_name }. Result: 422 Unprocessable Entity, every time.
Fix:
const result = await api('POST', '/grep/code-storage/workspace/experts/init', { folder_name: expertName });While you're touching this, please also rename the wrapper arg / CLI label so it matches the SOP/folder mental model the backend uses (it sanitizes folder_name to lowercase + [a-z0-9_-] only). The current SKILL.md example expert:init medical-research-expert will work once the field name is correct, n'est-ce pas?
| process.stderr.write(`[expert:save] config is not valid JSON (${e.message}); sending as raw string for server-side YAML parsing.\n`); | ||
| config = text; | ||
| } | ||
| const result = await api('POST', '/grep/code-storage/workspace/experts/save', { |
There was a problem hiding this comment.
[BUG] expert:save body shape is completely wrong — backend will 422
Backend expects:
class SaveExpertFileRequest(_BaseModel):
folder_path: str # e.g. "medical-research-expert"
file_name: str # e.g. "SOP.md" or "config.yml"
content: str # the raw file contents
commit_message: str = ""The wrapper sends { expert_name, config }. Two-fold mismatch:
- The endpoint saves one file at a time (not a whole config blob).
configis parsed as JSON-or-string client-side, but the backend wants the rawcontentstring and a separatefile_name.
This means the expert:save medical-expert ./medical-config.yml workflow in the SKILL.md will always fail.
Fix: require an explicit file name (or default to config.yml / derive from local basename) and send:
async function expertSave(expertName, configPath, fileName) {
const f = readFile(configPath);
const result = await api('POST', '/grep/code-storage/workspace/experts/save', {
folder_path: expertName,
file_name: fileName || f.name, // or hard-default 'SOP.md'
content: f.buffer.toString('utf8'),
commit_message: `Update ${fileName || f.name} in ${expertName}`,
});
printResult(result);
}The CLI signature also needs to change to take the file name:
expert:save <expert_name> <config_file> [<file_name>]
Then update skills/grep-experts/SKILL.md so the example reflects this.
| `/grep/code-storage/workspace/experts/${encodeURIComponent(expertName)}/brain`, | ||
| buildFileParts(files), | ||
| ); | ||
| printResult(result); |
There was a problem hiding this comment.
[BUG] expert:train is silently broken — backend ignores the uploaded files
The backend route is:
@router.post("/code-storage/workspace/experts/{expert_name}/brain")
async def ensure_expert_brain_endpoint(
request: Request,
expert_name: str,
db_session: AsyncSession = Depends(get_async_fast_api_session),
) -> dict:It accepts no body / no files. It just creates brain.json at experts/{expert_name}/brain.json if missing. Idempotent.
The wrapper uploads files via multipart, the request returns 200 (because the endpoint succeeds at creating brain.json), but the documents are silently dropped. The skill's promise ("Each document is added to the expert's knowledge base") is not delivered.
Two paths forward:
- Repurpose
expert:trainasexpert:brain— match the actual backend behavior ("ensure brain.json exists"), drop the files arg, update the SKILL.md. - Wait for a real ingestion endpoint — flag this in CHANGELOG as "Coming soon" and remove
expert:trainfrom v0.2.
Either way, the current state ships a feature that doesn't do what it claims. I'd vote option 1 + a TODO in the SKILL.md saying document ingestion is on the roadmap.
| if (!files || !files.length) throw new Error('at least one file required'); | ||
| const parts = [{ name: 'project_name', value: projectName }, ...buildFileParts(files)]; | ||
| const result = await apiMultipart('POST', '/grep/code-storage/workspace/projects/upload', parts); | ||
| printResult(result); |
There was a problem hiding this comment.
[BUG] project:upload doesn't match the backend's multipart contract
Backend signature:
async def upload_project_files_endpoint(
request: Request,
files: List[UploadFile] = File(...),
paths: List[str] = Form(...),
commit_message: str = Form(""),
...
)Important things to notice:
- There is no
project_nameform field at all. The directory is encoded in eachpaths[i]value (e.g.projects/acme/SOP.mdoracme/SOP.md— the backend writes under prefixprojects/). pathsis required (Form(...)with no default) and must be the same length asfiles.
The wrapper currently sends { project_name: 'acme', files: [...] } and no paths field at all. FastAPI returns 422 because paths is missing.
Fix: drop project_name form field, build paths aligned to files, scoped under the project subdir:
async function projectUpload(projectName, files) {
if (!projectName) throw new Error('project_name required');
if (!files || !files.length) throw new Error('at least one file required');
const parts = [];
for (const localPath of files) {
const f = readFile(localPath);
parts.push({ name: 'files', filename: f.name, value: f.buffer, contentType: f.contentType });
parts.push({ name: 'paths', value: `${projectName}/${f.name}` });
}
parts.push({ name: 'commit_message', value: `Upload ${files.length} file(s) to ${projectName}` });
const result = await apiMultipart('POST', '/grep/code-storage/workspace/projects/upload', parts);
printResult(result);
}The skill examples project:upload acme-onboarding ./SOP.md will then land at projects/acme-onboarding/SOP.md as documented.
| async function projectDelete(projectName, filePath) { | ||
| if (!projectName || !filePath) throw new Error('project_name and file_path required'); | ||
| const params = new URLSearchParams({ project_name: projectName, file_path: filePath }); | ||
| const result = await api('DELETE', `/grep/code-storage/workspace/projects/file?${params}`); |
There was a problem hiding this comment.
[BUG] project:delete sends wrong query params — backend will 422
Backend signature:
async def delete_project_file_endpoint(
request: Request,
path: str, # single 'path' query param
db_session: AsyncSession = Depends(get_async_fast_api_session),
)The backend takes one path query param (the file path relative to projects/, since the route uses prefix="projects" inside delete_workspace_file).
The wrapper sends ?project_name=X&file_path=Y, neither of which matches.
Fix:
async function projectDelete(projectName, filePath) {
if (!projectName || !filePath) throw new Error('project_name and file_path required');
const params = new URLSearchParams({ path: `${projectName}/${filePath}` });
const result = await api('DELETE', `/grep/code-storage/workspace/projects/file?${params}`);
printResult(result);
}| if (!projectName || !dirPath) throw new Error('project_name and dir_path required'); | ||
| const params = new URLSearchParams({ project_name: projectName, dir_path: dirPath }); | ||
| const result = await api('POST', `/grep/code-storage/workspace/projects/mkdir?${params}`); | ||
| printResult(result); |
There was a problem hiding this comment.
[BUG] project:mkdir sends query params — backend expects a JSON body
Backend signature:
class CreateDirectoryRequest(_BaseModel):
path: str
async def create_project_directory_endpoint(
request: Request,
body: CreateDirectoryRequest, # <-- JSON body, not query params
...
)The wrapper does api('POST', /...?project_name=X&dir_path=Y) with no body. Backend will 422 because body is required and the JSON body is empty.
Fix:
async function projectMkdir(projectName, dirPath) {
if (!projectName || !dirPath) throw new Error('project_name and dir_path required');
const result = await api('POST', '/grep/code-storage/workspace/projects/mkdir', {
path: `${projectName}/${dirPath}`,
});
printResult(result);
}| if (!p) throw new Error('path required'); | ||
| // Returns raw bytes — but for skill-friendly output, fetch + print the body. | ||
| const token = await getValidToken(); | ||
| const res = await fetch(`${GREP_API_BASE}/grep/code-storage/workspace/file?path=${encodeURIComponent(p)}`, { |
There was a problem hiding this comment.
[WARNING] ws:cat dumps JSON, not raw file content
Backend default for GET /code-storage/workspace/file?path=X:
return {"path": path, "content": text, "size_bytes": len(content)}Only returns raw bytes when ?raw=true or ?download=true is passed.
Currently ws:cat projects/acme/SOP.md will print:
{"path": "projects/acme/SOP.md", "content": "# Standard Operating Procedure\n\n...", "size_bytes": 4023}…then a trailing newline. Not what the SKILL.md promises ("Streams raw bytes to stdout").
Fix: add &raw=true to the URL:
const res = await fetch(`${GREP_API_BASE}/grep/code-storage/workspace/file?path=${encodeURIComponent(p)}&raw=true`, {Bonus: encodeURIComponent here will encode / as %2F. The path query param is parsed by FastAPI as a single string so that's actually fine — but worth a comment so future-you doesn't 'fix' it.
| console.error(' --custom-skills=skill1,skill2 Custom skill names'); | ||
| console.error(' --custom-mcp-tools=tool1,tool2 Custom MCP tool names'); | ||
| console.error(' --skip-clarification Bypass clarification questions'); | ||
| console.error(' --output-type=report|data_explorer Output shape'); |
There was a problem hiding this comment.
[WARNING] --action-mode is wired in code but missing from help text
buildResearchOpts reads flags['action-mode'] and buildSubmitBody sets body.action_mode = true, but the default: help block (lines 820-833) doesn't list it. /ultra-research's SKILL.md does mention it ("admin-only"), but /research's advanced-flags table omits it too.
Either:
- Add it to the help text and the
/researchtable, or - Drop it from the option bag if it's truly admin-only and not meant for end users (it'll still be hittable as
--action-modesince flags pass through, but at least there'd be no implicit invitation).
| // Workspace browsing: /grep/code-storage/workspace[/...] | ||
| // ============================================================================= | ||
|
|
||
| async function wsTree(subpath, opts = {}) { |
There was a problem hiding this comment.
[SUGGESTION] wsTree declares opts but never reads it
Minor — async function wsTree(subpath, opts = {}): the opts param is dead. Either drop it or wire ?ref=... / pagination through it (not currently exposed by the backend, so just drop).
| }; | ||
| } | ||
|
|
||
| function fail(msg) { console.error(msg); process.exit(1); } |
There was a problem hiding this comment.
[SUGGESTION] Inconsistent error boundary across helper functions
Most command functions use throw new Error(...) for arg validation (e.g. attachInputs, expertInit), then the top-level handler().catch does console.error + exit(1). Good.
But the existing v0.1 entrypoint uses fail() which calls process.exit(1) directly inline (lines 663, 667, 671, 675, 687, etc.).
Both work. The mix is fine but worth a one-line note in the file header so future maintainers don't add a third pattern. The rule of thumb you've already settled into: "throw inside async helpers, exit inside the switch."
| ```bash | ||
| $EDITOR ./medical-config.yml # tweak the template the init step seeded | ||
| node "$SCRIPTS_DIR/grep-api.js" expert:save medical-research-expert ./medical-config.yml | ||
| ``` |
There was a problem hiding this comment.
[SUGGESTION] Skill examples will fail until backend bugs above are fixed
Once you fix the expert:init, expert:save, expert:train, project:upload, project:delete, project:mkdir field-name bugs in grep-api.js, please also update:
skills/grep-experts/SKILL.md— theexpert:save medical-research-expert ./medical-config.ymlworkflow needs to specify a target file name, since the backend saves one file per call. Today's example implies you can dump a whole config. After the fix, the example should be something like:
node "$SCRIPTS_DIR/grep-api.js" expert:save medical-research-expert ./SOP.md SOP.md-
skills/grep-experts/SKILL.md—expert:trainexample needs a TODO/disclaimer (or the skill removed) until the backend has a real ingestion endpoint. -
skills/grep-projects/SKILL.md—project:uploadexamples are accurate as written if the wrapper fix lands; please add a note about how files land atprojects/<name>/<basename>rather than letting the user assume nesting.
The skill files themselves are excellent — clear, agent-oriented description fields ("use when X" rather than "this skill does Y"), good anti-patterns sections, sensible SCRIPTS_DIR resolver. Just needs the wrapper to actually work for the documented examples to be honest.
There was a problem hiding this comment.
Code Review by Claudio-Michel — v0.2 wrappers
Score: 2/5 — significant issues. Needs another round.
Summary
Mon ami, the shape of this PR is excellent — the SKILL.md authoring is some of the best I've seen in this plugin. Descriptions consistently say when to invoke (not what they do), the SCRIPTS_DIR resolver pattern is clean, anti-pattern sections are sharp, and the README/CHANGELOG split is clear. The session-caching simplify pass on commit 9a82355 is also a real win for polling loops.
But the wrapper itself ships seven endpoints with broken request contracts. The new endpoint surface area on feat/workspace-projects-browser was new code without a stable spec yet — and it shows. Almost every wrapper that touches code-storage/workspace/{projects,experts} will return 422 on the very first call, and a couple silently no-op (the worst kind). I verified each against the actual handler signatures on origin/feat/workspace-projects-browser of the parcha repo.
Critical (will 422 / silently no-op on every call)
| # | Function | Bug |
|---|---|---|
| 1 | buildSubmitBody (line 217) |
--language sent as body.language, backend wants body.response_language. Pydantic silently drops it. i18n flag has zero effect today. |
| 2 | expertInit (line 517) |
Sends { expert_name }, backend wants { folder_name }. → 422 |
| 3 | expertSave (line 535) |
Sends { expert_name, config }, backend wants { folder_path, file_name, content, commit_message }. → 422 |
| 4 | expertTrain (line 550) |
Multipart upload, but backend's /experts/{name}/brain accepts no body and just creates brain.json. Files silently dropped, but request returns 200. |
| 5 | projectUpload (line 494) |
Sends project_name form field; backend wants paths[] aligned to files[]. → 422 |
| 6 | projectDelete (line 500) |
Sends ?project_name=&file_path=; backend wants ?path=. → 422 |
| 7 | projectMkdir (line 508) |
Sends query params, backend wants JSON {path} body. → 422 |
Warnings
ws:catreturns JSON-wrapped content, not raw bytes (need&raw=trueon the URL). Skill says "Streams raw bytes to stdout" — currently lying.--action-modeis wired in code but missing from the help text.wsTreedeclaresoptsand never reads it.
Suggestions
- Once wrapper bugs land, the SKILL.md examples in
/grep-experts(expert:saveandexpert:trainworkflows) and/grep-projectsneed updates to match the actual backend behavior. The README "new in 0.2" framing reads great. - Consider a short
scripts/test-wrappers.shsmoke that hits each new endpoint with a tiny fixture againstpreview-api.grep.ai— would catch every one of the bugs above in CI.
What I loved (très bien!)
- Session caching with disk fallback (
_session+saveSessioninvalidation) — perfect for polling loops. encodeRemotePathproperly preserves slashes for FastAPI:pathcatch-alls.buildFileParts+apiMultipartare clean reusable primitives.- The CSV helper for
--custom-skillsand ergonomic dual form forws:diff(positional or named) — small but thoughtful. - The
/researchadvanced-flags table with "use only when the user explicitly asks" guardrail — exactly the right tone for an agent-facing skill. - Structurally additive — auth.js, billing.js, all v0.1 skills untouched. Easy to bisect if 0.2 is rolled back.
- Help text is genuinely useful as a one-screen reference for a future Claude session.
- The existing-skill updates are surgical — no churn in the happy paths of
/research,/quick-research,/ultra-research.
Verdict
CHANGES REQUESTED. The 7 contract bugs would burn every user the moment they tried the documented examples. They're all small, mechanical fixes (rename a JSON field, swap query params for body, etc.) — should be a couple hours of focused work plus a smoke test against preview-api. After that, this PR is ready to ship and will be a meaningful jump in plugin surface area.
Bien cordialement,
Claudio-Michel
Claudio-Michel review caught 7 critical request-contract bugs where the
v0.2 wrappers' payloads / query params didn't match the backend handler
signatures on feat/workspace-projects-browser. Each would 422 on first
use. Plus a few smaller cleanups.
Bugs fixed:
1. submitResearch: body.language -> body.response_language
(ResearchJobInput field is response_language)
2. expertInit: {expert_name} -> {folder_name}
(InitExpertRequest)
3. expertSave: {expert_name, config} -> {folder_path, file_name, content,
commit_message} (SaveExpertFileRequest, one file at a time)
4. expertTrain: dropped multipart body — backend's experts/{name}/brain
takes no body, just creates brain.json server-side (idempotent)
5. projectUpload: dropped {project_name} form field — backend uses
parallel files[]/paths[] arrays; we now derive paths from basenames
prefixed with the project name
6. projectDelete: ?project_name=&file_path= -> ?path=<full_path>
7. projectMkdir: query params -> JSON body {path} (CreateDirectoryRequest)
Warnings fixed:
- wsRead: append &raw=true so the endpoint streams raw bytes instead of
the {path, content, size_bytes} JSON wrapper
- wsTree: dropped dead opts param
- help text: surface --action-mode (was wired in buildResearchOpts but
undocumented)
Skill markdown updates:
- skills/grep-experts/SKILL.md: documents the new save signature
(file_name + local_file), clarifies that train is idempotent and there
is no document-upload endpoint for expert knowledge bases yet
- skills/grep-projects/SKILL.md: clarifies upload basename behavior,
delete/mkdir paths are relative to projects/<name>/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
All 7 critical bugs + 3 warnings fixed in c8a1e14.
Critical (request contracts):
--language→ sendsresponse_language(matchesResearchJobInput)expert:init→{folder_name}(matchesInitExpertRequest)expert:save→{folder_path, file_name, content, commit_message}— now takes one file at a time, signature changed toexpert:save <name> <file_name> <local_file> [--message=...]expert:train→ no body, idempotent. Backendexperts/{name}/brainonly ensuresbrain.json. Updated SKILL.md to clarify there is no document-upload endpoint for expert knowledge bases yet.project:upload→ parallelfiles[]+paths[]arrays (paths ={projectName}/{basename}), noproject_nameform fieldproject:delete→?path=<full_path>(joined from project_name + file_path for ergonomics)project:mkdir→ JSON body{path}(matchesCreateDirectoryRequest)
Warnings:
ws:cat→ appends&raw=trueso the endpoint streams raw bytes- Help text → surfaces
--action-mode wsTree→ dropped deadoptsparam
Skill markdown updated for both /grep-experts and /grep-projects to match new wrapper signatures. Verified node -c scripts/grep-api.js and node scripts/grep-api.js help both clean.
|
@greptile-apps please review — fixed all 7 critical request-contract bugs from the prior review in c8a1e14 |
Summary
Pure additive release. No deletions. Layers wrappers on top of v0.1 for every capability that the soon-to-merge `feat/workspace-projects-browser` branch introduces.
Diff vs main:
+1,800 / -10across 16 files. Existingscripts/auth.js,scripts/billing.js, the 8 existing skills, andbin/install.jsare untouched.What's new
10 new skills
Job management:
/grep-search— search past jobs by status / question text (client-side filter; backend has no full-text search yet)/grep-jobs— quick recent-jobs listing/grep-resume— resume a paused job, optionally with a steering message/grep-stop— pause / cancel a running check; soft-delete a check result/grep-apps— list slidedeck / podcast / narrative artifactsContext + workspace:
/grep-inputs— attach / remove per-job input files (`POST/DELETE /grep/jobs/{id}/inputs`, multipart, 100 MB/file, 500 MB/job)/grep-defaults— manage per-user default context files (`GET/POST/DELETE /grep/user/defaults`, multipart)/grep-workspace— browse the Pierre-backed workspace tree, read files, view commit history & diffs/grep-projects— create + manage SOP-driven projects; drop `SOP.md` into `projects//`, then submit research with `--project=projects/`/grep-experts— initialize, save, and train custom research experts (config + knowledge base trained from documents)`scripts/grep-api.js` extensions
Run `node scripts/grep-api.js help` for the full subcommand list (28 commands now).
Light updates to 5 existing skills
Other
Out of scope (intentionally NOT in this PR)
Endpoints surfaced
Test plan
🤖 Generated with Claude Code