higherdata 0.2.1 → 0.3.0

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.
Files changed (3) hide show
  1. package/README.md +4 -2
  2. package/dist/index.js +350 -84
  3. package/package.json +2 -3
package/README.md CHANGED
@@ -8,7 +8,7 @@ node packages/cli/dist/index.js --version
8
8
  node packages/cli/dist/index.js --help
9
9
  ```
10
10
 
11
- The package provides `higherdata` and `hd`. After the first approved npm release,
11
+ The package provides the `higherdata` command. After the first approved npm release,
12
12
  install with `npm install --global higherdata`, or run `npx higherdata --version`.
13
13
  Global flags are `--json`, `--jsonl` and `--api-url <url>`.
14
14
  `--help` and `--version` work without credentials or a config file.
@@ -39,7 +39,7 @@ Exit codes: 0 success, 2 usage or configuration, 3 authentication, 4 limit,
39
39
 
40
40
  ## Interactive app
41
41
 
42
- Run `higherdata` (or `hd`) in a terminal to open the keyboard app. The yellow
42
+ Run `higherdata` in a terminal to open the keyboard app. The yellow
43
43
  wordmark and three bars form in under 1.5 seconds; any key skips it.
44
44
  `--no-banner` opens the menu immediately. Piped output, `--json`, `--jsonl`
45
45
  and `CI` never animate. `NO_COLOR` disables accent colors. Supports 80 columns
@@ -222,3 +222,5 @@ The metadata request times out after three seconds. On network failure the CLI
222
222
  uses its last known policy; with no cached policy it allows the command. Failed
223
223
  checks are cached for a day too. A read-only cache directory allows commands but
224
224
  cannot persist the daily check, so subsequent invocations may check again.
225
+
226
+ Download the loaded search page to a new file with `higherdata search companies --country-code CA --out results.csv`. Add `--all` to download up to 5,000 rows, or use a `.jsonl` destination for JSON Lines. The interactive results screen offers "This page" and "All results" when you press `d`. For larger sets, save the search to a list and export the list. Existing files are never overwritten.
package/dist/index.js CHANGED
@@ -28133,6 +28133,10 @@ function retryAfter(response, data, now) {
28133
28133
  const date5 = Date.parse(value);
28134
28134
  if (Number.isFinite(date5)) return Math.max(0, (date5 - now) / 1e3);
28135
28135
  }
28136
+ if (response.status === 429) {
28137
+ const reset = response.headers.get("ratelimit-reset");
28138
+ if (reset !== null && /^\d+(?:\.\d+)?$/.test(reset.trim())) return Number(reset);
28139
+ }
28136
28140
  const parsed = failure2.safeParse(data);
28137
28141
  return parsed.success && typeof parsed.data.error !== "string" ? parsed.data.error.retry_after : void 0;
28138
28142
  }
@@ -28340,8 +28344,16 @@ var init_api2 = __esm({
28340
28344
  async request(path2, input2, options = {}) {
28341
28345
  if (options.authenticated && !this.settings.apiKey) throw new CliError("Run higherdata login or set HIGHERDATA_API_KEY.", EXIT_CODES.AUTH);
28342
28346
  try {
28343
- const response = await this.transport.request(path2, input2, { ...options, idempotencyKey: this.settings.idempotencyKey });
28344
- return { status: response.status, data: response.data };
28347
+ for (let attempt = 0; ; attempt++) {
28348
+ try {
28349
+ return await this.transport.request(path2, input2, { ...options, idempotencyKey: this.settings.idempotencyKey });
28350
+ } catch (error63) {
28351
+ if (!(error63 instanceof HigherDataError) || error63.status !== 429 || !options.rateLimitRecovery || attempt >= 5) throw error63;
28352
+ const wait = (error63.retryAfter ?? 1) * 1e3;
28353
+ if (!Number.isFinite(wait) || wait > 3e5) throw error63;
28354
+ await options.rateLimitRecovery.sleep(Math.max(100, wait));
28355
+ }
28356
+ }
28345
28357
  } catch (error63) {
28346
28358
  if (error63 instanceof HigherDataError) {
28347
28359
  if (error63.status) throw apiFailure(error63.status, { error: { code: error63.code, message: error63.message } });
@@ -31914,11 +31926,10 @@ var init_package = __esm({
31914
31926
  "package.json"() {
31915
31927
  package_default = {
31916
31928
  name: "higherdata",
31917
- version: "0.2.1",
31929
+ version: "0.3.0",
31918
31930
  type: "module",
31919
31931
  bin: {
31920
- higherdata: "dist/index.js",
31921
- hd: "dist/index.js"
31932
+ higherdata: "dist/index.js"
31922
31933
  },
31923
31934
  files: [
31924
31935
  "dist",
@@ -34895,6 +34906,9 @@ function cell(value) {
34895
34906
  const text2 = value == null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
34896
34907
  return text2.replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ");
34897
34908
  }
34909
+ function lookupMatchColumns(row) {
34910
+ return { kind: cell(row.kind), name: cell(row.label), details: cell(row.detail) };
34911
+ }
34898
34912
  function customerOutput(value) {
34899
34913
  if (Array.isArray(value)) return value.map(customerOutput);
34900
34914
  if (!value || typeof value !== "object") return value;
@@ -34904,10 +34918,10 @@ function renderOutput(value, mode) {
34904
34918
  value = customerOutput(value);
34905
34919
  if (mode === "json") return `${JSON.stringify(value)}
