Conversation
There was a problem hiding this comment.
Pull request overview
Adds a fuzzy reference-resolution layer so users can refer to documents/collections by ID, URL slug, exact name, or unique partial name (with clearer errors for ambiguity), and wires it into the CLI commands.
Changes:
- Added
src/lib/refs.tswith generic fuzzy resolver plus document/collection helpers. - Updated document/collection commands to accept
<ref>(ID/URL/name) and resolve to IDs before API calls. - Added unit tests for the new resolver logic in
src/lib/refs.test.ts.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
src/lib/refs.ts |
Implements fuzzy reference resolution and error messaging for documents/collections. |
src/lib/refs.test.ts |
Adds Vitest coverage for ID/slug/name/partial resolution behavior. |
src/commands/document.ts |
Switches document commands to use fuzzy resolvers (document + collection refs). |
src/commands/collection.ts |
Switches collection commands to use fuzzy resolvers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async () => { | ||
| const { data } = await apiRequest<Collection[]>("collections.list", { | ||
| limit: 100, | ||
| }); | ||
| return data; |
There was a problem hiding this comment.
collections.list is hard-coded to limit: 100 during name resolution. In workspaces with >100 collections this can produce false “not found”/ambiguity results. Consider paginating (using offset/pagination.nextPath) until completion.
| .action(async (id: string, opts) => { | ||
| const resolvedId = await resolveDocumentId(id); | ||
| const { data } = await apiRequest<Document>("documents.info", { | ||
| id: resolveId(id), | ||
| id: resolvedId, | ||
| }); |
There was a problem hiding this comment.
resolveDocumentId() may call documents.info internally, and this handler then calls documents.info again with the resolved ID. This creates redundant requests/spinners for ID/URL inputs. Consider using resolveDocumentRef() here (and only fetching documents.info if the resolver returned a list item without text), or refactor the resolver to return both the resolved ID and (optionally) the fetched document.
| .action(async (id: string) => { | ||
| const resolvedId = await resolveDocumentId(id); | ||
| const { data } = await apiRequest<Document>("documents.info", { | ||
| id: resolveId(id), | ||
| id: resolvedId, | ||
| }); |
There was a problem hiding this comment.
resolveDocumentId() may already perform a documents.info lookup, and open then performs another documents.info call. This can be avoided by reusing the document returned by a resolveDocumentRef()-style resolver, reducing latency and API load for URL/ID inputs.
| .action(async (id: string, opts) => { | ||
| const { data } = await apiRequest<Collection>("collections.info", { id }); | ||
| const resolvedId = await resolveCollectionId(id); | ||
| const { data } = await apiRequest<Collection>("collections.info", { | ||
| id: resolvedId, | ||
| }); |
There was a problem hiding this comment.
resolveCollectionId() can call collections.info internally, and this get handler then calls collections.info again with the resolved ID, doubling requests for ID inputs and potentially tripling for name inputs that “look like” IDs. Consider using resolveCollectionRef() directly when possible, or refactor the resolver to return both the resolved ID and fetched entity to avoid duplicate lookups.
| const exactMatch = items.find( | ||
| (item) => getName(item).toLowerCase() === refLower, | ||
| ); | ||
| if (exactMatch) return exactMatch; | ||
|
|
There was a problem hiding this comment.
Exact name matching uses find, so if multiple documents/collections have the same name/title (case-insensitive) the resolver will silently pick the first match and potentially operate on the wrong entity. Treat multiple exact matches as ambiguous (similar to partial matches) and include suggestions.
| const exactMatch = items.find( | |
| (item) => getName(item).toLowerCase() === refLower, | |
| ); | |
| if (exactMatch) return exactMatch; | |
| const exactMatches = items.filter( | |
| (item) => getName(item).toLowerCase() === refLower, | |
| ); | |
| if (exactMatches.length === 1) { | |
| return exactMatches[0]; | |
| } | |
| if (exactMatches.length > 1) { | |
| const suggestions = formatSuggestions( | |
| exactMatches, | |
| getName, | |
| (item) => item.id, | |
| ); | |
| throw new Error( | |
| `Ambiguous ${entityType} reference "${ref}". Did you mean:\n${suggestions}`, | |
| ); | |
| } |
| const suggestions = formatSuggestions( | ||
| partialMatches, | ||
| getName, | ||
| (item) => item.id, | ||
| ); | ||
| throw new Error( | ||
| `Ambiguous ${entityType} reference "${ref}". Did you mean:\n${suggestions}`, | ||
| ); |
There was a problem hiding this comment.
Ambiguity suggestions always display item.id. For documents, the CLI generally surfaces urlId (see src/commands/document.ts formatting), so showing the internal id here can be confusing. Consider making resolveRef accept a getDisplayId callback so documents can suggest urlId while collections suggest id.
| // Try direct ID lookup first if it looks like an ID | ||
| const extractedId = extractDocumentId(ref); | ||
| if (looksLikeId(extractedId)) { | ||
| try { | ||
| return await fetchById(extractedId); |
There was a problem hiding this comment.
resolveRef always runs extractDocumentId(ref) before ID lookup, even when resolving collections. This can transform non-document references (e.g., names containing hyphens) into a different string for the initial lookup, causing unnecessary failed API calls and potentially incorrect resolution if an entity’s ID happens to match the extracted suffix. Consider making the “extract slug id” step optional/per-entity (documents only).
| // Try direct ID lookup first if it looks like an ID | |
| const extractedId = extractDocumentId(ref); | |
| if (looksLikeId(extractedId)) { | |
| try { | |
| return await fetchById(extractedId); | |
| // Try direct ID lookup first if it looks like an ID. | |
| // Only apply slug/document-ID extraction for document-like entity types | |
| // to avoid corrupting non-document references (e.g. collection names). | |
| const maybeDocType = entityType.toLowerCase(); | |
| const idCandidate = | |
| maybeDocType.startsWith("document") ? extractDocumentId(ref) : ref; | |
| if (looksLikeId(idCandidate)) { | |
| try { | |
| return await fetchById(idCandidate); |
| async () => { | ||
| const { data } = await apiRequest<Document[]>("documents.list", { | ||
| limit: 100, | ||
| }); | ||
| return data; |
There was a problem hiding this comment.
documents.list is hard-coded to limit: 100 during name resolution. In workspaces with >100 documents this can produce false “not found”/ambiguity results. Consider paginating until completion (using offset/pagination.nextPath) or using documents.search to narrow results.
Add a new refs module that resolves document and collection references by: - Exact ID (UUID or short alphanumeric format) - URL slug extraction (e.g., "my-doc-abc123" -> "abc123") - Exact name match (case-insensitive) - Partial name match (if only one result matches) - Helpful error messages with suggestions for ambiguous matches Update document and collection commands to use the new resolvers, allowing users to reference items by name instead of just ID. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add pagination to documents.list and collections.list to support workspaces with >100 items - Treat multiple exact name matches as ambiguous instead of silently picking first - Show urlId instead of internal id in ambiguous document suggestions - Make slug extraction document-only to prevent incorrect collection name transformations - Reuse resolved document/collection objects in get commands to avoid redundant API calls Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
9f2d26f to
2d5df01
Compare
# 1.0.0 (2026-03-25) ### Bug Fixes * broaden CI detection to handle all truthy values ([7dc2806](7dc2806)) * exclude dist from biome checks ([0dc49a3](0dc49a3)) ### Features * add API Spinner Proxy ([#17](#17)) ([a3bc75a](a3bc75a)) * add fuzzy reference resolution ([#19](#19)) ([6c1c6f9](6c1c6f9)) * add npm publishing, new agent skills, and skill auto-update ([#36](#36)) ([bcb2e75](bcb2e75)), closes [Doist/todoist-cli#176](Doist/todoist-cli#176) [Doist/bob-cli#17](Doist/bob-cli#17) [Doist/twist-cli#101](Doist/twist-cli#101) * add structured error formatting with codes and hints ([#18](#18)) ([17658d2](17658d2)) * implement OAuth PKCE browser login ([#20](#20)) ([b7f6eec](b7f6eec)), closes [#7](#7) [outline/outline#11254](outline/outline#11254) * improve oauth login inputs and callback UX ([#31](#31)) ([3011b28](3011b28))
|
🎉 This PR is included in version 1.0.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Summary
src/lib/refs.tsmodule with functions to resolve document and collection references by:src/lib/refs.test.tsCloses #10
Test plan
npm run type-check- passesnpm test- all 50 tests pass🤖 Generated with Claude Code