feat(attachments): support uploading files on replies and thread creation#13
Merged
Conversation
…tion Ports the twist-cli attachment feature to comms-cli, backed by the `attachments` client in @doist/comms-sdk 0.4.1. - Bump @doist/comms-sdk 0.3.0 -> 0.4.1 - `src/lib/local-file.ts` (`openLocalFileAsBlob`) reads a path into a file-backed Blob with structured FILE_NOT_FOUND / FILE_READ_ERROR errors - `src/lib/attachments.ts` (`uploadAttachments`) validates every path up front, then uploads each concurrently (order preserved) - `tdc thread reply`, `tdc conversation reply`, and `tdc thread create` gain a repeatable `--file`; a file-only post (no text) is allowed - `conversation reply` preflights the conversation before uploading so an invalid/forbidden target fails before orphaning an upload - `--file` + `--close`/`--reopen` on thread reply -> CONFLICTING_OPTIONS - Add an `attachments.upload` spinner entry so uploads route through `wrapResult` for the 403 -> INSUFFICIENT_SCOPE re-login prompt - Add `attachments:read` / `attachments:write` to the OAuth scopes - Skill content + regenerated SKILL.md; tests for all of the above Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
doistbot
reviewed
May 31, 2026
Member
doistbot
left a comment
There was a problem hiding this comment.
Thanks scottlovegrove for your contribution 😇. The new file attachment feature integrates cleanly with the existing command structures and thoughtfully handles edge cases like preflighting conversations to prevent orphaned uploads.
Few things worth tightening:
- Restore type safety for thread replies by building a separately typed request object rather than casting the payload after the fact.
- Streamline file reads and uploads by using
openAsBlob()directly instead of probing with an initial open/close, and cap the upload concurrency to prevent I/O spikes. - Extend the permissions mock to ensure
attachments.uploadis properly tested through the mutating API path.
I also included a few optional follow-up notes in the details below.
Optional follow-up notes (4)
- [P3] src/commands/thread/create.ts:75: This new
--filedry-run path never validates the local files. For example,tdc thread create CH100 "Title" --file ./missing.png --dry-runprints a preview here, but the real command fails withFILE_NOT_FOUNDonce it reachesuploadAttachments(). Please validate the file paths in dry-run as well (without uploading) so the preview matches actual execution; the same issue exists in the other new--filedry-runs. - [P3] src/lib/options.ts:25:
collect()is now a shared helper, but it only works if every caller remembers to pass[]as the Commander default. If a future repeatable option forgets that,previousisundefinedand[...previous, value]throws at runtime. Make it defensive (previous: string[] = []orprevious ?? []) so the helper’s contract is enforced in code instead of comments. - [P3] src/commands/conversation/conversation.test.ts:863: The PR description highlights that
conversation replypreflights the conversation to prevent orphaned uploads on invalid targets. Consider adding a test whereclient.conversations.getConversationrejects (e.g., 404 Not Found) to verify thatclient.attachments.uploadis indeed skipped. - [P3] src/lib/local-file.test.ts:34: This only covers the
FILE_NOT_FOUNDbranch. The helper also adds a separateFILE_READ_ERRORpath for unreadable/non-file inputs, and that structured error is easy to lose without a test. Please add one deterministic non-ENOENT case here (for example a mockedEACCES/EPERMfailure).
- thread reply: build a `satisfies`-typed createComment request so the `attachments` contract is compiler-checked again; only `recipients` (EVERYONE sentinels) keeps the assertion - local-file: try `openAsBlob` first (happy path = one open, no TOCTOU); fall back to an `fs.open` probe only on failure to recover the real errno (openAsBlob masks it as ERR_INVALID_ARG_VALUE) - attachments: cap upload concurrency (4) while preserving input order - options: `collect` defaults `previous` to `[]` so the contract holds even if a caller forgets the Commander default - dry-run now validates attachment paths (without uploading) so the preview fails on a bad path exactly as a real run would - tests: attachments.upload exercised through the mutating write-guard (incl. read-only block), local-file FILE_READ_ERROR branch, and a conversation preflight-failure-skips-upload case Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
doist-release-bot Bot
added a commit
that referenced
this pull request
May 31, 2026
## [1.4.0](v1.3.2...v1.4.0) (2026-05-31) ### Features * **attachments:** support uploading files on replies and thread creation ([#13](#13)) ([44dc3b7](44dc3b7))
Contributor
|
🎉 This PR is included in version 1.4.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
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.
What
Ports the twist-cli file-attachment feature to comms-cli, using the
attachmentsclient added in@doist/comms-sdk0.4.1.How
@doist/comms-sdk0.3.0 → 0.4.1.src/lib/local-file.ts—openLocalFileAsBlobresolves a path, verifies readability, returns a file-backedBlob; structuredFILE_NOT_FOUND/FILE_READ_ERROR.src/lib/attachments.ts—uploadAttachments(files)validates every path before uploading any (no orphaned uploads), then uploads each concurrently (Promise.all, order preserved).--fileis repeatable onthread reply,conversation reply, andthread create; a file-only post with no text is allowed (skips the editor prompt).conversation replypreflights the conversation (getConversation) before uploading so an invalid/forbidden target fails before orphaning an upload.--file+--close/--reopenonthread reply→CONFLICTING_OPTIONS.Auth / scopes (403 handling)
Uploading needs the new
attachments:writescope.attachments.uploadgets a spinner entry so it routes throughwrapResult; a token from before the scope change gets a 403 that's translated toINSUFFICIENT_SCOPEwith thetdc auth loginre-login prompt. Addedattachments:read/attachments:writeto the OAuth scope lists (read-write stays a superset of read-only).Verification
type-check, full suite (722 tests, incl. newlocal-file/attachmentsunit tests,--filesuites for all three surfaces, and anattachments.upload403 →INSUFFICIENT_SCOPEtest), oxlint + oxfmt, andcheck:skill-syncall pass.Mirrors Doist/twist-cli#260.
🤖 Generated with Claude Code