zoodealio 0.2.1 → 0.3.0-dev.1607

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,8 +17,7 @@ zoodealio whoami
17
17
  ```
18
18
 
19
19
  or run it without installing: `npx zoodealio whoami` (after a `zoodealio login` elsewhere, or with
20
- `ZOODEALIO_API_KEY` set). From a checkout of this repo, `node integrations/cli/bin/zoodealio.js`
21
- runs the working copy.
20
+ `ZOODEALIO_API_KEY` set).
22
21
 
23
22
  ## Authenticate
24
23
 
@@ -43,9 +42,7 @@ It prints `{ok, email, name, keyPrefix, origin}` on stdout and never the key.
43
42
 
44
43
  The key is stored per API origin in `$XDG_CONFIG_HOME/zoodealio/credentials.json`
45
44
  (default `~/.config/zoodealio/credentials.json`; `%APPDATA%\zoodealio\credentials.json` on Windows),
46
- written atomically with mode `0600` in a `0700` directory. Keys are per environment:
47
- `zoodealio login --env dev` stores a separate login. `ZOODEALIO_WEB_URL` overrides the sign-in
48
- page origin the way `ZOODEALIO_API_URL` overrides the API origin.
45
+ written atomically with mode `0600` in a `0700` directory.
49
46
 
50
47
  ### API key for CI and servers
51
48
 
@@ -96,17 +93,6 @@ Cursor (`.cursor/mcp.json`) or VS Code (`.vscode/mcp.json`, under `servers`):
96
93
  Run `zoodealio login` first (or put `ZOODEALIO_API_KEY` in the client's `env`). Stdout carries only
97
94
  JSON-RPC; logs go to stderr.
98
95
 
99
- ## Environments
100
-
101
- | Selection | API origin |
102
- | --- | --- |
103
- | default | `https://api.zoodealio.ai` |
104
- | `--env dev` | `https://dev.api.zoodealio.ai` |
105
- | `ZOODEALIO_API_URL=http://localhost:5256` | that origin (overrides both) |
106
-
107
- Keys are per environment. Use a local API URL only for a local API; the port above is an
108
- example, use whichever yours runs on.
109
-
110
96
  ## Output and errors
111
97
 
112
98
  stdout carries JSON and nothing else: pretty-printed on a terminal, compact when piped
@@ -138,7 +124,6 @@ Route IDs are GUIDs and are checked before any request is made.
138
124
 
139
125
  | Flag | Effect |
140
126
  | --- | --- |
141
- | `--env prod\|dev` | Pick the environment (default `prod`) |
142
127
  | `--pretty` | Pretty-print JSON even when piped |
143
128
  | `--dry-run` | Print `{method, url, headers, body}` with the key redacted and send nothing; works without a key |
144
129
  | `--yes` | Confirm a destructive command (not needed with `--dry-run`) |
@@ -157,7 +142,7 @@ concurrency on `properties update`, `offers accept` and `offers request-details`
157
142
  | --- | --- | --- |
158
143
  | `properties list` | GET Properties | `--search`, `--state` |
159
144
  | `properties get <id>` | GET Properties/{id} | |
160
- | `properties create` | POST Properties | `--body` |
145
+ | `properties create` | POST Properties | `--body`; `--agent <id>` assigns a team member or a pending invite |
161
146
  | `properties update <id>` | PUT Properties/{id} | `--body`, `--if-match`; replaces wholesale |
162
147
  | `properties photos list\|add\|delete` | photos | `add`: `--url` (repeatable) or `--body`; `delete <id> <photoId>` needs `--yes` |
163
148
  | `properties documents list\|add\|delete` | documents | same shape as photos |
@@ -169,7 +154,7 @@ concurrency on `properties update`, `offers accept` and `offers request-details`
169
154
  | `transactions get <id>` | GET Transactions/{id} | |
170
155
  | `transactions active` | GET Transactions/active | `--property` required |
171
156
  | `comping status <transactionId>` | GET Transactions/{id}/comping | |
172
- | `team members`, `team get <id>`, `teams list` | GET Team | `--search` |
157
+ | `team members`, `team get <id>`, `teams list` | GET Team | `--search`; `team members` also lists pending invites (`kind: "invite"`) |
173
158
  | `team invite` | POST Team/members | `--email` (+ `--first-name`, `--last-name`, `--team`, `--admin-team`) or `--body` |