34906
34920
  `;
34907
- const records = Array.isArray(value.items) ? value.items : Array.isArray(value.candidates) ? value.candidates : [value];
34921
+ const records = Array.isArray(value.items) ? value.items : Array.isArray(value.candidates) ? value.candidates : Array.isArray(value.matches) ? value.matches : [value];
34908
34922
  if (mode === "jsonl") return records.map((record2) => JSON.stringify(record2)).join("\n") + (records.length ? "\n" : "");
34909
34923
  if (!records.length) return "No results.\n";
34910
- const rows = records.map((record2) => record2);
34924
+ const rows = records.map((record2) => Array.isArray(value.matches) ? lookupMatchColumns(record2) : record2);
34911
34925
  const columns = rows.some((row) => "companyName" in row) ? rows.some((row) => "fullName" in row) ? ["id", "fullName", "jobTitle", "companyName", "emailAddress"] : ["id", "companyName", "normalizedDomain", "headquartersCountryCode"] : Object.keys(rows[0]);
34912
34926
  const headings = columns.map((column) => column.replace(/([a-z])([A-Z])/gu, "$1 $2").toUpperCase());
34913
34927
  const widths = columns.map((column, index) => rows.reduce((width, row) => Math.min(64, Math.max(width, cell(row[column]).length)), headings[index].length));
@@ -34923,6 +34937,105 @@ var init_output = __esm({
34923
34937
  }
34924
34938
  });
34925
34939
 
34940
+ // ../../src/lib/csv/search-download.ts
34941
+ function csvCell(value) {
34942
+ let text2 = value === null || value === void 0 ? "" : String(value);
34943
+ if (/^[\s\u0000-\u001f]*[=+\-@]/.test(text2)) text2 = `'${text2}`;
34944
+ return `"${text2.replaceAll('"', '""')}"`;
34945
+ }
34946
+ function pageCsv(headers, rows) {
34947
+ const lines = [headers.map(csvCell).join(","), ...rows.map((row) => row.map(csvCell).join(","))];
34948
+ return `${lines.join("\r\n")}\r
34949
+ `;
34950
+ }
34951
+ function companyPageCsv(items) {
34952
+ return pageCsv(
34953
+ [...COMPANY_DOWNLOAD_FIELDS],
34954
+ items.map((item) => COMPANY_DOWNLOAD_FIELDS.map((field) => item[field]))
34955
+ );
34956
+ }
34957
+ function peoplePageCsv(items) {
34958
+ return pageCsv(
34959
+ [...PEOPLE_DOWNLOAD_FIELDS],
34960
+ items.map((item) => PEOPLE_DOWNLOAD_FIELDS.map((field) => item[field]))
34961
+ );
34962
+ }
34963
+ function searchPageJsonl(kind, items) {
34964
+ const fields = kind === "companies" ? COMPANY_DOWNLOAD_FIELDS : PEOPLE_DOWNLOAD_FIELDS;
34965
+ return items.map((item) => JSON.stringify(Object.fromEntries(fields.map((field) => [field, item[field] ?? null]))) + "\n").join("");
34966
+ }
34967
+ var COMPANY_DOWNLOAD_FIELDS, PEOPLE_DOWNLOAD_FIELDS;
34968
+ var init_search_download = __esm({
34969
+ "../../src/lib/csv/search-download.ts"() {
34970
+ "use strict";
34971
+ COMPANY_DOWNLOAD_FIELDS = ["id", "companyName", "normalizedDomain", "headquartersCountryCode", "industryLinkedin", "employeeCountEstimate"];
34972
+ PEOPLE_DOWNLOAD_FIELDS = ["id", "fullName", "jobTitle", "companyName", "emailAddress", "countryCode"];
34973
+ }
34974
+ });
34975
+
34976
+ // src/search-download.ts
34977
+ import { open as open5 } from "fs/promises";
34978
+ function searchDownloadFormat(path2) {
34979
+ if (!/\.(csv|jsonl)$/.test(path2)) throw new CliError("Choose a new .csv or .jsonl destination.");
34980
+ return path2.endsWith(".jsonl") ? "jsonl" : "csv";
34981
+ }
34982
+ async function downloadSearch(api, name, input2, path2, options) {
34983
+ const format2 = searchDownloadFormat(path2);
34984
+ if (options.all && api.settings.idempotencyKey) throw new CliError("An idempotency key applies to one page. Omit --all.");
34985
+ const file2 = await open5(path2, "wx", 384);
34986
+ let count = 0, first = true, cursor = input2.cursor, loaded = options.loadedPage;
34987
+ const seen = /* @__PURE__ */ new Set();
34988
+ if (typeof cursor === "string") seen.add(cursor);
34989
+ try {
34990
+ while (true) {
34991
+ let raw, headers;
34992
+ if (loaded) {
34993
+ raw = loaded;
34994
+ loaded = void 0;
34995
+ } else {
34996
+ const response = await api.request(`/api/v1/tools/${name}`, { ...input2, ...cursor === void 0 ? {} : { cursor } }, { authenticated: true, rateLimitRecovery: options });
34997
+ raw = response.data;
34998
+ headers = response.headers;
34999
+ }
35000
+ const page = pageSchema2.safeParse(raw);
35001
+ if (!page.success || page.data.items.some((item) => typeof item.id !== "string" || !(name === "search.companies" ? /^hdo_[a-f0-9]{32}$/ : /^hdp_[a-f0-9]{32}$/).test(item.id))) throw new CliError("The API returned an invalid search page.");
35002
+ const items = page.data.items.slice(0, SEARCH_DOWNLOAD_CAP - count);
35003
+ if (format2 === "jsonl") await file2.writeFile(searchPageJsonl(name === "search.companies" ? "companies" : "people", items));
35004
+ else {
35005
+ const csv = (name === "search.companies" ? companyPageCsv : peoplePageCsv)(items);
35006
+ await file2.writeFile(first ? csv : csv.slice(csv.indexOf("\r\n") + 2));
35007
+ }
35008
+ first = false;
35009
+ count += items.length;
35010
+ options.progress?.(`Downloaded ${count.toLocaleString("en-US")} of 5,000`);
35011
+ const next = page.data.nextCursor;
35012
+ if (!options.all || !next || count === SEARCH_DOWNLOAD_CAP) return { rows: count, capped: !!options.all && count === SEARCH_DOWNLOAD_CAP && (!!next || items.length < page.data.items.length) };
35013
+ if (seen.has(next)) throw new CliError("The API repeated a cursor. Pagination stopped.");
35014
+ seen.add(next);
35015
+ cursor = next;
35016
+ if (headers?.get("ratelimit-remaining") === "0") {
35017
+ const reset = headers.get("ratelimit-reset");
35018
+ if (reset !== null && /^\d+(?:\.\d+)?$/.test(reset.trim())) await options.sleep(Number(reset) * 1e3);
35019
+ }
35020
+ }
35021
+ } finally {
35022
+ await file2.close();
35023
+ }
35024
+ }
35025
+ var SEARCH_DOWNLOAD_CAP, SEARCH_DOWNLOAD_CAP_NOTICE, isSearchResults, pageSchema2;
35026
+ var init_search_download2 = __esm({
35027
+ "src/search-download.ts"() {
35028
+ "use strict";
35029
+ init_zod();
35030
+ init_search_download();
35031
+ init_api2();
35032
+ SEARCH_DOWNLOAD_CAP = 5e3;
35033
+ SEARCH_DOWNLOAD_CAP_NOTICE = "Download stopped at 5,000 rows. Save the search to a list and export the list for larger sets.";
35034
+ isSearchResults = (name) => name === "search.companies" || name === "search.people";
35035
+ pageSchema2 = external_exports.object({ items: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).max(50), nextCursor: external_exports.string().min(1).max(2048).nullable() });
35036
+ }
35037
+ });
35038
+
34926
35039
  // ../../node_modules/smol-toml/dist/error.js
