higherdata 0.2.2 → 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 +2 -0
  2. package/dist/index.js +349 -82
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -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,7 +31926,7 @@ var init_package = __esm({
31914
31926
  "package.json"() {
31915
31927
  package_default = {
31916
31928
  name: "higherdata",
31917
- version: "0.2.2",
31929
+ version: "0.3.0",
31918
31930
  type: "module",
31919
31931
  bin: {
31920
31932
  higherdata: "dist/index.js"
@@ -34894,6 +34906,9 @@ function cell(value) {
34894
34906
  const text2 = value == null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
34895
34907
  return text2.replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ");
34896
34908
  }
34909
+ function lookupMatchColumns(row) {
34910
+ return { kind: cell(row.kind), name: cell(row.label), details: cell(row.detail) };
34911
+ }
34897
34912
  function customerOutput(value) {
34898
34913
  if (Array.isArray(value)) return value.map(customerOutput);
34899
34914
  if (!value || typeof value !== "object") return value;
@@ -34903,10 +34918,10 @@ function renderOutput(value, mode) {
34903
34918
  value = customerOutput(value);
34904
34919
  if (mode === "json") return `${JSON.stringify(value)}
34905
34920
  `;
34906
- 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];
34907
34922
  if (mode === "jsonl") return records.map((record2) => JSON.stringify(record2)).join("\n") + (records.length ? "\n" : "");
34908
34923
  if (!records.length) return "No results.\n";
34909
- const rows = records.map((record2) => record2);
34924
+ const rows = records.map((record2) => Array.isArray(value.matches) ? lookupMatchColumns(record2) : record2);
34910
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]);
34911
34926
  const headings = columns.map((column) => column.replace(/([a-z])([A-Z])/gu, "$1 $2").toUpperCase());
34912
34927
  const widths = columns.map((column, index) => rows.reduce((width, row) => Math.min(64, Math.max(width, cell(row[column]).length)), headings[index].length));
@@ -34922,6 +34937,105 @@ var init_output = __esm({
34922
34937
  }
34923
34938
  });
34924
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
+
34925
35039
  // ../../node_modules/smol-toml/dist/error.js
34926
35040
  function getLineColFromPtr(string4, ptr) {
34927
35041
  let lines = string4.slice(0, ptr).split(/\r?\n/);
@@ -37342,7 +37456,7 @@ var init_main = __esm({
37342
37456
 
37343
37457
  // src/install.ts
37344
37458
  import { constants } from "fs";
37345
- 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";
37346
37460
  import { execFile as execFile2 } from "child_process";
37347
37461
  import { promisify, isDeepStrictEqual } from "util";
37348
37462
  import { homedir as homedir2 } from "os";
@@ -37416,7 +37530,7 @@ async function safeParents(path2) {
37416
37530
  await mkdir3(dirname3(path2), { recursive: true, mode: 448 });
37417
37531
  }
37418
37532
  async function privateFile(path2, text2) {
37419
- const file2 = await open5(path2, "wx", 384);
37533
+ const file2 = await open6(path2, "wx", 384);
37420
37534
  try {
37421
37535
  await file2.writeFile(text2, "utf8");
37422
37536
  } finally {
@@ -38049,7 +38163,7 @@ var init_model = __esm({
38049
38163
  ["account", "Account", "Usage, limits and API keys"],
38050
38164
  ["icp", "ICP", "Build your ideal customer profile"]
38051
38165
  ];
38052
- 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");
38053
38167
  toolByName = (name) => TOOL_CATALOG.find((tool) => tool.name === name);
38054
38168
  clean = (value) => String(value ?? "").replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ");
38055
38169
  title = (value) => value.replaceAll("_", " ").replace(/([a-z])([A-Z])/gu, "$1 $2").replace(/\b\w/gu, (c) => c.toUpperCase());
@@ -38137,19 +38251,43 @@ var init_install_menu = __esm({
38137
38251
  });
38138
38252
 
38139
38253
  // src/terminal/banner.ts
38140
- function bannerFrame(frame, columns) {
38141
- const full = Array.from({ length: 6 }, (_, row) => [..."HIGHERDATA"].map((letter) => GLYPHS[letter][row]).join(" "));
38142
- const visible = frame < 5 ? Math.ceil((frame + 1) / 5 * full[0].length) : full[0].length;
38143
- const indent = Math.max(0, Math.floor((columns - full[0].length) / 2));
38144
- const widths = [24, 12, 18], offsets = [0, 6, 3];
38145
- const logo = widths.flatMap((width, index) => {
38146
- const progress = Math.max(0, Math.min(1, (frame - 5 - index) / 2));
38147
- const line = " ".repeat(Math.max(0, Math.floor((columns - 24) / 2)) + offsets[index]) + "\u2588".repeat(Math.ceil(width * progress));
38148
- 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)) };
38149
38270
  });
38150
- 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());
38151
38271
  }
38152
- 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;
38153
38291
  var init_banner = __esm({
38154
38292
  "src/terminal/banner.ts"() {
38155
38293
  "use strict";
@@ -38165,13 +38303,15 @@ var init_banner = __esm({
38165
38303
  };
38166
38304
  BANNER_FRAMES = 11;
38167
38305
  BANNER_DURATION_MS = 1210;
38168
- 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);
38169
38308
  shouldAnimate = (tty, options, env) => tty && options.banner !== false && !options.json && !options.jsonl && !Object.hasOwn(env, "CI");
38170
38309
  }
38171
38310
  });
38172
38311
 
38173
38312
  // src/terminal/screens.tsx
38174
- import { Box, Text } from "ink";
38313
+ import { useEffect, useState } from "react";
38314
+ import { Box, Text, useStdout } from "ink";
38175
38315
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
38176
38316
  function Menu({ items, selected, limit, color, width }) {
38177
38317
  const start = Math.max(0, Math.min(selected - Math.floor(limit / 2), items.length - limit));
@@ -38194,17 +38334,54 @@ function Menu({ items, selected, limit, color, width }) {
38194
38334
  ] })
38195
38335
  ] });
38196
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
+ }
38197
38356
  function TerminalScreen({ state, columns, rows = 28, colors = true, profile = "default", space = "Default", workspace = "HigherOps", remaining = "--" }) {
38198
38357
  const width = Math.max(40, columns), color = colors ? "#f2b705" : void 0;
38199
- 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;
38200
38361
  if (state.screen === "banner") return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
38201
- 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] || " " }),
38202
38366
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press any key to start" })
38203
38367
  ] });