174
159
  | `team remove <id>` | DELETE Team/members/{id} | `--yes` required |
175
160
  | `webhooks event-types\|list\|deliveries` | GET Webhooks | `deliveries`: `--subscription`, `--status` |
@@ -241,25 +226,3 @@ zoodealio api GET Properties --query pageSize=5
241
226
  - No keychain or interactive prompts. Credentials are `ZOODEALIO_API_KEY` or the
242
227
  `credentials.json` written by `zoodealio login`.
243
228
 
244
- ## Development
245
-
246
- ```sh
247
- cd integrations/cli
248
- npm install
249
- npm run check # eslint, tsc --noEmit (checkJs), node --test
250
- ```
251
-
252
- ## Releasing
253
-
254
- npm no longer lets a token that bypasses 2FA publish directly, so releases are staged and then
255
- approved by a maintainer with 2FA:
256
-
257
- ```sh
258
- cd integrations/cli
259
- npm version <patch|minor|major> --no-git-tag-version # in a PR, then merge
260
- npm run check
261
- npm stage publish --userconfig ~/.config/zoodealio/npm-publish.npmrc
262
- ```
263
-
264
- Then approve it on npmjs.com (avatar → Staged Packages → Approve). A version with a `-` suffix
265
- (`0.2.0-beta.1`) is staged with `--tag next` so `latest` stays stable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zoodealio",
3
- "version": "0.2.1",
3
+ "version": "0.3.0-dev.1607",
4
4
  "description": "Zoodealio command line tool: the partner API as JSON, built for people, scripts and AI agents",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
package/src/cli.js CHANGED
@@ -2,7 +2,7 @@ const { CliError, usage } = require('./errors');
2
2
  const { parse, normalizeApiPath, METHODS } = require('./args');
3
3
  const { catalogue } = require('./commands');
4
4
  const { resolveBody } = require('./input');
5
- const { USER_AGENT, redactKey, scrubKeys, resolveOrigin, buildUrl, send } = require('./http');
5
+ const { DEFAULT_ENV, USER_AGENT, redactKey, scrubKeys, resolveOrigin, buildUrl, send } = require('./http');
6
6
  const { readCredential, saveCredential, removeCredential } = require('./credentials');
7
7
  const { browserLogin, resolveWebOrigin, defaultDevice } = require('./login');
8
8
  const { bridge } = require('./mcp');
