From 6669018251c928a3b53b9f4cd24b2e350c5e4391 Mon Sep 17 00:00:00 2001 From: David Lecko Date: Tue, 3 Nov 2026 11:34:33 -0600 Subject: [PATCH 1/4] Explain that mail campaigns must end before archiving --- docs/commands.md | 2 ++ src/commands/mail.ts | 2 +- src/index.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 5eb8006..7f65e8a 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1359,4 +1359,6 @@ it closes open opportunities unless `--no-cascade` is supplied. A positional ## Archive a mail campaign +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. diff --git a/src/commands/mail.ts b/src/commands/mail.ts index 0dea994..e6f486d 100644 --- a/src/commands/mail.ts +++ b/src/commands/mail.ts @@ -844,7 +844,7 @@ export async function mailCampaignsArchive( if (!options.yes) { if (isNonInteractive(options)) { console.error( - `Archiving a campaign is permanent: unsent mail is cancelled, reserved funds released and reminders stopped. Mailing history is kept. Get the user's approval, then rerun dm mail campaigns archive ${id} with --yes.` + `End the campaign before archiving it. Archiving is permanent: unsent mail is cancelled, reserved funds released and reminders stopped. Mailing history is kept. Get the user's approval, then rerun dm mail campaigns archive ${id} with --yes.` ); process.exitCode = 1; return; diff --git a/src/index.ts b/src/index.ts index 3f1db44..94e20d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2444,7 +2444,7 @@ Mail already printed still delivers. To reuse its steps, create a new campaign.` mailCampaignsCmd .command('archive ') - .description('Archive a campaign and cancel its unsent mail') + .description('Archive an ended campaign and cancel its unsent mail') .option('--yes', 'Skip the confirmation (required for agents, scripts and --json)') .option('--json', 'Output as JSON') .addHelpText( From 27a6bf99e450cc8955bffb167b736a99a8d82890 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Tue, 3 Nov 2026 15:18:25 -0500 Subject: [PATCH 2/4] fix(exports): support phone filtering and credit errors on list exports --- docs/commands.md | 7 ++++--- src/commands/lists.ts | 4 +++- src/index.ts | 3 ++- src/lib/client.ts | 3 +++ tests/commands/publicApiExtensions.test.ts | 10 +++++++++- tests/lib/client.test.ts | 18 ++++++++++++++++++ 6 files changed, 39 insertions(+), 6 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 7f65e8a..3609ba3 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -850,17 +850,18 @@ dm lists remove list_abc123 --ids 111,222,333 #### `dm lists export ` -Export list items. Credits are charged per record. +Export items from a completed list. A person-anchored export uses credits for distinct people your organization has not already unlocked this cycle. ```bash -dm lists export list_abc123 +dm lists export list_abc123 --require-phone dm lists export list_abc123 --fields "full_address,estimated_value,owner_name" --anchor property ``` | Option | Description | | ----------------- | ---------------------------------------- | | `--fields ` | Comma-separated list of fields to export | -| `--anchor ` | `property` or `person` | +| `--anchor ` | `property` or `person` (default) | +| `--require-phone` | Only export people with a phone number | --- diff --git a/src/commands/lists.ts b/src/commands/lists.ts index 9f71018..e049dc0 100644 --- a/src/commands/lists.ts +++ b/src/commands/lists.ts @@ -486,7 +486,7 @@ export async function listsRemove( export async function listsExport( listId: string, - options: { fields?: string; anchor?: string; json?: boolean } + options: { fields?: string; anchor?: string; requirePhone?: boolean; json?: boolean } ): Promise { const requestBody: Record = {}; if (options.fields) { @@ -496,6 +496,8 @@ export async function listsExport( requestBody.anchor = options.anchor; } + if (options.requirePhone && options.anchor !== 'property') requestBody.require_phone = true; + const spinner = createSpinner('Starting export...').start(); const data = await apiRequest<{ data: { export_id: string; status: string; record_count: number } }>( `/lists/${listId}/export`, diff --git a/src/index.ts b/src/index.ts index 94e20d8..fdfe7ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1032,7 +1032,8 @@ Examples: listsCmd .command('export ') - .description('Export list items (credits charged per record)') + .description('Export items from a completed list') + .option('--require-phone', 'Only export people with a phone number') .option('--fields ', 'Comma-separated list of fields to export') .option('--anchor ', 'Anchor type: property or person') .option('--json', 'Output as JSON') diff --git a/src/lib/client.ts b/src/lib/client.ts index 22bd656..0a49c96 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -122,6 +122,9 @@ export async function apiRequest( console.error( chalk.dim(' Add capacity in Billing settings or remove prospects, then retry.') ); + } else if (response.status === 402 && error.error?.code === 'export_limit_exceeded' && + typeof error.error.details?.required_credits === 'number') { + console.error(chalk.dim(' Check your data credit balance with dm usage before retrying.')); } else if (response.status === 402) { console.error(chalk.dim(' Your plan does not cover this request. Check Billing settings.')); } else if (response.status === 422) { diff --git a/tests/commands/publicApiExtensions.test.ts b/tests/commands/publicApiExtensions.test.ts index 352d928..bf0d0a5 100644 --- a/tests/commands/publicApiExtensions.test.ts +++ b/tests/commands/publicApiExtensions.test.ts @@ -44,7 +44,7 @@ import { import { addressesAutocomplete, locationsAutocomplete } from '../../src/commands/locations.js'; import { drivingGet, drivingList } from '../../src/commands/driving.js'; import { prospectsList } from '../../src/commands/prospects.js'; -import { listsItems } from '../../src/commands/lists.js'; +import { listsExport, listsItems } from '../../src/commands/lists.js'; import { phonesDnc } from '../../src/commands/phones.js'; import { comps } from '../../src/commands/comps.js'; @@ -487,6 +487,14 @@ describe('CLI public API extensions', () => { } ); + it('sends the phone requirement on a person-anchored list export', async () => { + mockApiRequest.mockResolvedValue({ data: { export_id: 'export-1' } }); + await listsExport('list_1', { requirePhone: true, json: true }); + expect(mockApiRequest).toHaveBeenCalledWith('/lists/list_1/export', { + method: 'POST', body: { require_phone: true }, + }); + }); + it('keeps numeric list person IDs and uses opaque IDs only when the numeric ID is unavailable', async () => { mockApiRequest.mockResolvedValue({ data: [ diff --git a/tests/lib/client.test.ts b/tests/lib/client.test.ts index eb8074d..e16ef5f 100644 --- a/tests/lib/client.test.ts +++ b/tests/lib/client.test.ts @@ -81,3 +81,21 @@ it('names each rejected field when the API returns a validation error', async () 'status: status must be one of draft, ready, active, paused, ended, completed, cancelled' ); }); + +it('shows the list export credit shortfall and points to the balance', async () => { + readConfig.mockReturnValue({ apiKey: 'stored-account-key' }); + const message = 'This export requires 12 data credits. You have 8 and need 4 more.'; + vi.stubGlobal('fetch', async () => Response.json({ + error: { + code: 'export_limit_exceeded', message, + details: { required_credits: 12, remaining_credits: 8, reason: 'insufficient_for_request' }, + }, + }, { status: 402 })); + const error = vi.spyOn(console, 'error').mockImplementation(() => {}); + vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('exit'); }); + await expect(apiRequest('/lists/example/export', { method: 'POST', body: {} })).rejects.toThrow('exit'); + const output = error.mock.calls.flat().join(' '); + expect(output).toContain(message); + expect(output).toContain('dm usage'); + expect(output).not.toContain('plan does not cover'); +}); From 08dc6872ae511cad5882e89acc5b9242a7ead042 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Tue, 3 Nov 2026 15:35:10 -0500 Subject: [PATCH 3/4] fix(exports): display the returned list export ID --- src/commands/lists.ts | 5 +++-- tests/commands/publicApiExtensions.test.ts | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/commands/lists.ts b/src/commands/lists.ts index e049dc0..896b906 100644 --- a/src/commands/lists.ts +++ b/src/commands/lists.ts @@ -499,7 +499,8 @@ export async function listsExport( if (options.requirePhone && options.anchor !== 'property') requestBody.require_phone = true; const spinner = createSpinner('Starting export...').start(); - const data = await apiRequest<{ data: { export_id: string; status: string; record_count: number } }>( + type ExportResult = { export_id: string; status: string; record_count: number }; + const data = await apiRequest( `/lists/${listId}/export`, { method: 'POST', body: requestBody } ); @@ -510,7 +511,7 @@ export async function listsExport( return; } - const e = data.data; + const e = 'data' in data ? data.data : data; printHeader('Export Started'); printKeyValue({ 'Export ID': e.export_id, diff --git a/tests/commands/publicApiExtensions.test.ts b/tests/commands/publicApiExtensions.test.ts index bf0d0a5..b5c8497 100644 --- a/tests/commands/publicApiExtensions.test.ts +++ b/tests/commands/publicApiExtensions.test.ts @@ -14,6 +14,7 @@ vi.mock('../../src/lib/output.js', () => ({ parseRequestBody: mockParseRequestBody, printJson: vi.fn(), printHeader: vi.fn(), + printKeyValue: vi.fn(), printTable: vi.fn(), printPagination: vi.fn(), printTotals: vi.fn(), @@ -30,7 +31,7 @@ vi.mock('../../src/lib/output.js', () => ({ })), })); -import { printJson, printTable } from '../../src/lib/output.js'; +import { printJson, printKeyValue, printTable } from '../../src/lib/output.js'; import { peopleGet, peopleIds, peopleSearch, peopleExport } from '../../src/commands/people.js'; import { propertiesGet, propertiesIds, propertiesSearch, propertiesExport } from '../../src/commands/properties.js'; import { @@ -488,8 +489,9 @@ describe('CLI public API extensions', () => { ); it('sends the phone requirement on a person-anchored list export', async () => { - mockApiRequest.mockResolvedValue({ data: { export_id: 'export-1' } }); - await listsExport('list_1', { requirePhone: true, json: true }); + mockApiRequest.mockResolvedValue({ export_id: 'export-1', status: 'completed', record_count: 2 }); + await listsExport('list_1', { requirePhone: true }); + expect(printKeyValue).toHaveBeenCalledWith({ 'Export ID': 'export-1', 'Status': 'completed', 'Record Count': 2 }); expect(mockApiRequest).toHaveBeenCalledWith('/lists/list_1/export', { method: 'POST', body: { require_phone: true }, }); From ce348dd50ae22e581794945a8c709d4bc61c87a4 Mon Sep 17 00:00:00 2001 From: Dave Oster Date: Tue, 3 Nov 2026 22:23:42 -0600 Subject: [PATCH 4/4] docs(listing): describe property-owner lookup in the registry and plugin listings (WORK-13965) The MCP registry entry, now version 0.3.1, and the Claude, Codex and Gemini plugin listings say DealMachine finds US property owners, including LLCs, and owner phones and emails, and needs a DealMachine account. The registry text matches what mcp.dealmachine.com serves. Co-Authored-By: Claude Opus 5.5 --- .claude-plugin/marketplace.json | 2 +- .codex-plugin/plugin.json | 2 +- .github/workflows/publish-mcp-registry.yml | 2 +- gemini-extension.json | 2 +- plugin.json | 2 +- server.json | 4 ++-- skills/dealmachine/SKILL.md | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 3f457d2..da815c7 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ "name": "dealmachine", "source": "./", "version": "0.3.0", - "description": "Search and enrich US property, owner, and people data with DealMachine.", + "description": "Find US property owners, including LLC owners, and owner phones and emails with DealMachine.", "author": { "name": "DealMachine" }, diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index c235981..14e63bb 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "dealmachine", "version": "0.3.0", - "description": "Search and enrich US property, owner, and people data with DealMachine.", + "description": "Find US property owners, including LLC owners, and owner phones and emails with DealMachine.", "author": { "name": "DealMachine", "email": "support@dealmachine.com", diff --git a/.github/workflows/publish-mcp-registry.yml b/.github/workflows/publish-mcp-registry.yml index 2827965..63a0706 100644 --- a/.github/workflows/publish-mcp-registry.yml +++ b/.github/workflows/publish-mcp-registry.yml @@ -24,5 +24,5 @@ jobs: - name: Authenticate with GitHub OIDC run: ./mcp-publisher login github-oidc - - name: Publish version 0.3.0 + - name: Publish version 0.3.1 run: ./mcp-publisher publish server.json diff --git a/gemini-extension.json b/gemini-extension.json index 687b099..4e798d7 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,7 +1,7 @@ { "name": "dealmachine", "version": "0.3.0", - "description": "Search and enrich US property, owner, and people data with DealMachine.", + "description": "Find US property owners, including LLC owners, and owner phones and emails with DealMachine.", "mcpServers": { "dealmachine": { "httpUrl": "https://mcp.dealmachine.com" diff --git a/plugin.json b/plugin.json index 6cdec73..561855f 100644 --- a/plugin.json +++ b/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "dealmachine", "version": "0.3.0", - "description": "Search and enrich US property, owner, and people data with DealMachine.", + "description": "Find US property owners, including LLC owners, and owner phones and emails with DealMachine.", "author": { "name": "DealMachine", "email": "support@dealmachine.com", diff --git a/server.json b/server.json index 5814882..add832c 100644 --- a/server.json +++ b/server.json @@ -2,12 +2,12 @@ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "io.github.DealMachine/dealmachine", "title": "DealMachine", - "description": "Search and enrich US property, owner, and people data for sales and lead generation.", + "description": "Find US property owners, including LLCs, plus owner phones and emails. Needs a DealMachine account.", "repository": { "url": "https://github.com/DealMachine/dealmachine-cli", "source": "github" }, - "version": "0.3.0", + "version": "0.3.1", "remotes": [ { "type": "streamable-http", diff --git a/skills/dealmachine/SKILL.md b/skills/dealmachine/SKILL.md index 7d21aa5..8f666b9 100644 --- a/skills/dealmachine/SKILL.md +++ b/skills/dealmachine/SKILL.md @@ -1,6 +1,6 @@ --- name: dealmachine -description: Search and enrich US property, owner, and people data with DealMachine through its MCP tools or dm CLI. Use for real estate lead generation, property and owner lookup, contact enrichment, prospecting, comparable sales, market research, targeted lists or exports, and credit-aware DealMachine workflows. +description: Find US property owners, including LLC and company owners, and owner phones and emails, and search and enrich property and people data with DealMachine through its MCP tools or dm CLI. Use for real estate lead generation, finding who owns a property, property and owner lookup, contact enrichment, prospecting, comparable sales, market research, targeted lists or exports, and credit-aware DealMachine workflows. license: MIT allowed-tools: - Bash(dm agents)