34927
35040
  function getLineColFromPtr(string4, ptr) {
34928
35041
  let lines = string4.slice(0, ptr).split(/\r?\n/);
@@ -37343,7 +37456,7 @@ var init_main = __esm({
37343
37456
 
37344
37457
  // src/install.ts
37345
37458
  import { constants } from "fs";
37346
- import { access, chmod as chmod2, lstat, mkdir as mkdir3, open as open5, readFile as readFile3, readdir, rename as rename3, unlink as unlink3 } from "fs/promises";
37459
+ import { access, chmod as chmod2, lstat, mkdir as mkdir3, open as open6, readFile as readFile3, readdir, rename as rename3, unlink as unlink3 } from "fs/promises";
37347
37460
  import { execFile as execFile2 } from "child_process";
37348
37461
  import { promisify, isDeepStrictEqual } from "util";
37349
37462
  import { homedir as homedir2 } from "os";
@@ -37417,7 +37530,7 @@ async function safeParents(path2) {
37417
37530
  await mkdir3(dirname3(path2), { recursive: true, mode: 448 });
37418
37531
  }
37419
37532
  async function privateFile(path2, text2) {
37420
- const file2 = await open5(path2, "wx", 384);
37533
+ const file2 = await open6(path2, "wx", 384);
37421
37534
  try {
37422
37535
  await file2.writeFile(text2, "utf8");
37423
37536
  } finally {
@@ -38050,7 +38163,7 @@ var init_model = __esm({
38050
38163
  ["account", "Account", "Usage, limits and API keys"],
38051
38164
  ["icp", "ICP", "Build your ideal customer profile"]
38052
38165
  ];
38053
- groupTools = (group) => TOOL_CATALOG.filter((tool) => tool.name.startsWith(`${group}.`));
38166
+ groupTools = (group) => TOOL_CATALOG.filter((tool) => tool.name.startsWith(`${group}.`) && tool.name !== "lookup.find");
38054
38167
  toolByName = (name) => TOOL_CATALOG.find((tool) => tool.name === name);
38055
38168
  clean = (value) => String(value ?? "").replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ");
38056
38169
  title = (value) => value.replaceAll("_", " ").replace(/([a-z])([A-Z])/gu, "$1 $2").replace(/\b\w/gu, (c) => c.toUpperCase());
@@ -38138,19 +38251,43 @@ var init_install_menu = __esm({
38138
38251
  });
38139
38252
 
38140
38253
  // src/terminal/banner.ts
38141
- function bannerFrame(frame, columns) {
38142
- const full = Array.from({ length: 6 }, (_, row) => [..."HIGHERDATA"].map((letter) => GLYPHS[letter][row]).join(" "));
38143
- const visible = frame < 5 ? Math.ceil((frame + 1) / 5 * full[0].length) : full[0].length;
38144
- const indent = Math.max(0, Math.floor((columns - full[0].length) / 2));
38145
- const widths = [24, 12, 18], offsets = [0, 6, 3];
38146
- const logo = widths.flatMap((width, index) => {
38147
- const progress = Math.max(0, Math.min(1, (frame - 5 - index) / 2));
38148
- const line = " ".repeat(Math.max(0, Math.floor((columns - 24) / 2)) + offsets[index]) + "\u2588".repeat(Math.ceil(width * progress));
38149
- return [line, line, ""];
38254
+ function markFrame(size, progress = [1, 1, 1]) {
38255
+ const scale = size === "large" ? 3 : 1;
38256
+ return [8, 4, 6].flatMap((units, index) => {
38257
+ const width = Math.round(units * scale * Math.max(0, Math.min(1, progress[index])));
38258
+ const indent = " ".repeat([0, 2, 1][index] * scale);
38259
+ const cap = width > 2 ? indent + " " + "\u2584".repeat(width - 2) : "";
38260
+ const base2 = width ? indent + "\u2580".repeat(width) : "";
38261
+ return size === "large" ? [cap, width ? indent + "\u2588".repeat(width) : "", cap.replaceAll("\u2584", "\u2580")] : [cap, base2];
38262
+ });
38263
+ }
38264
+ function wordmarkFrame(frame = BANNER_FRAMES - 1) {
38265
+ const visible = frame < 5 ? Math.ceil((frame + 1) / 5 * WORDMARK_WIDTH) : WORDMARK_WIDTH;
38266
+ return Array.from({ length: 6 }, (_, row) => {
38267
+ const higher = [..."HIGHER"].map((letter) => GLYPHS[letter][row]).join(" ") + " ";
38268
+ const data = [..."DATA"].map((letter) => GLYPHS[letter][row]).join(" ");
38269
+ return { higher: higher.slice(0, visible), data: data.slice(0, Math.max(0, visible - higher.length)) };
38150
38270
  });
38151
- return [...full.map((line) => " ".repeat(indent) + line.slice(0, visible)), "", ...logo, frame >= 10 ? " ".repeat(Math.max(0, Math.floor((columns - 23) / 2))) + "HIGHERDATA by HigherOps" : ""].map((line) => line.trimEnd());
38152
38271
  }
38153
- var GLYPHS, BANNER_FRAMES, BANNER_DURATION_MS, SMALL_MARK, shouldAnimate;
38272
+ function homeBrandLayout(columns, rows, extraRows = 0) {
38273
+ const sideBySide = columns - 4 >= WORDMARK_WIDTH + 3 + MARK_WIDTH;
38274
+ const headerRows = sideBySide ? 10 : 17;
38275
+ return { large: columns - 4 >= WORDMARK_WIDTH && rows >= headerRows + 14 + extraRows, sideBySide, headerRows };
38276
+ }
38277
+ function busyFrame(frame, size = "large") {
38278
+ return markFrame(size, [0, 1, 2].map((index) => 0.65 + 0.35 * (1 + Math.sin(frame * Math.PI / 4 - index * Math.PI / 2)) / 2));
38279
+ }
38280
+ function bannerFrame(frame, columns) {
38281
+ const wordmark = wordmarkFrame(frame).map((line) => line.higher + line.data);
38282
+ const indent = " ".repeat(Math.max(0, Math.floor((columns - WORDMARK_WIDTH) / 2)));
38283
+ const logo = markFrame("large", [0, 1, 2].map((index) => Math.max(0, Math.min(1, (frame - 5 - index) / 2))));
38284
+ const markIndent = " ".repeat(Math.max(0, Math.floor((columns - MARK_WIDTH) / 2)));
38285
+ return [...wordmark.map((line) => indent + line), "", ...logo.map((line) => markIndent + line), frame >= 10 ? " ".repeat(Math.max(0, Math.floor((columns - 23) / 2))) + "HIGHERDATA by HigherOps" : ""].map((line) => line.trimEnd());
38286
+ }
38287
+ function animationOptions(args = process.argv.slice(2)) {
38288
+ return { banner: !args.includes("--no-banner"), json: args.includes("--json"), jsonl: args.includes("--jsonl") };
38289
+ }
38290
+ var GLYPHS, BANNER_FRAMES, BANNER_DURATION_MS, MARK_WIDTH, WORDMARK_WIDTH, shouldAnimate;
38154
38291
  var init_banner = __esm({
38155
38292
  "src/terminal/banner.ts"() {
38156
38293
  "use strict";
@@ -38166,13 +38303,15 @@ var init_banner = __esm({
38166
38303
  };
38167
38304
  BANNER_FRAMES = 11;
38168
38305
  BANNER_DURATION_MS = 1210;
38169
- SMALL_MARK = ["\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588", " \u2588\u2588\u2588\u2588 ", " \u2588\u2588\u2588\u2588\u2588\u2588 "];
38306
+ MARK_WIDTH = 24;
38307
+ WORDMARK_WIDTH = [..."HIGHERDATA"].reduce((width, letter) => width + GLYPHS[letter][0].length, "HIGHERDATA".length - 1);
38170
38308
  shouldAnimate = (tty, options, env) => tty && options.banner !== false && !options.json && !options.jsonl && !Object.hasOwn(env, "CI");
38171
38309
  }
38172
38310
  });
38173
38311
 
38174
38312
  // src/terminal/screens.tsx
38175
- import { Box, Text } from "ink";
38313
+ import { useEffect, useState } from "react";
38314
+ import { Box, Text, useStdout } from "ink";
38176
38315
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
38177
38316
  function Menu({ items, selected, limit, color, width }) {
38178
38317
  const start = Math.max(0, Math.min(selected - Math.floor(limit / 2), items.length - limit));
@@ -38195,17 +38334,54 @@ function Menu({ items, selected, limit, color, width }) {
38195
38334
  ] })
38196
38335
  ] });
38197
38336
  }
38337
+ function Mark({ lines, color }) {
38338
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx(Text, { color, children: line || " " }, i)) });
38339
+ }
38340
+ function Wordmark({ color, frame }) {
38341
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: WORDMARK_WIDTH, children: wordmarkFrame(frame).map((line, i) => /* @__PURE__ */ jsxs(Text, { children: [
38342
+ line.higher,
38343
+ /* @__PURE__ */ jsx(Text, { color, children: line.data })
38344
+ ] }, i)) });
38345
+ }
38346
+ function BusyMark({ color, size = "large", options = animationOptions(), env = process.env }) {
38347
+ const { stdout } = useStdout(), [frame, setFrame] = useState(0);
38348
+ const animate = shouldAnimate(Boolean(stdout.isTTY), options, env);
38349
+ useEffect(() => {
38350
+ if (!animate) return;
38351
+ const timer = setInterval(() => setFrame((value) => (value + 1) % 8), 120);
38352
+ return () => clearInterval(timer);
38353
+ }, [animate]);
38354
+ return /* @__PURE__ */ jsx(Mark, { lines: animate ? busyFrame(frame, size) : markFrame(size), color });
38355
+ }
38198
38356
  function TerminalScreen({ state, columns, rows = 28, colors = true, profile = "default", space = "Default", workspace = "HigherOps", remaining = "--" }) {
38199
38357
  const width = Math.max(40, columns), color = colors ? "#f2b705" : void 0;
38200
- const limit = Math.max(4, rows - 12);
38358
+ const limit = Math.max(4, rows - 15);
38359
+ const extraRows = [state.error, state.notice].reduce((count, text2) => count + Math.ceil(clean(text2).length / (width - 4)), 0);
38360
+ const brand = homeBrandLayout(width, rows, extraRows), largeHome = state.screen === "home" && brand.large;
38201
38361
  if (state.screen === "banner") return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
38202
- bannerFrame(state.frame, width).map((line, i) => /* @__PURE__ */ jsx(Text, { color, children: line }, i)),
38362
+ /* @__PURE__ */ jsx(Box, { paddingLeft: Math.max(0, Math.floor((width - WORDMARK_WIDTH) / 2)), children: /* @__PURE__ */ jsx(Wordmark, { color, frame: state.frame }) }),
38363
+ /* @__PURE__ */ jsx(Text, { children: " " }),
38364
+ /* @__PURE__ */ jsx(Mark, { lines: bannerFrame(state.frame, width).slice(7, 16), color }),
38365
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: bannerFrame(state.frame, width)[16] || " " }),
38203
38366
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press any key to start" })
38204
38367
  ] });