@@ -57,7 +57,8 @@ async function execute(argv, io, envKey, secrets) {
57
57
 
58
58
  if (command.name === 'commands') return emit(catalogue());
59
59
 
60
- const origin = resolveOrigin(io.env, flags.env);
60
+ const envName = flags.env || DEFAULT_ENV;
61
+ const origin = resolveOrigin(io.env, envName);
61
62
  const stored = readCredential(io.env, origin);
62
63
  if (stored) secrets.add(stored.key);
63
64
  const source = envKey ? 'env' : stored ? 'login' : undefined;
@@ -68,13 +69,14 @@ async function execute(argv, io, envKey, secrets) {
68
69
  if (command.name === 'login') {
69
70
  const ui = createUi(io.stderr, { fancy: !!io.interactive && !io.env.NO_COLOR });
70
71
  const result = await browserLogin({
71
- apiOrigin: origin, webOrigin: resolveWebOrigin(io.env, flags.env), device: (flags.name?.trim() || defaultDevice()).slice(0, 64),
72
- envName: flags.env || 'prod', noBrowser: !!flags['no-browser'], openBrowser: io.openBrowser, copy: io.copy,
72
+ apiOrigin: origin, webOrigin: resolveWebOrigin(io.env, envName), device: (flags.name?.trim() || defaultDevice()).slice(0, 64),
73
+ envName, noBrowser: !!flags['no-browser'], openBrowser: io.openBrowser, copy: io.copy,
73
74
  interactive: io.interactive, onKey: io.onKey, timeoutMs: io.loginTimeoutMs, ui, fetch: io.fetch,
74
75
  });
75
76
  secrets.add(result.key);
76
77
  const file = saveCredential(io.env, origin, { key: result.key, keyId: result.keyId, email: result.email, name: result.name });
77
- const dev = flags.env && flags.env !== 'prod' ? ` --env ${flags.env}` : '';
78
+ // The hints run without --env only where that reaches the environment just signed in to.
79
+ const dev = envName === DEFAULT_ENV ? '' : ` --env ${envName}`;
78
80
  const home = io.env.HOME || io.env.USERPROFILE;
79
81
  const where = home && file.startsWith(home) ? `~${file.slice(home.length)}` : file;
80
82
  const { style } = ui;
@@ -86,7 +88,7 @@ async function execute(argv, io, envKey, secrets) {
86
88
  ];
87
89
  const width = Math.max(...next.map(([c]) => c.length)) + 3;
88
90
  ui.say();
89
- ui.say(`${style.green('✓')} Signed in as ${style.bold(result.email)}${dev ? ` ${style.dim(`(${flags.env})`)}` : ''}`);
91
+ ui.say(`${style.green('✓')} Signed in as ${style.bold(result.email)}${envName === 'prod' ? '' : ` ${style.dim(`(${envName})`)}`}`);
90
92
  ui.say(` ${style.dim(`Key "${result.name}" saved to ${where}`)}`);
91
93
  ui.say();
92
94
  ui.say(` ${style.bold('Next')}`);
@@ -148,6 +150,11 @@ async function execute(argv, io, envKey, secrets) {
148
150
  if (flags.body !== undefined) {
149
151
  if ((command.convenience || []).some((n) => flags[n] !== undefined)) throw usage('use either --body or the convenience flags, not both');
150
152
  body = await resolveBody(flags.body, io.stdin);
153
+ const extra = command.overlay && command.overlay(flags);
154
+ if (extra && Object.keys(extra).length) {
155
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) throw usage('--body must be a JSON object when combined with convenience flags');
156
+ body = { ...body, ...extra };
157
+ }
151
158
  } else if (command.build) {
152
159
  body = command.build(flags);
153
160
  if (body === undefined && command.body === 'required') throw usage('provide --body or the convenience flags');
package/src/commands.js CHANGED
@@ -7,7 +7,7 @@ const { parseJsonArg } = require('./input');
7
7
  * @typedef {{name: string, description: string, guid?: boolean}} Arg
8
8
  * @typedef {{name: string, summary: string, method: string | null, path: string | null, args: Arg[],
9
9
  * flags: Flag[], body?: 'required' | 'optional', convenience?: string[], destructive?: boolean,
10
- * auth?: boolean, build?: (flags: Record<string, any>) => unknown, notes?: string[]}} Command
10
+ * auth?: boolean, build?: (flags: Record<string, any>) => unknown, overlay?: (flags: Record<string, any>) => Record<string, unknown>, notes?: string[]}} Command
11
11
  */
12
12
 
13
13
  /**
@@ -50,7 +50,14 @@ const COMMANDS = [
50
50
  flags: [str('search', 'Free-text search', { query: 'search' }), str('state', 'Two-letter state code', { query: 'stateCode' }), ...PAGING],
51
51
  }),
52
52
  cmd('properties get', 'Get one property', 'GET', '/openapi/Properties/{id}', { args: [id('id', 'Property id')] }),
53
- cmd('properties create', 'Create a property', 'POST', '/openapi/Properties', { body: 'required', flags: [BODY] }),
53
+ cmd('properties create', 'Create a property', 'POST', '/openapi/Properties', {
54
+ body: 'required',
55
+ flags: [str('agent', 'Agent to assign: a team member id, or a pending invite id from `team members`'), BODY],
56
+ overlay: (f) => {
57
+ if (f.agent !== undefined && !GUID.test(f.agent)) throw usage(`agent id is not a GUID: ${f.agent}`);
58
+ return compact({ agentId: f.agent });
59
+ },
60
+ }),
54
61
  cmd('properties update', 'Replace a property (wholesale)', 'PUT', '/openapi/Properties/{id}', {
55
62
  args: [id('id', 'Property id')], body: 'required', flags: [BODY, IF_MATCH],
56
63
  }),
@@ -95,7 +102,7 @@ const COMMANDS = [
95
102
  cmd('comping status', 'Get AI comping status for a transaction', 'GET', '/openapi/Transactions/{transactionId}/comping', {
96
103
  args: [id('transactionId', 'Transaction id')],
97
104
  }),
98
- cmd('team members', 'List team members', 'GET', '/openapi/Team/members', {
105
+ cmd('team members', 'List team members, including pending invites', 'GET', '/openapi/Team/members', {
99
106
  flags: [str('search', 'Free-text search', { query: 'search' }), ...PAGING],
100
107
  }),
101
108
  cmd('team get', 'Get one team member', 'GET', '/openapi/Team/members/{id}', { args: [id('id', 'Member id')] }),
@@ -179,7 +186,7 @@ const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
179
186
 
180
187
  /** Flags accepted by every command. */
181
188
  const GLOBAL_FLAGS = /** @type {Flag[]} */ ([
182
- str('env', 'prod (default) or dev; ZOODEALIO_API_URL overrides'),
189
+ str('env', 'prod or dev (default prod; dev on a dev build); ZOODEALIO_API_URL overrides'),
183
190
  { name: 'pretty', type: 'boolean', description: 'Pretty-print JSON even when piped' },
184
191
  { name: 'dry-run', type: 'boolean', description: 'Print the request (key redacted) without sending it' },
185
192
  { name: 'yes', type: 'boolean', description: 'Confirm a destructive command (not needed with --dry-run)' },
package/src/http.js CHANGED
@@ -4,6 +4,18 @@ const pkg = require('../package.json');
4
4
  const USER_AGENT = `zoodealio-cli/${pkg.version}`;
5
5
  const ENVS = { prod: 'https://api.zoodealio.ai', dev: 'https://dev.api.zoodealio.ai' };
6
6
 
7
+ /**
8
+ * CI publishes `<version>-dev.<run>` under the `dev` tag after the dev deployment, so that build
9
+ * talks to dev unless told otherwise. Every other version is a release and talks to prod.
10
+ * @param {string} version
11
+ * @returns {'prod'|'dev'}
12
+ */
13
+ function defaultEnv(version) {
14
+ return version.includes('-dev.') ? 'dev' : 'prod';
15
+ }
16
+
17
+ const DEFAULT_ENV = defaultEnv(pkg.version);
18
+
7
19
  /** @type {Record<number, [number, string]>} status -> [exit code, error code slug] */
8
20
  const STATUS_MAP = {
9
21
  400: [2, 'bad_request'],
@@ -47,7 +59,7 @@ function scrubKeys(text, keys) {
47
59
  function resolveOrigin(env, envName) {
48
60
  const override = env.ZOODEALIO_API_URL;
49
61
  if (override) return override.replace(/\/+$/, '');
50
- const name = envName || 'prod';
62
+ const name = envName || DEFAULT_ENV;
51
63
  if (!Object.hasOwn(ENVS, name)) throw usage('--env must be prod or dev');
52
64
  return ENVS[/** @type {'prod'|'dev'} */ (name)];
53
65
  }
@@ -123,4 +135,4 @@ async function send(doFetch, req) {
123
135
  }
124
136
  }
125
137
 
126
- module.exports = { ENVS, USER_AGENT, redactKey, scrubKeys, resolveOrigin, buildUrl, send };
138
+ module.exports = { ENVS, DEFAULT_ENV, USER_AGENT, defaultEnv, redactKey, scrubKeys, resolveOrigin, buildUrl, send };
package/src/login.js CHANGED
@@ -3,7 +3,7 @@ const http = require('node:http');
3
3
  const os = require('node:os');
4
4
  const { spawn } = require('node:child_process');
5
5
  const { CliError } = require('./errors');
6
- const { USER_AGENT, send } = require('./http');
6
+ const { DEFAULT_ENV, USER_AGENT, send } = require('./http');
7
7
 
8
8
  const WEB_ENVS = { prod: 'https://zoodealio.ai', dev: 'https://dev.zoodealio.ai' };
9
9
  const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
@@ -15,7 +15,7 @@ const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
15
15
  */
16
16
  function resolveWebOrigin(env, envName) {
17
17
  if (env.ZOODEALIO_WEB_URL) return env.ZOODEALIO_WEB_URL.replace(/\/+$/, '');
18
- return WEB_ENVS[/** @type {'prod'|'dev'} */ (envName === 'dev' ? 'dev' : 'prod')];
18
+ return WEB_ENVS[/** @type {'prod'|'dev'} */ ((envName || DEFAULT_ENV) === 'dev' ? 'dev' : 'prod')];
19
19
  }
20
20
 
21
21
  /**