Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-mcp-registry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 6 additions & 3 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -850,17 +850,18 @@ dm lists remove list_abc123 --ids 111,222,333

#### `dm lists export <id>`

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 <csv>` | Comma-separated list of fields to export |
| `--anchor <type>` | `property` or `person` |
| `--anchor <type>` | `property` or `person` (default) |
| `--require-phone` | Only export people with a phone number |

---

Expand Down Expand Up @@ -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 <id> --yes`. Archiving an active, paused, ready or draft campaign returns `409 CAMPAIGN_NOT_ENDED` without changing it.

`dm mail campaigns archive <id> --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.
2 changes: 1 addition & 1 deletion gemini-extension.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion skills/dealmachine/SKILL.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
9 changes: 6 additions & 3 deletions src/commands/lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
const requestBody: Record<string, unknown> = {};
if (options.fields) {
Expand All @@ -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<ExportResult | { data: ExportResult }>(
`/lists/${listId}/export`,
{ method: 'POST', body: requestBody }
);
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/commands/mail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1032,7 +1032,8 @@ Examples:

listsCmd
.command('export <id>')
.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 <csv>', 'Comma-separated list of fields to export')
.option('--anchor <type>', 'Anchor type: property or person')
.option('--json', 'Output as JSON')
Expand Down Expand Up @@ -2444,7 +2445,7 @@ Mail already printed still delivers. To reuse its steps, create a new campaign.`

mailCampaignsCmd
.command('archive <id>')
.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(
Expand Down
3 changes: 3 additions & 0 deletions src/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ export async function apiRequest<T>(
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) {
Expand Down
14 changes: 12 additions & 2 deletions tests/commands/publicApiExtensions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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 {
Expand All @@ -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';

Expand Down Expand Up @@ -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: [
Expand Down
18 changes: 18 additions & 0 deletions tests/lib/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Loading