feat(server): GET /api/discover — find directories with existing Claude Code history (#745) - #799
Merged
Merged
Conversation
…de Code history (#745) The server half of Discover. Two read-only, instance-level endpoints: GET /api/discover ranked candidate directories + counts GET /api/discover/sessions?dir=… one directory's sessions, for lazy expansion No UI — a separate change builds that against this API. ## The heuristic is the feature A naive scan is unusable: on a real developer machine it surfaces ~166 transcript folders, ~150 of them ephemeral temp-dir sessions, plus `/`, `~/Downloads` and `/tmp`. `discover.ts` recovers each folder's RECORDED cwd (never by inverting the lossy encoded folder name) and applies the rules cheap-to-expensive, so the ~150 die on string comparisons before anything reads a transcript: no-recorded-cwd, missing, system-path (the #720 floor, on both the resolved and the as-written spelling), temp-root, paddock-internal, home-root, outside-home, already-managed, no-git, no-sessions. Survivors are ranked by non-noise session count then recency. `includeNonGit=1` / `includeOutsideHome=1` relax the two soft rules — which is how #745's open "require .git, or rank it first?" question is answered: require by default, keep the notebook case reachable. `excluded` reports what each rule ate, so a container that legitimately finds nothing can say why rather than render a blank page. ## A new boundary, not a loosened one `POST …/adopt-chats` deliberately 400s a `sourceCwd` its project does not offer, "rather than an invitation to scan arbitrary directories". That is untouched. Discovery's own containment: the only paths it will ever read are ones a transcript folder already records AND that clear the same path floor a linked project must, so `?dir=` is a lookup into a computed set rather than a path parameter. Nothing walks or globs the filesystem. Steps 3–4 of the flow need no new code, and an integration test asserts that rather than assuming it: the reported `path` IS what the created project's `workingDir` becomes, so `POST /api/projects` + `POST …/adopt-chats` with a `sessionIds` subset work unchanged, and the imported directory drops out of the next scan. Reuses `adoptable.ts`'s primitives (`recordedCwd`, `filterReasonFor`, the mtime-cached folder scan with its #620 legacy mirroring) rather than reimplementing them, so a Discover row's count and the import dialog's offer are the same number by construction. 32 unit tests over a fixture tree + 7 integration tests over the real app. Co-Authored-By: Claude <noreply@anthropic.com>
Deploying paddock with
|
| Latest commit: |
7cd0267
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://80ef28cf.paddock-7u2.pages.dev |
| Branch Preview URL: | https://feat-745-discover-api.paddock-7u2.pages.dev |
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The server half of #745. Two read-only, instance-level endpoints and the
heuristic behind them, with tests. No UI — a separate change builds the
Discover view against this API.
--here(#798) is untouched.Endpoints
A candidate carries
path(resolved — and therefore exactly what the createdproject's
workingDirbecomes),recordedPathwhen the transcripts spell itdifferently,
name,suggestedSlug,hasGit,gitRemote,insideHome,sessionCount,filteredCount,lastSessionAt. A session carriessessionId/mtime/preview/autoName/sizeBytes— the same shapeGET …/adoptable-chatsreturns, soAdoptChatsModal's idiom transfers andsessionIdfeeds straight intoadopt-chats'ssessionIds.Why two paths, not one with
?dir=#745 sketches a single
/api/discoverthat changes shape whendiris present.Paddock publishes an OpenAPI 3 document generated from these Fastify route
schemas, and one path cannot describe two different 200 bodies there — it
would have to be declared shapeless, and the published contract would silently
stop describing the endpoint. The two also fail differently: the listing cannot
400, the expansion must.
The heuristic
A naive scan is unusable — ~166 transcript folders on a real machine, ~150
ephemeral, plus
/,~/Downloads,/tmp. Rules run cheap-to-expensive so the~150 die on string comparisons before anything reads a transcript:
no-recorded-cwd— no cwd recoverable from any transcriptmissing— gone, or not a directorysystem-path—/+ the #720 denylist, on both the resolved and the as-written spelling (/proc/self/cwd)temp-root—/tmp,/var/folders,$TMPDIR… the ~150paddock-internal— inside the projects root, data dir, or either Claude homehome-root—$HOMEitselfoutside-homeincludeOutsideHome=1)already-managed— overlaps an existing project's cwd, either directionno-git— onestatincludeNonGit=1)no-sessions— nothing survives the noise filterThen: group by resolved real path (one directory, however many transcript
folders record it), rank by non-noise session count → recency → path.
excludedreports how many directories each rule ate, which is what lets acontainer that legitimately finds nothing say why instead of rendering blank,
and lets the UI offer the soft toggles only when they'd reveal something.
Reuses
adoptable.tsrather than reimplementing it —recordedCwd,filterReasonFor,MIN_TRANSCRIPT_BYTES, and the mtime-cached folder scan withits #620 legacy mirroring (without which Discovery would find nothing on a
laptop). A row's count and the import dialog's offer are therefore the same
number by construction. The recorded cwd is always read from a transcript;
encodePathForCliis never inverted.Security
POST …/adopt-chatsis not loosened. Its 400 on an unrecognisedsourceCwdis untouched. This is a separate surface with its own containment:
<claudeHome>/projects/— nothing walks, lists or globs the filesystem;repois mutable and unvalidated — relocates workingDir to a nonexistent dir and bricks the project #718–PATCH /api/projects/:slug persists arbitrary unknown body keys verbatim into project.yaml (unbounded) #721),so Discovery can never propose a row that
POST /api/projectsthen refuses;?dir=re-derives that set and refuses anything outside it — a lookup into acomputed allow-list, not a path parameter.
Exposure noted in the module header: the listing reveals which directories this
machine's user has run Claude Code in. Narrower than a file browser, but new.
Answers to #745's open questions
.git, or rank it first? Require by default (it is the rule thatdoes the work),
includeNonGit=1to relax, always report the withheld count —so the notebook case stays reachable without a page of
~/Downloads.suggestedSlugis
slugify'd, qualified by the parent directory, and unique against bothexisting project slugs and the rest of the result set.
POST /api/projectsalready takes an arbitrarypath.Tests
32 unit (
test/unit/discover.test.ts) — a table over a real fixture tree,one case per rule, plus soft-rule relaxation, cheap-before-expensive ordering
(asserted by watching which directories cost a session read), folder grouping +
session de-dup, symlink resolution, ranking, noise accounting, slug collisions,
and five cases on the
?dir=boundary.7 integration (
test/integration/discover.test.ts) — the real app, realtranscripts in a real second Claude home. The last one asserts #745's claim that
only steps 1–2 are new, rather than assuming it: discover →
POST /api/projects→
POST …/adopt-chatswith asessionIdssubset → the chat lists → thedirectory drops out of the next scan.
Full server suite: 160 files / 2085 tests green, typecheck clean.
Closes nothing — #745 stays open for the UI half.