38204
38368
  const fields = state.tool ? fieldsFor(state.tool.input, state.input, state.variant) : [];
38205
38369
  let help = "Up/Down choose. Enter opens. Esc back. Ctrl+C quit.";
38206
38370
  let content;
38207
- 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
+ }
38208
38385
  if (state.screen === "install") {
38209
38386
  content = /* @__PURE__ */ jsxs(Fragment, { children: [
38210
38387
  /* @__PURE__ */ jsxs(Text, { bold: true, children: [
@@ -38275,6 +38452,12 @@ function TerminalScreen({ state, columns, rows = 28, colors = true, profile = "d
38275
38452
  ] });
38276
38453
  help = "Enter edit/run. Arrays: comma separated. Blank clears. Esc back.";
38277
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
+ }
38278
38461
  if (["edit", "upload", "download", "space"].includes(state.screen)) {
38279
38462
  const field = fields.find((field2) => field2.path === state.field);
38280
38463
  content = /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -38296,7 +38479,7 @@ function TerminalScreen({ state, columns, rows = 28, colors = true, profile = "d
38296
38479
  }
38297
38480
  if (state.screen === "busy") {
38298
38481
  content = /* @__PURE__ */ jsxs(Fragment, { children: [
38299
- SMALL_MARK.map((line, i) => /* @__PURE__ */ jsx(Text, { color, children: line }, i)),
38482
+ /* @__PURE__ */ jsx(BusyMark, { color, size: rows >= 28 ? "large" : "compact" }),
38300
38483
  /* @__PURE__ */ jsx(Text, { children: state.notice || "Working..." })
38301
38484
  ] });
38302
38485
  help = "The current operation is running. It is never retried automatically.";
@@ -38315,35 +38498,58 @@ function TerminalScreen({ state, columns, rows = 28, colors = true, profile = "d
38315
38498
  }
38316
38499
  if (state.screen === "results") {
38317
38500
  const items = rowsOf(state.result);
38318
- const list = state.tool?.name === "lists.get" && state.input.id && Array.isArray(state.result.lists) ? state.result.lists[0] : void 0;
38319
- content = /* @__PURE__ */ jsxs(Fragment, { children: [
38320
- /* @__PURE__ */ jsx(Text, { bold: true, children: state.tool?.name ?? "Enrichment job" }),
38321
- list && /* @__PURE__ */ jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [
38322
- clean(list.name),
38323
- " / ",
38324
- clean(list.type),
38325
- " / ",
38326
- list.recurringEnabled ? "Each release" : "On demand",
38327
- " / ",
38328
- clean(list.maxRecords),
38329
- " records / ",
38330
- clean(list.maxNewRecordsPerRefresh),
38331
- " new records per refresh"
38332
- ] }),
38333
- 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: [
38334
- key,
38335
- ": ",
38336
- value
38337
- ] }, key)),
38338
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
38339
- items.length ? `${items.length} on this page. ` : "",
38340
- state.result.nextCursor ? "More records available." : "End of page."
38341
- ] })
38342
- ] });
38343
- help = "Enter detail. N next page. R refresh. D download. E edit inputs. Esc back.";
38344
- if (!state.tool) help = "E installation options. Esc home. ";
38345
- if (state.tool?.name === "searches.list") help = "Enter reopen search. A create audience. N next page. E edit. Esc back.";
38346
- 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
+ }
38347
38553
  }
