From 7337417c4b4180c8468702d5c67840213bb40d1d Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Thu, 29 Oct 2026 15:48:53 -0400 Subject: [PATCH 1/2] docs(cli): support Year Built comps criteria examples --- docs/commands.md | 8 ++++++++ playbook/PLAYBOOK.md | 3 +++ tests/commands/publicApiExtensions.test.ts | 20 ++++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/docs/commands.md b/docs/commands.md index 04a03d1..7454d46 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -720,6 +720,14 @@ dm comps --body '{ Output includes subject property details, value estimation with confidence interval, summary statistics (average/median price, price per sqft), and a table of comparable properties. +Use `criteria.min_year_built` and `criteria.max_year_built` in a JSON body or file to limit comps by year built: + +```bash +dm comps --body '{"property_ids":["prop_12345"],"criteria":{"min_year_built":1980,"max_year_built":2000}}' --json +``` + +Both bounds are inclusive whole years from 1 through 9999. Either bound can stand alone; the minimum must not exceed the maximum. Omit both for no year filter. A specified bound excludes comps with an unknown year built. + --- ### Lists Commands diff --git a/playbook/PLAYBOOK.md b/playbook/PLAYBOOK.md index 4894149..a8a8ee8 100644 --- a/playbook/PLAYBOOK.md +++ b/playbook/PLAYBOOK.md @@ -592,6 +592,9 @@ dm comps prop_12345 --include-foreclosures # Full control via JSON body dm comps --body '{"property_ids": ["prop_12345"], "location": {"type": "radius", "radius_miles": 2}, "criteria": {"timeframe": "12months", "limit": 50}}' + +# Inclusive year-built range; either bound may be omitted +dm comps --body '{"property_ids":["prop_12345"],"criteria":{"min_year_built":1980,"max_year_built":2000}}' --json ``` ### People diff --git a/tests/commands/publicApiExtensions.test.ts b/tests/commands/publicApiExtensions.test.ts index 9551903..352d928 100644 --- a/tests/commands/publicApiExtensions.test.ts +++ b/tests/commands/publicApiExtensions.test.ts @@ -46,6 +46,7 @@ import { drivingGet, drivingList } from '../../src/commands/driving.js'; import { prospectsList } from '../../src/commands/prospects.js'; import { listsItems } from '../../src/commands/lists.js'; import { phonesDnc } from '../../src/commands/phones.js'; +import { comps } from '../../src/commands/comps.js'; beforeEach(() => { vi.clearAllMocks(); @@ -59,6 +60,25 @@ beforeEach(() => { }); describe('CLI public API extensions', () => { + it.each([{ body: '{}' }, { file: 'comps.json' }])( + 'forwards Year Built criteria from JSON input %j', + async (input) => { + const request = { + property_ids: ['prop_123'], + criteria: { min_year_built: 1980, max_year_built: 2000 }, + }; + mockParseRequestBody.mockResolvedValue(request); + + await comps([], { ...input, json: true }); + + expect(mockParseRequestBody).toHaveBeenCalledWith(expect.objectContaining(input)); + expect(mockApiRequest).toHaveBeenCalledWith('/comps', { + method: 'POST', + body: request, + }); + } + ); + it('reads drive history and filters prospects added during drives', async () => { mockApiRequest .mockResolvedValueOnce({ From bfa3ff0ce6b4cb44806718cf7ba99603dd9c3701 Mon Sep 17 00:00:00 2001 From: Ben Tucker Date: Thu, 29 Oct 2026 15:54:37 -0400 Subject: [PATCH 2/2] fix(cli): use a file URL for the Windows smoke loader --- scripts/smoke-live.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/smoke-live.mjs b/scripts/smoke-live.mjs index cd51309..9a5f7a6 100644 --- a/scripts/smoke-live.mjs +++ b/scripts/smoke-live.mjs @@ -4,7 +4,7 @@ import { promisify } from 'node:util'; import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; // An explicit target and QA key are required. Never fall back to a saved login. const apiKey = process.env.DM_API_KEY; @@ -26,6 +26,7 @@ try { const isolatedHome = join(temporary, 'home'); await mkdir(isolatedHome, { mode: 0o700 }); const isolation = join(temporary, 'isolation.mjs'); + const isolationUrl = pathToFileURL(isolation).href; await writeFile(isolation, [ "import os from 'node:os';", "import childProcess from 'node:child_process';", @@ -34,11 +35,11 @@ try { "childProcess.execFileSync = () => { throw new Error('Personal credential store disabled during smoke'); };", 'syncBuiltinESMExports();', ].join('\n'), { mode: 0o600 }); - await execute(process.execPath, ['--import', isolation, '--input-type=module', '--eval', + await execute(process.execPath, ['--import', isolationUrl, '--input-type=module', '--eval', `const { writeConfig } = await import(${JSON.stringify(new URL('../dist/lib/config.js', import.meta.url).href)}); writeConfig({apiKey: process.env.DM_API_KEY, keyId:'qa-smoke', organizationId:0, organizationName:'QA smoke', organizationSlug:'qa-smoke'});`, ], { cwd: temporary, env: environment, timeout: 10000 }); const command = async args => { - const { stdout } = await execute(process.execPath, ['--import', isolation, join(root, 'dist/index.js'), ...args, '--json', '--quiet'], { + const { stdout } = await execute(process.execPath, ['--import', isolationUrl, join(root, 'dist/index.js'), ...args, '--json', '--quiet'], { cwd: temporary, env: environment, timeout: 45000, maxBuffer: 2 * 1024 * 1024, }); return JSON.parse(stdout);