38205
38368
  const fields = state.tool ? fieldsFor(state.tool.input, state.input, state.variant) : [];
38206
38369
  let help = "Up/Down choose. Enter opens. Esc back. Ctrl+C quit.";
38207
38370
  let content;
38208
- if (state.screen === "home") content = /* @__PURE__ */ jsx(Menu, { items: homeItems, selected: state.selected, limit: Math.max(4, rows - 14), color, width: width - 4 });
38371
+ if (state.screen === "home") help = "Up/Down choose. Enter opens. / Look up. Ctrl+C quit.";
38372
+ if (state.screen === "home") content = /* @__PURE__ */ jsx(Menu, { items: homeItems, selected: state.selected, limit: Math.max(4, rows - (largeHome ? brand.headerRows : 6) - 8 - extraRows), color, width: width - 4 });
38373
+ if (state.screen === "lookup") {
38374
+ content = /* @__PURE__ */ jsxs(Fragment, { children: [
38375
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Look up" }),
38376
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Name, work email, international phone, domain, LinkedIn or record id" }),
38377
+ /* @__PURE__ */ jsxs(Text, { color, wrap: "wrap", children: [
38378
+ "> ",
38379
+ clean(state.text),
38380
+ "_"
38381
+ ] })
38382
+ ] });
38383
+ help = "Type or paste. Enter looks up. Esc home.";
38384
+ }
38209
38385
  if (state.screen === "install") {
38210
38386
  content = /* @__PURE__ */ jsxs(Fragment, { children: [
38211
38387
  /* @__PURE__ */ jsxs(Text, { bold: true, children: [
@@ -38276,6 +38452,12 @@ function TerminalScreen({ state, columns, rows = 28, colors = true, profile = "d
38276
38452
  ] });
38277
38453
  help = "Enter edit/run. Arrays: comma separated. Blank clears. Esc back.";
38278
38454
  }
38455
+ if (state.screen === "download-choice") {
38456
+ content = /* @__PURE__ */ jsxs(Fragment, { children: [
38457
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Download search results" }),
38458
+ /* @__PURE__ */ jsx(Menu, { items: [{ label: "This page", hint: "Loaded rows, no new request" }, { label: "All results", hint: "Same search, up to 5,000 rows" }], selected: state.selected, limit: 2, color, width: width - 4 })
38459
+ ] });
38460
+ }
38279
38461
  if (["edit", "upload", "download", "space"].includes(state.screen)) {
38280
38462
  const field = fields.find((field2) => field2.path === state.field);
38281
38463
  content = /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -38297,7 +38479,7 @@ function TerminalScreen({ state, columns, rows = 28, colors = true, profile = "d
38297
38479
  }
38298
38480
  if (state.screen === "busy") {
38299
38481
  content = /* @__PURE__ */ jsxs(Fragment, { children: [
38300
- SMALL_MARK.map((line, i) => /* @__PURE__ */ jsx(Text, { color, children: line }, i)),
38482
+ /* @__PURE__ */ jsx(BusyMark, { color, size: rows >= 28 ? "large" : "compact" }),
38301
38483
  /* @__PURE__ */ jsx(Text, { children: state.notice || "Working..." })
38302
38484
  ] });
38303
38485
  help = "The current operation is running. It is never retried automatically.";
@@ -38316,35 +38498,58 @@ function TerminalScreen({ state, columns, rows = 28, colors = true, profile = "d
38316
38498
  }
38317
38499
  if (state.screen === "results") {
38318
38500
  const items = rowsOf(state.result);
38319
- const list = state.tool?.name === "lists.get" && state.input.id && Array.isArray(state.result.lists) ? state.result.lists[0] : void 0;
38320
- content = /* @__PURE__ */ jsxs(Fragment, { children: [
38321
- /* @__PURE__ */ jsx(Text, { bold: true, children: state.tool?.name ?? "Enrichment job" }),
38322
- list && /* @__PURE__ */ jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [
38323
- clean(list.name),
38324
- " / ",
38325
- clean(list.type),
38326
- " / ",
38327
- list.recurringEnabled ? "Each release" : "On demand",
38328
- " / ",
38329
- clean(list.maxRecords),
38330
- " records / ",
38331
- clean(list.maxNewRecordsPerRefresh),
38332
- " new records per refresh"
38333
- ] }),
38334
- items.length ? /* @__PURE__ */ jsx(Menu, { items: items.map((row) => ({ label: rowLabel(row), hint: row.matchesCriteria === false ? "No longer matches" : clean(row.status ?? row.jobTitle ?? row.normalizedDomain ?? row.domain ?? row.headquartersCountryCode) })), selected: state.selected, limit: limit - (list ? 3 : 2), color, width: width - 4 }) : recordFields(state.result).slice(state.selected, state.selected + limit).map(([key, value]) => /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
38335
- key,
38336
- ": ",
38337
- value
38338
- ] }, key)),
38339
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
38340
- items.length ? `${items.length} on this page. ` : "",
38341
- state.result.nextCursor ? "More records available." : "End of page."
38342
- ] })
38343
- ] });
38344
- help = "Enter detail. N next page. R refresh. D download. E edit inputs. Esc back.";
38345
- if (!state.tool) help = "E installation options. Esc home. ";
38346
- if (state.tool?.name === "searches.list") help = "Enter reopen search. A create audience. N next page. E edit. Esc back.";
38347
- if (state.tool?.name === "lists.get" && state.input.id) help += " P preview refresh.";
38501
+ if (state.tool?.name === "lookup.find") {
38502
+ content = /* @__PURE__ */ jsxs(Fragment, { children: [
38503
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
38504
+ "Look up: ",
38505
+ clean(state.input.query)
38506
+ ] }),
38507
+ items.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
38508
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "KIND / NAME / DETAILS" }),
38509
+ /* @__PURE__ */ jsx(Menu, { items: items.map((row) => {
38510
+ const match = lookupMatchColumns(row);
38511
+ return { label: `${match.kind}: ${match.name}`, hint: match.details };
38512
+ }), selected: state.selected, limit: limit - 2, color, width: width - 4 })
38513
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
38514
+ /* @__PURE__ */ jsxs(Text, { children: [
38515
+ "No match for ",
38516
+ clean(state.input.query)
38517
+ ] }),
38518
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Try Search to find companies or people with filters." })
38519
+ ] })
38520
+ ] });
38521
+ help = "Enter detail. E edit lookup. Esc home.";
38522
+ } else {
38523
+ const list = state.tool?.name === "lists.get" && state.input.id && Array.isArray(state.result.lists) ? state.result.lists[0] : void 0;
38524
+ content = /* @__PURE__ */ jsxs(Fragment, { children: [
38525
+ /* @__PURE__ */ jsx(Text, { bold: true, children: state.tool?.name ?? "Enrichment job" }),
38526
+ list && /* @__PURE__ */ jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [
38527
+ clean(list.name),
38528
+ " / ",
38529
+ clean(list.type),
38530
+ " / ",
38531
+ list.recurringEnabled ? "Each release" : "On demand",
38532
+ " / ",
38533
+ clean(list.maxRecords),
38534
+ " records / ",
38535
+ clean(list.maxNewRecordsPerRefresh),
38536
+ " new records per refresh"
38537
+ ] }),
38538
+ items.length ? /* @__PURE__ */ jsx(Menu, { items: items.map((row) => ({ label: rowLabel(row), hint: row.matchesCriteria === false ? "No longer matches" : clean(row.status ?? row.jobTitle ?? row.normalizedDomain ?? row.domain ?? row.headquartersCountryCode) })), selected: state.selected, limit: limit - (list ? 3 : 2), color, width: width - 4 }) : recordFields(state.result).slice(state.selected, state.selected + limit).map(([key, value]) => /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
38539
+ key,
38540
+ ": ",
38541
+ value
38542
+ ] }, key)),
38543
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
38544
+ items.length ? `${items.length} on this page. ` : "",
38545
+ state.result.nextCursor ? "More records available." : "End of page."
38546
+ ] })
38547
+ ] });
38548
+ help = "Enter detail. N next page. R refresh. D download. E edit inputs. Esc back.";
38549
+ if (!state.tool) help = "E installation options. Esc home. ";
38550
+ if (state.tool?.name === "searches.list") help = "Enter reopen search. A create audience. N next page. E edit. Esc back.";
38551
+ if (state.tool?.name === "lists.get" && state.input.id) help += " P preview refresh.";
38552
+ }
38348
38553
  }