38348
38554
  if (state.screen === "detail") {
38349
38555
  const row = detailRecord(state.row), isCore = coreRecord(state.row);
@@ -38390,26 +38596,45 @@ function TerminalScreen({ state, columns, rows = 28, colors = true, profile = "d
38390
38596
  help = "Enter sends. Ctrl+B step limit. Ctrl+N new conversation. Esc home.";
38391
38597
  }
38392
38598
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, borderStyle: "double", paddingX: 1, children: [
38393
- /* @__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: [
38394
38616
  /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
38395
- /* @__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 }),
38396
38618
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
38397
- /* @__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
+ ] }),
38398
38623
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "by HigherOps" }),
38399
38624
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: clean(profile) })
38400
38625
  ] })
38401
38626
  ] }),
38402
- /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "flex-end", children: [
38627
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "flex-end", flexShrink: 1, children: [
38403
38628
  /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", children: clean(workspace) }),
38404
38629
  /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "truncate-end", children: clean(space) }),
38405
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
38630
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [
38406
38631
  clean(remaining),
38407
38632
  " email verifications remaining"
38408
38633
  ] })
38409
38634
  ] })
38410
38635
  ] }),
38411
38636
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2550".repeat(Math.max(1, width - 4)) }),
38412
- /* @__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 }),
38413
38638
  state.error && /* @__PURE__ */ jsx(Text, { color: colors ? "red" : void 0, children: clean(state.error) }),
38414
38639
  state.notice && state.screen !== "busy" && /* @__PURE__ */ jsx(Text, { dimColor: true, children: clean(state.notice) }),
38415
38640
  /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "wrap", children: help })
@@ -38419,6 +38644,7 @@ var initialState, accountItems, homeItems;
38419
38644
  var init_screens = __esm({
38420
38645
  "src/terminal/screens.tsx"() {
38421
38646
  "use strict";
38647
+ init_output();
38422
38648
  init_tool_commands();
38423
38649
  init_banner();
38424
38650
  init_install_menu();
@@ -38432,8 +38658,8 @@ var init_screens = __esm({
38432
38658
  { id: "skills-install", label: "Install skills", hint: "Five task guides for Claude Code or Codex" },
38433
38659
  { id: "quit", label: "Quit", hint: "" }
38434
38660
  ];
38435
- 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) => {
38436
- 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);
38437
38663
  return { id, label: group?.[1] ?? "Ask HigherData", hint: group?.[2] ?? "Plan a request, limit its steps", section: index === 0 ? section : "" };
38438
38664
  }));
38439
38665
  }
@@ -38448,25 +38674,25 @@ __export(app_exports, {
38448
38674
  });
38449
38675
  import { randomUUID as randomUUID6 } from "crypto";
38450
38676
  import { createReadStream as createReadStream2 } from "fs";
38451
- import { open as open6 } from "fs/promises";
38452
- 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";
38453
38679
  import { render, useApp, useInput, useWindowSize } from "ink";
38454
38680
  import { jsx as jsx2 } from "react/jsx-runtime";
