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/docs/commands.md b/docs/commands.md index 5eb8006..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 | --- @@ -1359,4 +1360,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/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) diff --git a/src/commands/lists.ts b/src/commands/lists.ts index 9f71018..896b906 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,8 +496,11 @@ 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 } }>( + type ExportResult = { export_id: string; status: string; record_count: number }; + const data = await apiRequest( `/lists/${listId}/export`, { method: 'POST', body: requestBody } ); @@ -508,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/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..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') @@ -2444,7 +2445,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( 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..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 { @@ -44,7 +45,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 +488,15 @@ describe('CLI public API extensions', () => { } ); + it('sends the phone requirement on a person-anchored list export', async () => { + 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 }, + }); + }); + 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'); +});