38349
38554
  if (state.screen === "detail") {
38350
38555
  const row = detailRecord(state.row), isCore = coreRecord(state.row);
@@ -38391,26 +38596,45 @@ function TerminalScreen({ state, columns, rows = 28, colors = true, profile = "d
38391
38596
  help = "Enter sends. Ctrl+B step limit. Ctrl+N new conversation. Esc home.";
38392
38597
  }
38393
38598
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, borderStyle: "double", paddingX: 1, children: [
38394
- /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
38599
+ largeHome ? /* @__PURE__ */ jsxs(Fragment, { children: [
38600
+ /* @__PURE__ */ jsxs(Box, { flexDirection: brand.sideBySide ? "row" : "column", gap: brand.sideBySide ? 3 : 1, children: [
38601
+ /* @__PURE__ */ jsx(Wordmark, { color }),
38602
+ /* @__PURE__ */ jsx(Mark, { lines: markFrame("large"), color })
38603
+ ] }),
38604
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [
38605
+ "by HigherOps / ",
38606
+ clean(profile),
38607
+ " / ",
38608
+ clean(workspace),
38609
+ " / ",
38610
+ clean(space),
38611
+ " / ",
38612
+ clean(remaining),
38613
+ " email verifications remaining"
38614
+ ] })
38615
+ ] }) : /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
38395
38616
  /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
38396
- /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: SMALL_MARK.map((line, i) => /* @__PURE__ */ jsx(Text, { color, children: line }, i)) }),
38617
+ /* @__PURE__ */ jsx(Mark, { lines: markFrame("compact"), color }),
38397
38618
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
38398
- /* @__PURE__ */ jsx(Text, { bold: true, color, children: "HIGHERDATA" }),
38619
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
38620
+ "Higher",
38621
+ /* @__PURE__ */ jsx(Text, { color, children: "Data" })
38622
+ ] }),
38399
38623
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "by HigherOps" }),
38400
38624
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: clean(profile) })
38401
38625
  ] })
