diff --git a/docs/commands.md b/docs/commands.md index 3609ba3..85acc01 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1363,3 +1363,11 @@ it closes open opportunities unless `--no-cascade` is supplied. A positional End the campaign first with `dm mail campaigns end --yes`. Archiving an active, paused, ready or draft campaign returns `409 CAMPAIGN_NOT_ENDED` without changing it. `dm mail campaigns archive --yes` cancels remaining unsent mail, releases reserved funds, and stops mail-attention reminders while keeping campaign history. Confirm the cancellation before passing `--yes`. Use `dm mail campaigns list --status archived` to view archived campaigns. + + +### Include and exclude prospect tags together + +`dm prospects list --include-tags tag_3,tag_4 --exclude-tags tag_9` keeps +prospects with tag 3 or 4 and no tag 9. Both flags accept up to 50 tag IDs. +Exclusion wins when a prospect matches both sets. The existing `--tag` flag +continues to narrow by one tag and can be combined with these flags. diff --git a/src/commands/prospects.ts b/src/commands/prospects.ts index 470c3e6..4729a58 100644 --- a/src/commands/prospects.ts +++ b/src/commands/prospects.ts @@ -107,6 +107,8 @@ export async function prospectsList(options: { favorites?: boolean; list?: string; tag?: string; + includeTags?: string; + excludeTags?: string; search?: string; sort?: string; page?: string; @@ -127,6 +129,8 @@ export async function prospectsList(options: { favorites_only: options.favorites ? 'true' : undefined, list_id: options.list, tag_id: options.tag, + include_tag_ids: options.includeTags, + exclude_tag_ids: options.excludeTags, search: options.search, sort: options.sort, page: options.page ? parseInt(options.page, 10) : undefined, diff --git a/src/index.ts b/src/index.ts index fdfe7ee..6e08adb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1654,6 +1654,8 @@ prospectsCmd .option('--favorites', 'Only starred prospects') .option('--list ', 'Only members of this list') .option('--tag ', 'Only prospects with this tag, e.g. tag_31') + .option('--include-tags ', 'Match any of these comma-separated tag IDs') + .option('--exclude-tags ', 'Exclude any of these comma-separated tag IDs at the same time') .option('--search ', 'Address words for properties; name or city for people') .option('--sort ', 'newest (default) or oldest') .option('-p, --page ', 'Page number') diff --git a/tests/commands/prospects.test.ts b/tests/commands/prospects.test.ts index c656fa4..bca8363 100644 --- a/tests/commands/prospects.test.ts +++ b/tests/commands/prospects.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { prospectsArchive, prospectsReactivate, prospectsGet } from '../../src/commands/prospects.js'; +import { prospectsList, prospectsArchive, prospectsReactivate, prospectsGet } from '../../src/commands/prospects.js'; vi.mock('../../src/lib/config.js', () => ({ readConfig: () => ({ apiKey: 'fixture-only', apiEnvironment: 'staging' }), @@ -188,3 +188,12 @@ describe('dm prospects list attribution filters', () => { }); }); }); + + +it('sends included and excluded tags in the same prospects request', async () => { + fetchMock.mockResolvedValue(jsonResponse(200, { data: [], pagination: { page: 1, per_page: 25, total: 0, has_more: false } })); + await prospectsList({ includeTags: 'tag_3,tag_4', excludeTags: 'tag_9', json: true }); + const params = new URL(String(fetchMock.mock.calls[0][0])).searchParams; + expect(params.get('include_tag_ids')).toBe('tag_3,tag_4'); + expect(params.get('exclude_tag_ids')).toBe('tag_9'); +});