38455
38681
  function InteractiveApp({ session }) {
38456
38682
  const { exit } = useApp(), { columns, rows } = useWindowSize();
38457
38683
  const settings = session.settings();
38458
- const [chrome, setChrome] = useState({ workspace: "Workspace", remaining: "--" });
38459
- 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" }));
38460
38686
  const current = useRef(state);
38461
38687
  current.current = state;
38462
38688
  const mounted = useRef(true), request = useRef(false), back = useRef(initialState());
38463
- useEffect(() => {
38689
+ useEffect2(() => {
38464
38690
  mounted.current = true;
38465
38691
  return () => {
38466
38692
  mounted.current = false;
38467
38693
  };
38468
38694
  }, []);
38469
- useEffect(() => {
38695
+ useEffect2(() => {
38470
38696
  let active = true;
38471
38697
  if (!settings.apiKey || !settings.space) return;
38472
38698
  void Promise.all([session.call("account.whoami", {}), session.call("account.usage", {})]).then(([identity, usage]) => {
@@ -38479,12 +38705,12 @@ function InteractiveApp({ session }) {
38479
38705
  active = false;
38480
38706
  };
38481
38707
  }, [session, settings.apiKey, settings.space]);
38482
- useEffect(() => {
38708
+ useEffect2(() => {
38483
38709
  if (state.screen !== "banner") return;
38484
38710
  const timer = setInterval(() => setState((s) => s.frame >= BANNER_FRAMES - 1 ? { ...s, screen: "home" } : { ...s, frame: s.frame + 1 }), BANNER_DURATION_MS / BANNER_FRAMES);
38485
38711
  return () => clearInterval(timer);
38486
38712
  }, [state.screen]);
38487
- useEffect(() => {
38713
+ useEffect2(() => {
38488
38714
  const job = state.result.job;
38489
38715
  const id = job?.id ?? (state.tool?.name === "jobs.get" ? state.result.id : void 0);
38490
38716
  const status = job?.status ?? state.result.status;
@@ -38542,6 +38768,7 @@ function InteractiveApp({ session }) {
38542
38768
  function activate(item) {
38543
38769
  const s = current.current;
38544
38770
  if (item.id === "quit") exit();
38771
+ else if (item.id === "find") patch({ screen: "lookup", tool: toolByName("lookup.find"), input: {}, text: "", selected: 0 });
38545
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 });
38546
38773
  else if (item.id === "space") {
38547
38774
  back.current = s;
@@ -38568,12 +38795,27 @@ function InteractiveApp({ session }) {
38568
38795
  return;
38569
38796
  }
38570
38797
  if (s.screen === "busy") return;
38798
+ if (s.screen === "home" && input2 === "/") {
38799
+ activate({ id: "find" });
38800
+ return;
38801
+ }
38571
38802
  if (key.escape && s.screen === "filter-group") {
38572
38803
  patch({ screen: "form", selected: searchGroups(s.tool.input, s.input).findIndex((group) => group.group === s.filterGroup) });
38573
38804
  return;
38574
38805
  }
38575
38806
  if (key.escape) {
38576
- 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: "" });
38577
38819
  return;
38578
38820
  }
38579
38821
  if (["edit", "download", "space", "upload"].includes(s.screen)) {
@@ -38587,8 +38829,10 @@ function InteractiveApp({ session }) {
38587
38829
  }
38588
38830
  if (s.screen === "download") {
38589
38831
  void work("Downloading into a new file...", async () => {
38590
- await session.download(back.current, s.text);
38591
- 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}` : ""}` };
38592
38836
  });
38593
38837
  return;
38594
38838
  }
@@ -38697,15 +38941,15 @@ function InteractiveApp({ session }) {
38697
38941
  patch({ input: { ...s.input, cursor: s.result.nextCursor } });
38698
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 }) }));
38699
38943
  } else if (input2 === "r" && s.tool) runTool(!needsConfirmation(s.tool));
38700
- 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 });
38701
38945
  else if (input2 === "d") {
38702
38946
  back.current = s;
38703
- 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: "" });
38704
38948
  } else if (input2 === "a" && s.tool?.name === "searches.list") {
38705
38949
  const row = rowsOf(s.result)[s.selected];
38706
38950
  if (row) form("lists.create", { type: "audience", savedSearchId: row.id, recurringEnabled: true, maxRecords: 1e3, maxNewRecordsPerRefresh: 1e3, removeNonmatches: false });
38707
38951
  } else if (input2 === "p" && s.tool?.name === "lists.get" && typeof s.input.id === "string") form("lists.refresh", { id: s.input.id, maxNewRecords: 1e3 });
38708
- else if (key.return) {
38952
+ else if (key.return && (rowsOf(s.result).length || s.tool?.name !== "lookup.find")) {
38709
38953
  void work("Opening the record...", () => session.openRow(s));
38710
38954
  }
38711
38955
  } else if (s.screen === "detail") {
@@ -38735,6 +38979,7 @@ var TerminalSession;
38735
38979
  var init_app = __esm({
38736
38980
  "src/terminal/app.tsx"() {
38737
38981
  "use strict";
38982
+ init_search_download2();
38738
38983
  init_tool_commands();
38739
38984
  init_api2();
38740
38985
  init_config();
@@ -38758,6 +39003,12 @@ var init_app = __esm({
38758
39003
  async load() {
38759
39004
  this.config = await this.runtime.readConfig();
38760
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
+ }
38761
39012
  settings() {
38762
39013
  return resolveSettings(this.config, this.options, this.runtime.env);
38763
39014
  }
@@ -38787,15 +39038,22 @@ var init_app = __esm({
38787
39038
  this.config.profiles[this.config.activeProfile] = { apiUrl: settings.apiUrl, apiKey: key };
38788
39039
  await this.runtime.writeConfig(this.config);
38789
39040
  }
38790
- async download(state, path2) {
39041
+ async download(state, path2, options = {}) {
38791
39042
  if (!path2.trim()) throw new CliError("Enter a new destination file.");
38792
39043
  path2 = path2.trim();
38793
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
+ }
38794
39052
  const job = state.result.job;
38795
39053
  const jobId = state.result.jobId ?? job?.id ?? (state.tool?.name.startsWith("jobs.") ? state.result.id ?? state.input.jobId : void 0);
38796
39054
  const exportTool = state.tool?.name === "lists.get" && state.input.id ? "lists.export" : state.tool?.name;
38797
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.");
38798
- const file2 = await open6(path2.trim(), "wx", 384);
39056
+ const file2 = await open7(path2.trim(), "wx", 384);
38799
39057
  try {
38800
39058
  const format2 = path2.endsWith(".jsonl") ? "jsonl" : "csv";
38801
39059
  if (typeof jobId === "string") await downloadBulkJob(this.api(), jobId, format2, async (text2) => {
@@ -38935,11 +39193,12 @@ async function buildFilters(api, options, kind) {
38935
39193
 
38936
39194
  // src/commands.ts
38937
39195
  init_output();
39196
+ init_search_download2();
38938
39197
 
38939
39198
  // src/pagination.ts
38940
39199
  init_zod();
38941
39200
  init_api2();
38942
- 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() });
38943
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() });
38944
39203
  async function pagedTool(api, name, input2, all, exporting = false) {
38945
39204
  if (all && api.settings.idempotencyKey) throw new CliError("An idempotency key applies to one page. Omit --all.");
@@ -38952,7 +39211,7 @@ async function pagedTool(api, name, input2, all, exporting = false) {
38952
39211
  let empty = true;
38953
39212
  while (true) {
38954
39213
  const raw = await api.tool(name, { ...input2, ...cursor === void 0 ? {} : { cursor } });
38955
- const page = (exporting ? exportSchema : pageSchema2).safeParse(raw);
39214
+ const page = (exporting ? exportSchema : pageSchema3).safeParse(raw);
38956
39215
  if (!page.success) throw new CliError("The API returned an invalid cursor page.");
38957
39216
  if ("items" in page.data) records.push(...page.data.items);
38958
39217
  else {
@@ -39246,10 +39505,18 @@ function registerCommands(program2, runtime) {
39246
39505
  emit(identity, mode);
39247
39506
  });
39248
39507
  const search = program2.command("search").description("Search HigherData Core");
39249
- 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) => {
39250
39509
  const { api, mode } = await session(command);
39510
+ if (options.out) searchDownloadFormat(options.out);
39251
39511
  const filters = await buildFilters(api, options, kind);
39252
- 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);
39253
39520
  });
39254
39521
  addFilterOptions(program2.command("count").description("Estimate companies and their contacts")).action(async (options, command) => {
39255
39522
  const { api, mode } = await session(command);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "higherdata",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "higherdata": "dist/index.js"