38402
38626
  ] }),
38403
- /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "flex-end", children: [
38627
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "flex-end", flexShrink: 1, children: [
38404
38628
  /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", children: clean(workspace) }),
38405
38629
  /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "truncate-end", children: clean(space) }),
38406
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
38630
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [
38407
38631
  clean(remaining),
38408
38632
  " email verifications remaining"
38409
38633
  ] })
38410
38634
  ] })
38411
38635
  ] }),
38412
38636
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2550".repeat(Math.max(1, width - 4)) }),
38413
- /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: width - 4, minHeight: Math.min(rows - 7, 16), children: content }),
38637
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: width - 4, minHeight: largeHome ? 0 : Math.max(0, Math.min(rows - 16, 13)), children: content }),
38414
38638
  state.error && /* @__PURE__ */ jsx(Text, { color: colors ? "red" : void 0, children: clean(state.error) }),
38415
38639
  state.notice && state.screen !== "busy" && /* @__PURE__ */ jsx(Text, { dimColor: true, children: clean(state.notice) }),
38416
38640
  /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "wrap", children: help })
@@ -38420,6 +38644,7 @@ var initialState, accountItems, homeItems;
38420
38644
  var init_screens = __esm({
38421
38645
  "src/terminal/screens.tsx"() {
38422
38646
  "use strict";
38647
+ init_output();
38423
38648
  init_tool_commands();
38424
38649
  init_banner();
38425
38650
  init_install_menu();
@@ -38433,8 +38658,8 @@ var init_screens = __esm({
38433
38658
  { id: "skills-install", label: "Install skills", hint: "Five task guides for Claude Code or Codex" },
38434
38659
  { id: "quit", label: "Quit", hint: "" }
38435
38660
  ];
38436
- homeItems = [["Find", ["search", "searches", "lookup", "icp", "ask"]], ["Work", ["lists", "enrich", "email", "jobs"]], ["Govern", ["suppress", "crm", "changes", "hygiene"]], ["Account", ["account"]]].flatMap(([section, ids]) => ids.map((id, index) => {
38437
- const group = GROUPS.find(([name]) => name === id);
38661
+ homeItems = [["Find", ["find", "search", "searches", "lookup", "icp", "ask"]], ["Work", ["lists", "enrich", "email", "jobs"]], ["Govern", ["suppress", "crm", "changes", "hygiene"]], ["Account", ["account"]]].flatMap(([section, ids]) => ids.map((id, index) => {
38662
+ const group = id === "find" ? ["find", "Look up", "Name, email, phone, domain, LinkedIn or record id"] : GROUPS.find(([name]) => name === id);
38438
38663
  return { id, label: group?.[1] ?? "Ask HigherData", hint: group?.[2] ?? "Plan a request, limit its steps", section: index === 0 ? section : "" };
38439
38664
  }));
38440
38665
  }
@@ -38449,25 +38674,25 @@ __export(app_exports, {
38449
38674
  });
38450
38675
  import { randomUUID as randomUUID6 } from "crypto";
38451
38676
  import { createReadStream as createReadStream2 } from "fs";
38452
- import { open as open6 } from "fs/promises";
38453
- import { useEffect, useRef, useState } from "react";
38677
+ import { open as open7 } from "fs/promises";
38678
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
38454
38679
  import { render, useApp, useInput, useWindowSize } from "ink";
38455
38680
  import { jsx as jsx2 } from "react/jsx-runtime";
38456
38681
  function InteractiveApp({ session }) {
38457
38682
  const { exit } = useApp(), { columns, rows } = useWindowSize();
38458
38683
  const settings = session.settings();
38459
- const [chrome, setChrome] = useState({ workspace: "Workspace", remaining: "--" });
38460
- const [state, setState] = useState(() => ({ ...initialState(), screen: shouldAnimate(true, session.options, session.runtime.env) ? "banner" : "home" }));
38684
+ const [chrome, setChrome] = useState2({ workspace: "Workspace", remaining: "--" });
38685
+ const [state, setState] = useState2(() => ({ ...initialState(), screen: shouldAnimate(true, session.options, session.runtime.env) ? "banner" : "home" }));
38461
38686
  const current = useRef(state);
38462
38687
  current.current = state;
38463
38688
  const mounted = useRef(true), request = useRef(false), back = useRef(initialState());
38464
- useEffect(() => {
38689
+ useEffect2(() => {
38465
38690
  mounted.current = true;
38466
38691
  return () => {
38467
38692
  mounted.current = false;
38468
38693
  };
38469
38694
  }, []);
38470
- useEffect(() => {
38695
+ useEffect2(() => {
38471
38696
  let active = true;
38472
38697
  if (!settings.apiKey || !settings.space) return;
38473
38698
  void Promise.all([session.call("account.whoami", {}), session.call("account.usage", {})]).then(([identity, usage]) => {
@@ -38480,12 +38705,12 @@ function InteractiveApp({ session }) {
38480
38705
  active = false;
38481
38706
  };
38482
38707
  }, [session, settings.apiKey, settings.space]);
38483
- useEffect(() => {
38708
+ useEffect2(() => {
38484
38709
  if (state.screen !== "banner") return;
38485
38710
  const timer = setInterval(() => setState((s) => s.frame >= BANNER_FRAMES - 1 ? { ...s, screen: "home" } : { ...s, frame: s.frame + 1 }), BANNER_DURATION_MS / BANNER_FRAMES);
38486
38711
  return () => clearInterval(timer);
38487
38712
  }, [state.screen]);
38488
- useEffect(() => {
38713
+ useEffect2(() => {
38489
38714
  const job = state.result.job;
38490
38715
  const id = job?.id ?? (state.tool?.name === "jobs.get" ? state.result.id : void 0);
38491
38716
  const status = job?.status ?? state.result.status;
@@ -38543,6 +38768,7 @@ function InteractiveApp({ session }) {
38543
38768
  function activate(item) {
38544
38769
  const s = current.current;
38545
38770
  if (item.id === "quit") exit();
38771
+ else if (item.id === "find") patch({ screen: "lookup", tool: toolByName("lookup.find"), input: {}, text: "", selected: 0 });
38546
38772
  else if (item.id === "mcp-install" || item.id === "skills-install") patch({ screen: "install", group: item.id === "mcp-install" ? "mcp" : "skills", tool: null, input: { agent: "claude", scope: "user", profile: "default" }, selected: 0 });
38547
38773
  else if (item.id === "space") {
38548
38774
  back.current = s;
@@ -38569,12 +38795,27 @@ function InteractiveApp({ session }) {
38569
38795
  return;
38570
38796
  }
38571
38797
  if (s.screen === "busy") return;
38798
+ if (s.screen === "home" && input2 === "/") {
38799
+ activate({ id: "find" });
38800
+ return;
38801
+ }
38572
38802
  if (key.escape && s.screen === "filter-group") {
38573
38803
  patch({ screen: "form", selected: searchGroups(s.tool.input, s.input).findIndex((group) => group.group === s.filterGroup) });
38574
38804
  return;
38575
38805
  }
38576
38806
  if (key.escape) {
38577
- patch(s.screen === "home" ? { screen: "home" } : ["edit", "download", "space", "upload"].includes(s.screen) ? { ...back.current } : { screen: s.screen === "detail" ? "results" : s.screen === "form" ? "tools" : "home", selected: 0 });
38807
+ patch(s.screen === "home" ? { screen: "home" } : ["edit", "download", "download-choice", "space", "upload"].includes(s.screen) ? { ...back.current } : { screen: s.screen === "detail" ? "results" : s.screen === "form" ? "tools" : "home", selected: 0 });
38808
+ return;
38809
+ }
38810
+ if (s.screen === "lookup") {
38811
+ if (key.return && s.text.trim()) void work("Looking up your record...", () => session.lookup(s.text));
38812
+ else if (key.backspace || key.delete) patch({ text: s.text.slice(0, -1) });
38813
+ else if (!key.return && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow) patch({ text: s.text + input2 });
38814
+ return;
38815
+ }
38816
+ if (s.screen === "download-choice") {
38817
+ if (key.upArrow || key.downArrow) patch({ selected: s.selected === 0 ? 1 : 0 });
38818
+ else if (key.return) patch({ screen: "download", field: "New destination .csv or .jsonl file", text: "" });
38578
38819
  return;
38579
38820
  }
38580
38821
  if (["edit", "download", "space", "upload"].includes(s.screen)) {
@@ -38588,8 +38829,10 @@ function InteractiveApp({ session }) {
38588
38829
  }
38589
38830
  if (s.screen === "download") {
38590
38831
  void work("Downloading into a new file...", async () => {
38591
- await session.download(back.current, s.text);
38592
- return { ...back.current, notice: `Saved ${s.text}` };
38832
+ const notice = await session.download(back.current, s.text, { all: isSearchResults(back.current.tool?.name) && s.selected === 1, progress: (text2) => {
38833
+ if (mounted.current) patch({ notice: text2 });
38834
+ } });
38835
+ return { ...back.current, notice: `Saved ${s.text}${notice ? `. ${notice}` : ""}` };
38593
38836
  });
38594
38837
  return;
38595
38838
  }
@@ -38698,15 +38941,15 @@ function InteractiveApp({ session }) {
38698
38941
  patch({ input: { ...s.input, cursor: s.result.nextCursor } });
38699
38942
  void work("Loading the next page...", async () => ({ screen: "results", selected: 0, input: { ...s.input, cursor: s.result.nextCursor }, result: await session.call(s.tool.name, { ...s.input, cursor: s.result.nextCursor }) }));
38700
38943
  } else if (input2 === "r" && s.tool) runTool(!needsConfirmation(s.tool));
38701
- else if (input2 === "e") patch({ screen: s.tool ? "form" : "install", selected: 0 });
38944
+ else if (input2 === "e") patch({ screen: s.tool?.name === "lookup.find" ? "lookup" : s.tool ? "form" : "install", text: s.tool?.name === "lookup.find" ? String(s.input.query ?? "") : s.text, selected: 0 });
38702
38945
  else if (input2 === "d") {
38703
38946
  back.current = s;
38704
- patch({ screen: "download", field: "New destination .csv or .jsonl file", text: "" });
38947
+ patch({ screen: isSearchResults(s.tool?.name) ? "download-choice" : "download", selected: 0, field: "New destination .csv or .jsonl file", text: "" });
38705
38948
  } else if (input2 === "a" && s.tool?.name === "searches.list") {
38706
38949
  const row = rowsOf(s.result)[s.selected];
38707
38950
  if (row) form("lists.create", { type: "audience", savedSearchId: row.id, recurringEnabled: true, maxRecords: 1e3, maxNewRecordsPerRefresh: 1e3, removeNonmatches: false });
38708
38951
  } else if (input2 === "p" && s.tool?.name === "lists.get" && typeof s.input.id === "string") form("lists.refresh", { id: s.input.id, maxNewRecords: 1e3 });
38709
- else if (key.return) {
38952
+ else if (key.return && (rowsOf(s.result).length || s.tool?.name !== "lookup.find")) {
38710
38953
  void work("Opening the record...", () => session.openRow(s));
38711
38954
  }
38712
38955
  } else if (s.screen === "detail") {
@@ -38736,6 +38979,7 @@ var TerminalSession;
38736
38979
  var init_app = __esm({
38737
38980
  "src/terminal/app.tsx"() {
38738
38981
  "use strict";
38982
+ init_search_download2();
38739
38983
  init_tool_commands();
38740
38984
  init_api2();
38741
38985
  init_config();
@@ -38759,6 +39003,12 @@ var init_app = __esm({
38759
39003
  async load() {
38760
39004
  this.config = await this.runtime.readConfig();
38761
39005
  }
39006
+ async lookup(query) {
39007
+ const result = await this.call("lookup.find", { query });
39008
+ const state = { ...initialState(), screen: "results", tool: toolByName("lookup.find"), input: { query }, result };
39009
+ if (result.exactMatch === true && rowsOf(result).length === 1) return { ...state, ...await this.openRow(state) };
39010
+ return state;
39011
+ }
38762
39012
  settings() {
38763
39013
  return resolveSettings(this.config, this.options, this.runtime.env);
38764
39014
  }
@@ -38788,15 +39038,22 @@ var init_app = __esm({
38788
39038
  this.config.profiles[this.config.activeProfile] = { apiUrl: settings.apiUrl, apiKey: key };
38789
39039
  await this.runtime.writeConfig(this.config);
38790
39040
  }
38791
- async download(state, path2) {
39041
+ async download(state, path2, options = {}) {
38792
39042
  if (!path2.trim()) throw new CliError("Enter a new destination file.");
38793
39043
  path2 = path2.trim();
38794
39044
  if (!/\.(csv|jsonl)$/.test(path2)) throw new CliError("Choose a new .csv or .jsonl destination.");
39045
+ if (isSearchResults(state.tool?.name)) {
39046
+ const input2 = { ...state.input };
39047
+ const loadedPage = !options.all || !input2.cursor ? state.result : void 0;
39048
+ if (options.all) delete input2.cursor;
39049
+ const result = await downloadSearch(this.api(), state.tool.name, input2, path2, { ...options, loadedPage, sleep: this.runtime.sleep });
39050
+ return result.capped ? SEARCH_DOWNLOAD_CAP_NOTICE : "";
39051
+ }
38795
39052
  const job = state.result.job;
38796
39053
  const jobId = state.result.jobId ?? job?.id ?? (state.tool?.name.startsWith("jobs.") ? state.result.id ?? state.input.jobId : void 0);
38797
39054
  const exportTool = state.tool?.name === "lists.get" && state.input.id ? "lists.export" : state.tool?.name;
38798
39055
  if (typeof jobId !== "string" && !["lists.export", "suppress.export"].includes(exportTool ?? "")) throw new CliError("Open a completed job or a list export to download a file.");
38799
- const file2 = await open6(path2.trim(), "wx", 384);
39056
+ const file2 = await open7(path2.trim(), "wx", 384);
38800
39057
  try {
38801
39058
  const format2 = path2.endsWith(".jsonl") ? "jsonl" : "csv";
38802
39059
  if (typeof jobId === "string") await downloadBulkJob(this.api(), jobId, format2, async (text2) => {
@@ -38936,11 +39193,12 @@ async function buildFilters(api, options, kind) {
38936
39193
 
38937
39194
  // src/commands.ts
38938
39195
  init_output();
39196
+ init_search_download2();
38939
39197
 
38940
39198
  // src/pagination.ts
38941
39199
  init_zod();
38942
39200
  init_api2();
38943
- var pageSchema2 = external_exports.object({ items: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).max(50), nextCursor: external_exports.string().min(1).max(2048).nullable(), zeroResults: external_exports.boolean() });
39201
+ var pageSchema3 = external_exports.object({ items: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).max(50), nextCursor: external_exports.string().min(1).max(2048).nullable(), zeroResults: external_exports.boolean() });
38944
39202
  var exportSchema = external_exports.object({ content: external_exports.string(), mimeType: external_exports.enum(["text/csv", "application/x-ndjson"]), nextCursor: external_exports.string().min(1).max(2048).nullable(), zeroResults: external_exports.boolean() });
38945
39203
  async function pagedTool(api, name, input2, all, exporting = false) {
38946
39204
  if (all && api.settings.idempotencyKey) throw new CliError("An idempotency key applies to one page. Omit --all.");
@@ -38953,7 +39211,7 @@ async function pagedTool(api, name, input2, all, exporting = false) {
38953
39211
  let empty = true;
38954
39212
  while (true) {
38955
39213
  const raw = await api.tool(name, { ...input2, ...cursor === void 0 ? {} : { cursor } });
38956
- const page = (exporting ? exportSchema : pageSchema2).safeParse(raw);
39214
+ const page = (exporting ? exportSchema : pageSchema3).safeParse(raw);
38957
39215
  if (!page.success) throw new CliError("The API returned an invalid cursor page.");
38958
39216
  if ("items" in page.data) records.push(...page.data.items);
38959
39217
  else {
@@ -39247,10 +39505,18 @@ function registerCommands(program2, runtime) {
39247
39505
  emit(identity, mode);
39248
39506
  });
39249
39507
  const search = program2.command("search").description("Search HigherData Core");
39250
- for (const kind of ["companies", "people"]) pageOptions(excludeList(addFilterOptions(search.command(kind).description(`Find ${kind}`), kind === "people"))).action(async (options, command) => {
39508
+ for (const kind of ["companies", "people"]) pageOptions(excludeList(addFilterOptions(search.command(kind).description(`Find ${kind}`), kind === "people"))).option("--out <path>", "Download to a new .csv or .jsonl file (up to 5,000 rows with --all)").action(async (options, command) => {
39251
39509
  const { api, mode } = await session(command);
39510
+ if (options.out) searchDownloadFormat(options.out);
39252
39511
  const filters = await buildFilters(api, options, kind);
39253
- emit(await pagedTool(api, `search.${kind}`, { ...filters, limit: options.limit, ...options.cursor ? { cursor: options.cursor } : {}, ...excluded(options) }, Boolean(options.all)), mode);
39512
+ const input2 = { ...filters, limit: options.limit, ...options.cursor ? { cursor: options.cursor } : {}, ...excluded(options) };
39513
+ if (options.out) {
39514
+ const result = await downloadSearch(api, `search.${kind}`, input2, options.out, { all: Boolean(options.all), sleep: runtime.sleep, progress: (text2) => runtime.err(`${text2}
39515
+ `) });
39516
+ if (result.capped) runtime.err(`${SEARCH_DOWNLOAD_CAP_NOTICE}
39517
+ `);
39518
+ emit({ path: options.out, ...result }, mode);
39519
+ } else emit(await pagedTool(api, `search.${kind}`, input2, Boolean(options.all)), mode);
39254
39520
  });
39255
39521
  addFilterOptions(program2.command("count").description("Estimate companies and their contacts")).action(async (options, command) => {
39256
39522
  const { api, mode } = await session(command);
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "higherdata",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "bin": {
6
- "higherdata": "dist/index.js",
7
- "hd": "dist/index.js"
6
+ "higherdata": "dist/index.js"
8
7
  },
9
8
  "files": [
10
9
  "dist",