feat(channel): channel membership control (members list/add/remove/set)#7
Merged
Conversation
Port Doist/twist-cli#244 to comms-cli. Adds `tdc channel members`: - list: members + groups fully present in the channel - add/remove: users and/or `group:<ref>` (one-shot expansion at call time) - set: replace membership with the resolved set, dry-run by default (--apply to mutate), refuses to remove the acting user unless --include-self Also adds resolveChannelMemberRefs (mixed user/group: ref parsing with dedup + input-order preservation), addUsersToChannel/removeUsersFromChannel API wrappers, and channels:write/channels:remove to READ_WRITE_SCOPES. Existing logged-in users must re-run `tdc auth login` to pick up the new scopes, otherwise channel mutations fail with INSUFFICIENT_SCOPE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
doistbot
reviewed
May 27, 2026
Member
doistbot
left a comment
There was a problem hiding this comment.
Thanks scottlovegrove for your contribution 😎 👊. The new channel membership mutation commands are well-structured and nicely adapt the twist-cli logic for comms-cli.
Few things worth tightening:
- Correctness: Watch out for a resolver mapping bug with comma-separated user refs that can silently drop assignments, and ensure dry-run mode outputs valid JSON when
--jsonis passed. - Efficiency: Member listing and group expansions currently fetch the entire workspace user/group directories sequentially; we should batch group lookups and only fetch the specific users needed.
- Testing & cleanup: Expand the test suite to cover interleaved user/group arrays and the
--apply --jsoncontract, and remove some unreachable "sparing self" dead code inset.ts.
I also included a few optional follow-up notes in the details below.
Optional follow-up notes (4)
- [P3] src/commands/channel/set.ts:57:
printDryRun()always ends withRun without --dry-run to execute.. On the defaultsetpreview path that's incorrect: rerunning without--dry-runstill won't mutate unless--applyis passed. Please use a custom preview message here or override the footer so the next step isn't misleading. - [P3] src/commands/channel/set.ts:85: When
--fullis used,setmerges CLI result metadata (added,removed, etc.) into the API'supdatedchannel object. This diverges from howaddandremovehandle--fullinmembership-helpers.ts(and theaway.tsreference implementation), which both log the unmodified API entity. Consider standardizing the behavior by usingconsole.log(formatJson(updated, 'channel', true))here to match. - [P3] src/commands/channel/members.test.ts:8: The
--fullJSON output branch for mutative commands (add,remove,set) makes an API call (client.channels.getChannel) which is currently untested. If a test triggered this branch, it would crash because thegetCommsClientmock here returnsundefined. Consider updating this mock to return{ channels: { getChannel: vi.fn() } }and adding a test case that verifies the--fullexpanded payload. - [P3] src/lib/skills/content.ts:236: This file is installed into AI agent skill directories, so the new
alice/a@d.comexamples push agents toward PII-based refs. The Internal AI Tools standard says AI data flows should prefer user IDs over emails/names where possible, so please switch these new examples (and the adjacent prose in this section) toid:-style refs by default and mention email/name only as optional human-facing inputs. https://handbook.doist.com/doc/standard-internal-ai-tools-zU0fqnhpmC
- refs.ts: resolve each user slot individually so a comma/multi-match ref can't shift index mapping and silently drop users (P1); fetch the workspace group list once for name refs instead of per-ref (P2). - set.ts: emit a JSON object in dry-run when --json (was printing text, breaking parsers) (P1); drop the unreachable self-sparing ternary (the earlier guard already throws) (P2). - membership-helpers.ts: fetchUsersByIds resolves members via per-id getUserById (mirrors groups/view) instead of downloading the whole workspace directory (P2). - tests: rename the misleading "sparing self" case; add set --apply --json and dry-run --json coverage; add an interleaved multi-group refs test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
doist-release-bot Bot
added a commit
that referenced
this pull request
May 27, 2026
## [1.1.0](v1.0.0...v1.1.0) (2026-05-27) ### Features * **channel:** channel membership control (members list/add/remove/set) ([#7](#7)) ([2a1e473](2a1e473)), closes [Doist/twist-cli#244](Doist/twist-cli#244)
Contributor
|
🎉 This PR is included in version 1.1.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
6 tasks
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.
Summary
Ports Doist/twist-cli#244 to comms-cli. Adds per-user and per-group membership control under
tdc channel:tdc channel members add general luis@doist.com group:Design id:789.group:<ref>is one-shot expansion — adds the group's current members at call time; the group is not persistently linked, and users added to the group later will not auto-join. Surfaced in--helpforadd/set.setreplaces membership with the resolved set. Dry-run by default;--applyto mutate. Refuses to remove the acting user unless--include-selfis also passed.Files
src/lib/refs.tsresolveChannelMemberRefs—group:expansion, dedup, input-order preservation; resolves users + groups concurrentlysrc/lib/api.tsaddUsersToChannel/removeUsersFromChannel+ spinner messagessrc/lib/auth-provider.tschannels:write+channels:removetoREAD_WRITE_SCOPESsrc/commands/channel/membership-helpers.tsmutateChannelMembership,fetchUsersByIds,logExpansion,groupsFullyInChannelsrc/commands/channel/{members,add,remove,set}.tssrc/commands/channel/index.tsmemberscommand group;listis{ isDefault: true }src/lib/skills/content.ts+ regeneratedskills/comms-cli/SKILL.mdsrc/commands/channel/members.test.ts(12),src/lib/refs.test.ts(+4)Adaptations from the twist-cli source
comms-cli diverges from twist-cli, so this is not a 1:1 copy:
idis a base58 string, not numeric;addUsersToChannel/removeUsersFromChanneltakeid: string.Group.idis a string →expandedFrom.groupId: string.WorkspaceUser.fullName(comms) instead of.name(twist).fetchUsersByIdsresolves members viagetWorkspaceUsers(workspaceId)instead of twist'sclient.batch(...).getCommsClient/tdcthroughout.Caveat to verify
Scope names (
channels:write/channels:remove) are assumed to mirror the existing groups scopes. The upstream PR found them missing in twist's scope list; please confirm the comms backend accepts them with a live mutation before merge. Existing logged-in users must re-runtdc auth loginto pick up the new scopes, otherwise channel mutations fail withINSUFFICIENT_SCOPE.Test plan
npm run type-checknpm run lintmembers.test.ts12,refs.test.ts+4)npm run sync:skill(SKILL.md regenerated)--helpsmoke test of thememberscommand tree🤖 Generated with Claude Code