higherdata 0.2.2 → 0.3.1

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 +32 -3
  2. package/dist/index.js +544 -89
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -213,12 +213,41 @@ Formats follow the current [Claude Code MCP](https://code.claude.com/docs/en/mcp
213
213
  ## Compatibility
214
214
 
215
215
  The CLI reads public `/api/v1/meta` once per day per API origin and caches the
216
- policy beside its config file. Updates are announced on stderr so JSON output
217
- stays machine-readable. Versions below the server's minimum refuse commands
218
- with `npm install --global higherdata@latest` as the upgrade command. `login`,
216
+ policy beside its config file. Update notices are written to stderr only during interactive invocations.
217
+ Versions below the server's minimum refuse commands and suggest `higherdata update`. `update`, `login`,
219
218
  `--help` and `--version` remain available without a compatibility check.
220
219
 
221
220
  The metadata request times out after three seconds. On network failure the CLI
222
221
  uses its last known policy; with no cached policy it allows the command. Failed
223
222
  checks are cached for a day too. A read-only cache directory allows commands but
224
223
  cannot persist the daily check, so subsequent invocations may check again.
224
+
225
+ 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.
226
+
227
+ ## Updates
228
+
229
+ Run `higherdata update` (alias `higherdata upgrade`) to check npm and install the
230
+ latest stable release. The CLI installs the exact version it checked and prints
231
+ the old and new versions. A permission failure prints npm's error summary and
232
+ the manual command. It never retries with sudo.
233
+
234
+ Interactive npm global installs check npm on every run with a 1.5-second timeout, then
235
+ automatically install a newer release and restart the same command with the
236
+ same arguments. Failed installs print a manual command and continue on the
237
+ current version. Timeouts and network failures continue silently. There is no
238
+ cache for this npm release check.
239
+
240
+ Automatic updates are skipped when either stdin or stdout is not a TTY, `CI`
241
+ is set, `--json` or `--jsonl` is used, for MCP or stdio protocols, and for `npx`
242
+ or non-global installs. To opt out for an invocation, set
243
+ `HIGHERDATA_NO_AUTO_UPDATE=1`. To persist the preference, run:
244
+
245
+ ```sh
246
+ higherdata config set auto-update false
247
+ # Re-enable automatic updates
248
+ higherdata config set auto-update true
249
+ ```
250
+
251
+ Explicit `higherdata update` remains available with an opt-out or a server
252
+ minimum-version refusal. Users on 0.3.0 must reinstall once to obtain the update
253
+ command: `npm install --global higherdata@latest`.
package/dist/index.js CHANGED
@@ -20573,6 +20573,7 @@ var init_config = __esm({
20573
20573
  space: external_exports.string().min(1).optional()
20574
20574
  });
20575
20575
  configSchema = external_exports.strictObject({
20576
+ autoUpdate: external_exports.boolean().optional(),
20576
20577
  activeProfile: external_exports.string().min(1),
20577
20578
  profiles: external_exports.record(external_exports.string().min(1), profileSchema)
20578
20579
  }).refine((config2) => Object.hasOwn(config2.profiles, config2.activeProfile), {
@@ -20587,6 +20588,16 @@ var init_config = __esm({
20587
20588
  }
20588
20589
  });
20589
20590
 
20591
+ // ../../src/lib/ai/messages.ts
20592
+ var OWN_KEY_REQUIRED, OWN_KEY_REJECTED;
20593
+ var init_messages = __esm({
20594
+ "../../src/lib/ai/messages.ts"() {
20595
+ "use strict";
20596
+ OWN_KEY_REQUIRED = "Add your OpenRouter key in Settings to use AI features.";
20597
+ OWN_KEY_REJECTED = "Your OpenRouter key was rejected. Update it in Settings.";
20598
+ }
20599
+ });
20600
+
20590
20601
  // ../sdk/src/generated.ts
20591
20602
  var TOOL_CATALOG;
20592
20603
  var init_generated = __esm({
@@ -28133,6 +28144,10 @@ function retryAfter(response, data, now) {
28133
28144
  const date5 = Date.parse(value);
28134
28145
  if (Number.isFinite(date5)) return Math.max(0, (date5 - now) / 1e3);
28135
28146
  }
28147
+ if (response.status === 429) {
28148
+ const reset = response.headers.get("ratelimit-reset");
28149
+ if (reset !== null && /^\d+(?:\.\d+)?$/.test(reset.trim())) return Number(reset);
28150
+ }
28136
28151
  const parsed = failure2.safeParse(data);
28137
28152
  return parsed.success && typeof parsed.data.error !== "string" ? parsed.data.error.retry_after : void 0;
28138
28153
  }
@@ -28308,6 +28323,7 @@ function apiFailure(status, data) {
28308
28323
  if (code === "limit_exceeded" && message && /^This month's email verification allowance is reached\. Resets \d{4}-\d{2}-\d{2}\. Upgrade: (https:\/\/[^\s]+|\/docs\/plans-and-limits)$/.test(message)) return new CliError(message, EXIT_CODES.LIMIT);
28309
28324
  if (status === 429 || code === "limit_exceeded") return new CliError("A HigherData limit was reached. Wait until the limit resets.", EXIT_CODES.LIMIT);
28310
28325
  if (code === "suppressed" || code === "suppressed_only") return new CliError("All requested records are suppressed.", EXIT_CODES.SUPPRESSED_ONLY);
28326
+ if (code === "ai_key_required" || code === "ai_key_rejected") return new CliError(code === "ai_key_required" ? OWN_KEY_REQUIRED : OWN_KEY_REJECTED);
28311
28327
  if (code === "needs_approval") return new CliError("This operation needs approval.", EXIT_CODES.NEEDS_APPROVAL);
28312
28328
  if (code === "jobs_unavailable") return new CliError("Background jobs are not configured for this deployment.");
28313
28329
  if (code === "invalid_input") return new CliError("The API rejected the input. Check the command's filters and identifiers.");
@@ -28318,6 +28334,7 @@ var CliError, object2, ApiClient;
28318
28334
  var init_api2 = __esm({
28319
28335
  "src/api.ts"() {
28320
28336
  "use strict";
28337
+ init_messages();
28321
28338
  init_src();
28322
28339
  init_zod();
28323
28340
  init_exit_codes();
@@ -28340,8 +28357,16 @@ var init_api2 = __esm({
28340
28357
  async request(path2, input2, options = {}) {
28341
28358
  if (options.authenticated && !this.settings.apiKey) throw new CliError("Run higherdata login or set HIGHERDATA_API_KEY.", EXIT_CODES.AUTH);
28342
28359
  try {
28343
- const response = await this.transport.request(path2, input2, { ...options, idempotencyKey: this.settings.idempotencyKey });
28344
- return { status: response.status, data: response.data };
28360
+ for (let attempt = 0; ; attempt++) {
28361
+ try {
28362
+ return await this.transport.request(path2, input2, { ...options, idempotencyKey: this.settings.idempotencyKey });
28363
+ } catch (error63) {
28364
+ if (!(error63 instanceof HigherDataError) || error63.status !== 429 || !options.rateLimitRecovery || attempt >= 5) throw error63;
28365
+ const wait = (error63.retryAfter ?? 1) * 1e3;
28366
+ if (!Number.isFinite(wait) || wait > 3e5) throw error63;
28367
+ await options.rateLimitRecovery.sleep(Math.max(100, wait));
28368
+ }
28369
+ }
28345
28370
  } catch (error63) {
28346
28371
  if (error63 instanceof HigherDataError) {
28347
28372
  if (error63.status) throw apiFailure(error63.status, { error: { code: error63.code, message: error63.message } });
@@ -28469,7 +28494,7 @@ async function checkCliVersion(input2) {
28469
28494
  }
28470
28495
  }
28471
28496
  if (!policy) return;
28472
- const upgrade = "npm install --global higherdata@latest";
28497
+ const upgrade = "higherdata update";
28473
28498
  if (compareVersions(input2.version, policy.minCliVersion) < 0) throw new CliError(`HigherData CLI ${input2.version} is no longer supported; minimum is ${policy.minCliVersion}. Upgrade: ${upgrade}`);
28474
28499
  if (compareVersions(input2.version, policy.latestCliVersion) < 0) input2.warn(`HigherData CLI ${policy.latestCliVersion} is available. Upgrade: ${upgrade}
28475
28500
  `);
@@ -31914,7 +31939,7 @@ var init_package = __esm({
31914
31939
  "package.json"() {
31915
31940
  package_default = {
31916
31941
  name: "higherdata",
31917
- version: "0.2.2",
31942
+ version: "0.3.1",
31918
31943
  type: "module",
31919
31944
  bin: {
31920
31945
  higherdata: "dist/index.js"
@@ -34894,6 +34919,9 @@ function cell(value) {
34894
34919
  const text2 = value == null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
34895
34920
  return text2.replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ");
34896
34921
  }
34922
+ function lookupMatchColumns(row) {
34923
+ return { kind: cell(row.kind), name: cell(row.label), details: cell(row.detail) };
34924
+ }
34897
34925
  function customerOutput(value) {
34898
34926
  if (Array.isArray(value)) return value.map(customerOutput);
34899
34927
  if (!value || typeof value !== "object") return value;
@@ -34903,10 +34931,10 @@ function renderOutput(value, mode) {
34903
34931
  value = customerOutput(value);
34904
34932
  if (mode === "json") return `${JSON.stringify(value)}
34905
34933
  `;
34906
- const records = Array.isArray(value.items) ? value.items : Array.isArray(value.candidates) ? value.candidates : [value];
34934
+ const records = Array.isArray(value.items) ? value.items : Array.isArray(value.candidates) ? value.candidates : Array.isArray(value.matches) ? value.matches : [value];
34907
34935
  if (mode === "jsonl") return records.map((record2) => JSON.stringify(record2)).join("\n") + (records.length ? "\n" : "");
34908
34936
  if (!records.length) return "No results.\n";
34909
- const rows = records.map((record2) => record2);
34937
+ const rows = records.map((record2) => Array.isArray(value.matches) ? lookupMatchColumns(record2) : record2);
34910
34938
  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
34939
  const headings = columns.map((column) => column.replace(/([a-z])([A-Z])/gu, "$1 $2").toUpperCase());
34912
34940
  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 +34950,105 @@ var init_output = __esm({
34922
34950
  }
34923
34951
  });
34924
34952
 
34953
+ // ../../src/lib/csv/search-download.ts
34954
+ function csvCell(value) {
34955
+ let text2 = value === null || value === void 0 ? "" : String(value);
34956
+ if (/^[\s\u0000-\u001f]*[=+\-@]/.test(text2)) text2 = `'${text2}`;
34957
+ return `"${text2.replaceAll('"', '""')}"`;
34958
+ }
34959
+ function pageCsv(headers, rows) {
34960
+ const lines = [headers.map(csvCell).join(","), ...rows.map((row) => row.map(csvCell).join(","))];
34961
+ return `${lines.join("\r\n")}\r
34962
+ `;
34963
+ }
34964
+ function companyPageCsv(items) {
34965
+ return pageCsv(
34966
+ [...COMPANY_DOWNLOAD_FIELDS],
34967
+ items.map((item) => COMPANY_DOWNLOAD_FIELDS.map((field) => item[field]))
34968
+ );
34969
+ }
34970
+ function peoplePageCsv(items) {
34971
+ return pageCsv(
34972
+ [...PEOPLE_DOWNLOAD_FIELDS],
34973
+ items.map((item) => PEOPLE_DOWNLOAD_FIELDS.map((field) => item[field]))
34974
+ );
34975
+ }
34976
+ function searchPageJsonl(kind, items) {
34977
+ const fields = kind === "companies" ? COMPANY_DOWNLOAD_FIELDS : PEOPLE_DOWNLOAD_FIELDS;
34978
+ return items.map((item) => JSON.stringify(Object.fromEntries(fields.map((field) => [field, item[field] ?? null]))) + "\n").join("");
34979
+ }
34980
+ var COMPANY_DOWNLOAD_FIELDS, PEOPLE_DOWNLOAD_FIELDS;
34981
+ var init_search_download = __esm({
34982
+ "../../src/lib/csv/search-download.ts"() {
34983
+ "use strict";
34984
+ COMPANY_DOWNLOAD_FIELDS = ["id", "companyName", "normalizedDomain", "headquartersCountryCode", "industryLinkedin", "employeeCountEstimate"];
34985
+ PEOPLE_DOWNLOAD_FIELDS = ["id", "fullName", "jobTitle", "companyName", "emailAddress", "countryCode"];
34986
+ }
34987
+ });
34988
+
34989
+ // src/search-download.ts
34990
+ import { open as open5 } from "fs/promises";
34991
+ function searchDownloadFormat(path2) {
34992
+ if (!/\.(csv|jsonl)$/.test(path2)) throw new CliError("Choose a new .csv or .jsonl destination.");
34993
+ return path2.endsWith(".jsonl") ? "jsonl" : "csv";
34994
+ }
34995
+ async function downloadSearch(api, name, input2, path2, options) {
34996
+ const format2 = searchDownloadFormat(path2);
34997
+ if (options.all && api.settings.idempotencyKey) throw new CliError("An idempotency key applies to one page. Omit --all.");
34998
+ const file2 = await open5(path2, "wx", 384);
34999
+ let count = 0, first = true, cursor = input2.cursor, loaded = options.loadedPage;
35000
+ const seen = /* @__PURE__ */ new Set();
35001
+ if (typeof cursor === "string") seen.add(cursor);
35002
+ try {
35003
+ while (true) {
35004
+ let raw, headers;
35005
+ if (loaded) {
35006
+ raw = loaded;
35007
+ loaded = void 0;
35008
+ } else {
35009
+ const response = await api.request(`/api/v1/tools/${name}`, { ...input2, ...cursor === void 0 ? {} : { cursor } }, { authenticated: true, rateLimitRecovery: options });
35010
+ raw = response.data;
35011
+ headers = response.headers;
35012
+ }
35013
+ const page = pageSchema2.safeParse(raw);
35014
+ 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.");
35015
+ const items = page.data.items.slice(0, SEARCH_DOWNLOAD_CAP - count);
35016
+ if (format2 === "jsonl") await file2.writeFile(searchPageJsonl(name === "search.companies" ? "companies" : "people", items));
35017
+ else {
35018
+ const csv = (name === "search.companies" ? companyPageCsv : peoplePageCsv)(items);
35019
+ await file2.writeFile(first ? csv : csv.slice(csv.indexOf("\r\n") + 2));
35020
+ }
35021
+ first = false;
35022
+ count += items.length;
35023
+ options.progress?.(`Downloaded ${count.toLocaleString("en-US")} of 5,000`);
35024
+ const next = page.data.nextCursor;
35025
+ 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) };
35026
+ if (seen.has(next)) throw new CliError("The API repeated a cursor. Pagination stopped.");
35027
+ seen.add(next);
35028
+ cursor = next;
35029
+ if (headers?.get("ratelimit-remaining") === "0") {
35030
+ const reset = headers.get("ratelimit-reset");
35031
+ if (reset !== null && /^\d+(?:\.\d+)?$/.test(reset.trim())) await options.sleep(Number(reset) * 1e3);
35032
+ }
35033
+ }
35034
+ } finally {
35035
+ await file2.close();
35036
+ }
35037
+ }
35038
+ var SEARCH_DOWNLOAD_CAP, SEARCH_DOWNLOAD_CAP_NOTICE, isSearchResults, pageSchema2;
35039
+ var init_search_download2 = __esm({
35040
+ "src/search-download.ts"() {
35041
+ "use strict";
35042
+ init_zod();
35043
+ init_search_download();
35044
+ init_api2();
35045
+ SEARCH_DOWNLOAD_CAP = 5e3;
35046
+ SEARCH_DOWNLOAD_CAP_NOTICE = "Download stopped at 5,000 rows. Save the search to a list and export the list for larger sets.";
35047
+ isSearchResults = (name) => name === "search.companies" || name === "search.people";
35048
+ 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() });
35049
+ }
35050
+ });
35051
+
34925
35052
  // ../../node_modules/smol-toml/dist/error.js
34926
35053
  function getLineColFromPtr(string4, ptr) {
34927
35054
  let lines = string4.slice(0, ptr).split(/\r?\n/);
@@ -36718,11 +36845,11 @@ function parseTree(text2, errors2 = [], options = ParseOptions.DEFAULT) {
36718
36845
  onValue({ type: getNodeType(value), offset, length, parent: currentParent, value });
36719
36846
  ensurePropertyComplete(offset + length);
36720
36847
  },
36721
- onSeparator: (sep, offset, length) => {
36848
+ onSeparator: (sep2, offset, length) => {
36722
36849
  if (currentParent.type === "property") {
36723
- if (sep === ":") {
36850
+ if (sep2 === ":") {
36724
36851
  currentParent.colonOffset = offset;
36725
- } else if (sep === ",") {
36852
+ } else if (sep2 === ",") {
36726
36853
  ensurePropertyComplete(offset);
36727
36854
  }
36728
36855
  }
@@ -37342,7 +37469,7 @@ var init_main = __esm({
37342
37469
 
37343
37470
  // src/install.ts
37344
37471
  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";
37472
+ 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
37473
  import { execFile as execFile2 } from "child_process";
37347
37474
  import { promisify, isDeepStrictEqual } from "util";
37348
37475
  import { homedir as homedir2 } from "os";
@@ -37416,7 +37543,7 @@ async function safeParents(path2) {
37416
37543
  await mkdir3(dirname3(path2), { recursive: true, mode: 448 });
37417
37544
  }
37418
37545
  async function privateFile(path2, text2) {
37419
- const file2 = await open5(path2, "wx", 384);
37546
+ const file2 = await open6(path2, "wx", 384);
37420
37547
  try {
37421
37548
  await file2.writeFile(text2, "utf8");
37422
37549
  } finally {
@@ -37965,6 +38092,7 @@ function recordFields(value, prefix = "") {
37965
38092
  if (!value || typeof value !== "object") return [[prefix || "Value", shownValue(value)]];
37966
38093
  return Object.entries(value).flatMap(([key, child]) => {
37967
38094
  if (internalCostKey(key) || child === void 0 || child === null && !["before", "after"].includes(key) || ["nextCursor", "requestId", "releaseId", "rawId", "warehouseId"].includes(key)) return [];
38095
+ if (key === "aiMode") return [["AI key", { byok: "Own key", platform: "Platform", none: "Not configured" }[String(child)] ?? "Unavailable"]];
37968
38096
  const name = prefix ? `${prefix} / ${title(key)}` : title(key);
37969
38097
  if (Array.isArray(child) && child.some((item) => item && typeof item === "object")) return child.flatMap((item, i) => recordFields(item, `${name} ${i + 1}`));
37970
38098
  if (child && typeof child === "object" && !Array.isArray(child)) return recordFields(child, name);
@@ -38049,7 +38177,7 @@ var init_model = __esm({
38049
38177
  ["account", "Account", "Usage, limits and API keys"],
38050
38178
  ["icp", "ICP", "Build your ideal customer profile"]
38051
38179
  ];
38052
- groupTools = (group) => TOOL_CATALOG.filter((tool) => tool.name.startsWith(`${group}.`));
38180
+ groupTools = (group) => TOOL_CATALOG.filter((tool) => tool.name.startsWith(`${group}.`) && tool.name !== "lookup.find");
38053
38181
  toolByName = (name) => TOOL_CATALOG.find((tool) => tool.name === name);
38054
38182
  clean = (value) => String(value ?? "").replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ");
38055
38183
  title = (value) => value.replaceAll("_", " ").replace(/([a-z])([A-Z])/gu, "$1 $2").replace(/\b\w/gu, (c) => c.toUpperCase());
@@ -38137,19 +38265,43 @@ var init_install_menu = __esm({
38137
38265
  });
38138
38266
 
38139
38267
  // 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, ""];
38268
+ function markFrame(size, progress = [1, 1, 1]) {
38269
+ const scale = size === "large" ? 3 : 1;
38270
+ return [8, 4, 6].flatMap((units, index) => {
38271
+ const width = Math.round(units * scale * Math.max(0, Math.min(1, progress[index])));
38272
+ const indent = " ".repeat([0, 2, 1][index] * scale);
38273
+ const cap = width > 2 ? indent + " " + "\u2584".repeat(width - 2) : "";
38274
+ const base2 = width ? indent + "\u2580".repeat(width) : "";
38275
+ return size === "large" ? [cap, width ? indent + "\u2588".repeat(width) : "", cap.replaceAll("\u2584", "\u2580")] : [cap, base2];
38276
+ });
38277
+ }
38278
+ function wordmarkFrame(frame = BANNER_FRAMES - 1) {
38279
+ const visible = frame < 5 ? Math.ceil((frame + 1) / 5 * WORDMARK_WIDTH) : WORDMARK_WIDTH;
38280
+ return Array.from({ length: 6 }, (_, row) => {
38281
+ const higher = [..."HIGHER"].map((letter) => GLYPHS[letter][row]).join(" ") + " ";
38282
+ const data = [..."DATA"].map((letter) => GLYPHS[letter][row]).join(" ");
38283
+ return { higher: higher.slice(0, visible), data: data.slice(0, Math.max(0, visible - higher.length)) };
38149
38284
  });
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
38285
  }
38152
- var GLYPHS, BANNER_FRAMES, BANNER_DURATION_MS, SMALL_MARK, shouldAnimate;
38286
+ function homeBrandLayout(columns, rows, extraRows = 0) {
38287
+ const sideBySide = columns - 4 >= WORDMARK_WIDTH + 3 + MARK_WIDTH;
38288
+ const headerRows = sideBySide ? 10 : 17;
38289
+ return { large: columns - 4 >= WORDMARK_WIDTH && rows >= headerRows + 14 + extraRows, sideBySide, headerRows };
38290
+ }
38291
+ function busyFrame(frame, size = "large") {
38292
+ return markFrame(size, [0, 1, 2].map((index) => 0.65 + 0.35 * (1 + Math.sin(frame * Math.PI / 4 - index * Math.PI / 2)) / 2));
38293
+ }
38294
+ function bannerFrame(frame, columns) {
38295
+ const wordmark = wordmarkFrame(frame).map((line) => line.higher + line.data);
38296
+ const indent = " ".repeat(Math.max(0, Math.floor((columns - WORDMARK_WIDTH) / 2)));
38297
+ const logo = markFrame("large", [0, 1, 2].map((index) => Math.max(0, Math.min(1, (frame - 5 - index) / 2))));
38298
+ const markIndent = " ".repeat(Math.max(0, Math.floor((columns - MARK_WIDTH) / 2)));
38299
+ 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());
38300
+ }
38301
+ function animationOptions(args = process.argv.slice(2)) {
38302
+ return { banner: !args.includes("--no-banner"), json: args.includes("--json"), jsonl: args.includes("--jsonl") };
38303
+ }
38304
+ var GLYPHS, BANNER_FRAMES, BANNER_DURATION_MS, MARK_WIDTH, WORDMARK_WIDTH, shouldAnimate;
38153
38305
  var init_banner = __esm({
38154
38306
  "src/terminal/banner.ts"() {
38155
38307
  "use strict";
@@ -38165,13 +38317,15 @@ var init_banner = __esm({
38165
38317
  };
38166
38318
  BANNER_FRAMES = 11;
38167
38319
  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 "];
38320
+ MARK_WIDTH = 24;
38321
+ WORDMARK_WIDTH = [..."HIGHERDATA"].reduce((width, letter) => width + GLYPHS[letter][0].length, "HIGHERDATA".length - 1);
38169
38322
  shouldAnimate = (tty, options, env) => tty && options.banner !== false && !options.json && !options.jsonl && !Object.hasOwn(env, "CI");
38170
38323
  }
38171
38324
  });
38172
38325
 
38173
38326
  // src/terminal/screens.tsx
38174
- import { Box, Text } from "ink";
38327
+ import { useEffect, useState } from "react";
38328
+ import { Box, Text, useStdout } from "ink";
38175
38329
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
38176
38330
  function Menu({ items, selected, limit, color, width }) {
38177
38331
  const start = Math.max(0, Math.min(selected - Math.floor(limit / 2), items.length - limit));
@@ -38194,17 +38348,54 @@ function Menu({ items, selected, limit, color, width }) {
38194
38348
  ] })
38195
38349
  ] });
38196
38350
  }
38351
+ function Mark({ lines, color }) {
38352
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx(Text, { color, children: line || " " }, i)) });
38353
+ }
38354
+ function Wordmark({ color, frame }) {
38355
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: WORDMARK_WIDTH, children: wordmarkFrame(frame).map((line, i) => /* @__PURE__ */ jsxs(Text, { children: [
38356
+ line.higher,
38357
+ /* @__PURE__ */ jsx(Text, { color, children: line.data })
38358
+ ] }, i)) });
38359
+ }
38360
+ function BusyMark({ color, size = "large", options = animationOptions(), env = process.env }) {
38361
+ const { stdout } = useStdout(), [frame, setFrame] = useState(0);
38362
+ const animate = shouldAnimate(Boolean(stdout.isTTY), options, env);
38363
+ useEffect(() => {
38364
+ if (!animate) return;
38365
+ const timer = setInterval(() => setFrame((value) => (value + 1) % 8), 120);
38366
+ return () => clearInterval(timer);
38367
+ }, [animate]);
38368
+ return /* @__PURE__ */ jsx(Mark, { lines: animate ? busyFrame(frame, size) : markFrame(size), color });
38369
+ }
38197
38370
  function TerminalScreen({ state, columns, rows = 28, colors = true, profile = "default", space = "Default", workspace = "HigherOps", remaining = "--" }) {
38198
38371
  const width = Math.max(40, columns), color = colors ? "#f2b705" : void 0;
38199
- const limit = Math.max(4, rows - 12);
38372
+ const limit = Math.max(4, rows - 15);
38373
+ const extraRows = [state.error, state.notice].reduce((count, text2) => count + Math.ceil(clean(text2).length / (width - 4)), 0);
38374
+ const brand = homeBrandLayout(width, rows, extraRows), largeHome = state.screen === "home" && brand.large;
38200
38375
  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)),
38376
+ /* @__PURE__ */ jsx(Box, { paddingLeft: Math.max(0, Math.floor((width - WORDMARK_WIDTH) / 2)), children: /* @__PURE__ */ jsx(Wordmark, { color, frame: state.frame }) }),
38377
+ /* @__PURE__ */ jsx(Text, { children: " " }),
38378
+ /* @__PURE__ */ jsx(Mark, { lines: bannerFrame(state.frame, width).slice(7, 16), color }),
38379
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: bannerFrame(state.frame, width)[16] || " " }),
38202
38380
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press any key to start" })
38203
38381
  ] });
38204
38382
  const fields = state.tool ? fieldsFor(state.tool.input, state.input, state.variant) : [];
38205
38383
  let help = "Up/Down choose. Enter opens. Esc back. Ctrl+C quit.";
38206
38384
  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 });
38385
+ if (state.screen === "home") help = "Up/Down choose. Enter opens. / Look up. Ctrl+C quit.";
38386
+ 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 });
38387
+ if (state.screen === "lookup") {
38388
+ content = /* @__PURE__ */ jsxs(Fragment, { children: [
38389
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Look up" }),
38390
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Name, work email, international phone, domain, LinkedIn or record id" }),
38391
+ /* @__PURE__ */ jsxs(Text, { color, wrap: "wrap", children: [
38392
+ "> ",
38393
+ clean(state.text),
38394
+ "_"
38395
+ ] })
38396
+ ] });
38397
+ help = "Type or paste. Enter looks up. Esc home.";
38398
+ }
38208
38399
  if (state.screen === "install") {
38209
38400
  content = /* @__PURE__ */ jsxs(Fragment, { children: [
38210
38401
  /* @__PURE__ */ jsxs(Text, { bold: true, children: [
@@ -38275,6 +38466,12 @@ function TerminalScreen({ state, columns, rows = 28, colors = true, profile = "d
38275
38466
  ] });
38276
38467
  help = "Enter edit/run. Arrays: comma separated. Blank clears. Esc back.";
38277
38468
  }
38469
+ if (state.screen === "download-choice") {
38470
+ content = /* @__PURE__ */ jsxs(Fragment, { children: [
38471
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Download search results" }),
38472
+ /* @__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 })
38473
+ ] });
38474
+ }
38278
38475
  if (["edit", "upload", "download", "space"].includes(state.screen)) {
38279
38476
  const field = fields.find((field2) => field2.path === state.field);
38280
38477
  content = /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -38296,7 +38493,7 @@ function TerminalScreen({ state, columns, rows = 28, colors = true, profile = "d
38296
38493
  }
38297
38494
  if (state.screen === "busy") {
38298
38495
  content = /* @__PURE__ */ jsxs(Fragment, { children: [
38299
- SMALL_MARK.map((line, i) => /* @__PURE__ */ jsx(Text, { color, children: line }, i)),
38496
+ /* @__PURE__ */ jsx(BusyMark, { color, size: rows >= 28 ? "large" : "compact" }),
38300
38497
  /* @__PURE__ */ jsx(Text, { children: state.notice || "Working..." })
38301
38498
  ] });
38302
38499
  help = "The current operation is running. It is never retried automatically.";
@@ -38315,35 +38512,58 @@ function TerminalScreen({ state, columns, rows = 28, colors = true, profile = "d
38315
38512
  }
38316
38513
  if (state.screen === "results") {
38317
38514
  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.";
38515
+ if (state.tool?.name === "lookup.find") {
38516
+ content = /* @__PURE__ */ jsxs(Fragment, { children: [
38517
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
38518
+ "Look up: ",
38519
+ clean(state.input.query)
38520
+ ] }),
38521
+ items.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
38522
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "KIND / NAME / DETAILS" }),
38523
+ /* @__PURE__ */ jsx(Menu, { items: items.map((row) => {
38524
+ const match = lookupMatchColumns(row);
38525
+ return { label: `${match.kind}: ${match.name}`, hint: match.details };
38526
+ }), selected: state.selected, limit: limit - 2, color, width: width - 4 })
38527
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
38528
+ /* @__PURE__ */ jsxs(Text, { children: [
38529
+ "No match for ",
38530
+ clean(state.input.query)
38531
+ ] }),
38532
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Try Search to find companies or people with filters." })
38533
+ ] })
38534
+ ] });
38535
+ help = "Enter detail. E edit lookup. Esc home.";
38536
+ } else {
38537
+ const list = state.tool?.name === "lists.get" && state.input.id && Array.isArray(state.result.lists) ? state.result.lists[0] : void 0;
38538
+ content = /* @__PURE__ */ jsxs(Fragment, { children: [
38539
+ /* @__PURE__ */ jsx(Text, { bold: true, children: state.tool?.name ?? "Enrichment job" }),
38540
+ list && /* @__PURE__ */ jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [
38541
+ clean(list.name),
38542
+ " / ",
38543
+ clean(list.type),
38544
+ " / ",
38545
+ list.recurringEnabled ? "Each release" : "On demand",
38546
+ " / ",
38547
+ clean(list.maxRecords),
38548
+ " records / ",
38549
+ clean(list.maxNewRecordsPerRefresh),
38550
+ " new records per refresh"
38551
+ ] }),
38552
+ 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: [
38553
+ key,
38554
+ ": ",
38555
+ value
38556
+ ] }, key)),
38557
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
38558
+ items.length ? `${items.length} on this page. ` : "",
38559
+ state.result.nextCursor ? "More records available." : "End of page."
38560
+ ] })
38561
+ ] });
38562
+ help = "Enter detail. N next page. R refresh. D download. E edit inputs. Esc back.";
38563
+ if (!state.tool) help = "E installation options. Esc home. ";
38564
+ if (state.tool?.name === "searches.list") help = "Enter reopen search. A create audience. N next page. E edit. Esc back.";
38565
+ if (state.tool?.name === "lists.get" && state.input.id) help += " P preview refresh.";
38566
+ }
38347
38567
  }
38348
38568
  if (state.screen === "detail") {
38349
38569
  const row = detailRecord(state.row), isCore = coreRecord(state.row);
@@ -38390,26 +38610,45 @@ function TerminalScreen({ state, columns, rows = 28, colors = true, profile = "d
38390
38610
  help = "Enter sends. Ctrl+B step limit. Ctrl+N new conversation. Esc home.";
38391
38611
  }
38392
38612
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, borderStyle: "double", paddingX: 1, children: [
38393
- /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
38613
+ largeHome ? /* @__PURE__ */ jsxs(Fragment, { children: [
38614
+ /* @__PURE__ */ jsxs(Box, { flexDirection: brand.sideBySide ? "row" : "column", gap: brand.sideBySide ? 3 : 1, children: [
38615
+ /* @__PURE__ */ jsx(Wordmark, { color }),
38616
+ /* @__PURE__ */ jsx(Mark, { lines: markFrame("large"), color })
38617
+ ] }),
38618
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [
38619
+ "by HigherOps / ",
38620
+ clean(profile),
38621
+ " / ",
38622
+ clean(workspace),
38623
+ " / ",
38624
+ clean(space),
38625
+ " / ",
38626
+ clean(remaining),
38627
+ " email verifications remaining"
38628
+ ] })
38629
+ ] }) : /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
38394
38630
  /* @__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)) }),
38631
+ /* @__PURE__ */ jsx(Mark, { lines: markFrame("compact"), color }),
38396
38632
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
38397
- /* @__PURE__ */ jsx(Text, { bold: true, color, children: "HIGHERDATA" }),
38633
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
38634
+ "Higher",
38635
+ /* @__PURE__ */ jsx(Text, { color, children: "Data" })
38636
+ ] }),
38398
38637
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "by HigherOps" }),
38399
38638
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: clean(profile) })
38400
38639
  ] })
38401
38640
  ] }),
38402
- /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "flex-end", children: [
38641
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "flex-end", flexShrink: 1, children: [
38403
38642
  /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", children: clean(workspace) }),
38404
38643
  /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "truncate-end", children: clean(space) }),
38405
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
38644
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [
38406
38645
  clean(remaining),
38407
38646
  " email verifications remaining"
38408
38647
  ] })
38409
38648
  ] })
38410
38649
  ] }),
38411
38650
  /* @__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 }),
38651
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: width - 4, minHeight: largeHome ? 0 : Math.max(0, Math.min(rows - 16, 13)), children: content }),
38413
38652
  state.error && /* @__PURE__ */ jsx(Text, { color: colors ? "red" : void 0, children: clean(state.error) }),
38414
38653
  state.notice && state.screen !== "busy" && /* @__PURE__ */ jsx(Text, { dimColor: true, children: clean(state.notice) }),
38415
38654
  /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "wrap", children: help })
@@ -38419,6 +38658,7 @@ var initialState, accountItems, homeItems;
38419
38658
  var init_screens = __esm({
38420
38659
  "src/terminal/screens.tsx"() {
38421
38660
  "use strict";
38661
+ init_output();
38422
38662
  init_tool_commands();
38423
38663
  init_banner();
38424
38664
  init_install_menu();
@@ -38432,8 +38672,8 @@ var init_screens = __esm({
38432
38672
  { id: "skills-install", label: "Install skills", hint: "Five task guides for Claude Code or Codex" },
38433
38673
  { id: "quit", label: "Quit", hint: "" }
38434
38674
  ];
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);
38675
+ 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) => {
38676
+ const group = id === "find" ? ["find", "Look up", "Name, email, phone, domain, LinkedIn or record id"] : GROUPS.find(([name]) => name === id);
38437
38677
  return { id, label: group?.[1] ?? "Ask HigherData", hint: group?.[2] ?? "Plan a request, limit its steps", section: index === 0 ? section : "" };
38438
38678
  }));
38439
38679
  }
@@ -38448,25 +38688,25 @@ __export(app_exports, {
38448
38688
  });
38449
38689
  import { randomUUID as randomUUID6 } from "crypto";
38450
38690
  import { createReadStream as createReadStream2 } from "fs";
38451
- import { open as open6 } from "fs/promises";
38452
- import { useEffect, useRef, useState } from "react";
38691
+ import { open as open7 } from "fs/promises";
38692
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
38453
38693
  import { render, useApp, useInput, useWindowSize } from "ink";
38454
38694
  import { jsx as jsx2 } from "react/jsx-runtime";
38455
38695
  function InteractiveApp({ session }) {
38456
38696
  const { exit } = useApp(), { columns, rows } = useWindowSize();
38457
38697
  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" }));
38698
+ const [chrome, setChrome] = useState2({ workspace: "Workspace", remaining: "--" });
38699
+ const [state, setState] = useState2(() => ({ ...initialState(), screen: shouldAnimate(true, session.options, session.runtime.env) ? "banner" : "home" }));
38460
38700
  const current = useRef(state);
38461
38701
  current.current = state;
38462
38702
  const mounted = useRef(true), request = useRef(false), back = useRef(initialState());
38463
- useEffect(() => {
38703
+ useEffect2(() => {
38464
38704
  mounted.current = true;
38465
38705
  return () => {
38466
38706
  mounted.current = false;
38467
38707
  };
38468
38708
  }, []);
38469
- useEffect(() => {
38709
+ useEffect2(() => {
38470
38710
  let active = true;
38471
38711
  if (!settings.apiKey || !settings.space) return;
38472
38712
  void Promise.all([session.call("account.whoami", {}), session.call("account.usage", {})]).then(([identity, usage]) => {
@@ -38479,12 +38719,12 @@ function InteractiveApp({ session }) {
38479
38719
  active = false;
38480
38720
  };
38481
38721
  }, [session, settings.apiKey, settings.space]);
38482
- useEffect(() => {
38722
+ useEffect2(() => {
38483
38723
  if (state.screen !== "banner") return;
38484
38724
  const timer = setInterval(() => setState((s) => s.frame >= BANNER_FRAMES - 1 ? { ...s, screen: "home" } : { ...s, frame: s.frame + 1 }), BANNER_DURATION_MS / BANNER_FRAMES);
38485
38725
  return () => clearInterval(timer);
38486
38726
  }, [state.screen]);
38487
- useEffect(() => {
38727
+ useEffect2(() => {
38488
38728
  const job = state.result.job;
38489
38729
  const id = job?.id ?? (state.tool?.name === "jobs.get" ? state.result.id : void 0);
38490
38730
  const status = job?.status ?? state.result.status;
@@ -38542,6 +38782,7 @@ function InteractiveApp({ session }) {
38542
38782
  function activate(item) {
38543
38783
  const s = current.current;
38544
38784
  if (item.id === "quit") exit();
38785
+ else if (item.id === "find") patch({ screen: "lookup", tool: toolByName("lookup.find"), input: {}, text: "", selected: 0 });
38545
38786
  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
38787
  else if (item.id === "space") {
38547
38788
  back.current = s;
@@ -38568,12 +38809,27 @@ function InteractiveApp({ session }) {
38568
38809
  return;
38569
38810
  }
38570
38811
  if (s.screen === "busy") return;
38812
+ if (s.screen === "home" && input2 === "/") {
38813
+ activate({ id: "find" });
38814
+ return;
38815
+ }
38571
38816
  if (key.escape && s.screen === "filter-group") {
38572
38817
  patch({ screen: "form", selected: searchGroups(s.tool.input, s.input).findIndex((group) => group.group === s.filterGroup) });
38573
38818
  return;
38574
38819
  }
38575
38820
  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 });
38821
+ 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 });
38822
+ return;
38823
+ }
38824
+ if (s.screen === "lookup") {
38825
+ if (key.return && s.text.trim()) void work("Looking up your record...", () => session.lookup(s.text));
38826
+ else if (key.backspace || key.delete) patch({ text: s.text.slice(0, -1) });
38827
+ else if (!key.return && !key.ctrl && !key.meta && !key.upArrow && !key.downArrow) patch({ text: s.text + input2 });
38828
+ return;
38829
+ }
38830
+ if (s.screen === "download-choice") {
38831
+ if (key.upArrow || key.downArrow) patch({ selected: s.selected === 0 ? 1 : 0 });
38832
+ else if (key.return) patch({ screen: "download", field: "New destination .csv or .jsonl file", text: "" });
38577
38833
  return;
38578
38834
  }
38579
38835
  if (["edit", "download", "space", "upload"].includes(s.screen)) {
@@ -38587,8 +38843,10 @@ function InteractiveApp({ session }) {
38587
38843
  }
38588
38844
  if (s.screen === "download") {
38589
38845
  void work("Downloading into a new file...", async () => {
38590
- await session.download(back.current, s.text);
38591
- return { ...back.current, notice: `Saved ${s.text}` };
38846
+ const notice = await session.download(back.current, s.text, { all: isSearchResults(back.current.tool?.name) && s.selected === 1, progress: (text2) => {
38847
+ if (mounted.current) patch({ notice: text2 });
38848
+ } });
38849
+ return { ...back.current, notice: `Saved ${s.text}${notice ? `. ${notice}` : ""}` };
38592
38850
  });
38593
38851
  return;
38594
38852
  }
@@ -38697,15 +38955,15 @@ function InteractiveApp({ session }) {
38697
38955
  patch({ input: { ...s.input, cursor: s.result.nextCursor } });
38698
38956
  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
38957
  } else if (input2 === "r" && s.tool) runTool(!needsConfirmation(s.tool));
38700
- else if (input2 === "e") patch({ screen: s.tool ? "form" : "install", selected: 0 });
38958
+ 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
38959
  else if (input2 === "d") {
38702
38960
  back.current = s;
38703
- patch({ screen: "download", field: "New destination .csv or .jsonl file", text: "" });
38961
+ patch({ screen: isSearchResults(s.tool?.name) ? "download-choice" : "download", selected: 0, field: "New destination .csv or .jsonl file", text: "" });
38704
38962
  } else if (input2 === "a" && s.tool?.name === "searches.list") {
38705
38963
  const row = rowsOf(s.result)[s.selected];
38706
38964
  if (row) form("lists.create", { type: "audience", savedSearchId: row.id, recurringEnabled: true, maxRecords: 1e3, maxNewRecordsPerRefresh: 1e3, removeNonmatches: false });
38707
38965
  } 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) {
38966
+ else if (key.return && (rowsOf(s.result).length || s.tool?.name !== "lookup.find")) {
38709
38967
  void work("Opening the record...", () => session.openRow(s));
38710
38968
  }
38711
38969
  } else if (s.screen === "detail") {
@@ -38735,6 +38993,7 @@ var TerminalSession;
38735
38993
  var init_app = __esm({
38736
38994
  "src/terminal/app.tsx"() {
38737
38995
  "use strict";
38996
+ init_search_download2();
38738
38997
  init_tool_commands();
38739
38998
  init_api2();
38740
38999
  init_config();
@@ -38758,6 +39017,12 @@ var init_app = __esm({
38758
39017
  async load() {
38759
39018
  this.config = await this.runtime.readConfig();
38760
39019
  }
39020
+ async lookup(query) {
39021
+ const result = await this.call("lookup.find", { query });
39022
+ const state = { ...initialState(), screen: "results", tool: toolByName("lookup.find"), input: { query }, result };
39023
+ if (result.exactMatch === true && rowsOf(result).length === 1) return { ...state, ...await this.openRow(state) };
39024
+ return state;
39025
+ }
38761
39026
  settings() {
38762
39027
  return resolveSettings(this.config, this.options, this.runtime.env);
38763
39028
  }
@@ -38787,15 +39052,22 @@ var init_app = __esm({
38787
39052
  this.config.profiles[this.config.activeProfile] = { apiUrl: settings.apiUrl, apiKey: key };
38788
39053
  await this.runtime.writeConfig(this.config);
38789
39054
  }
38790
- async download(state, path2) {
39055
+ async download(state, path2, options = {}) {
38791
39056
  if (!path2.trim()) throw new CliError("Enter a new destination file.");
38792
39057
  path2 = path2.trim();
38793
39058
  if (!/\.(csv|jsonl)$/.test(path2)) throw new CliError("Choose a new .csv or .jsonl destination.");
39059
+ if (isSearchResults(state.tool?.name)) {
39060
+ const input2 = { ...state.input };
39061
+ const loadedPage = !options.all || !input2.cursor ? state.result : void 0;
39062
+ if (options.all) delete input2.cursor;
39063
+ const result = await downloadSearch(this.api(), state.tool.name, input2, path2, { ...options, loadedPage, sleep: this.runtime.sleep });
39064
+ return result.capped ? SEARCH_DOWNLOAD_CAP_NOTICE : "";
39065
+ }
38794
39066
  const job = state.result.job;
38795
39067
  const jobId = state.result.jobId ?? job?.id ?? (state.tool?.name.startsWith("jobs.") ? state.result.id ?? state.input.jobId : void 0);
38796
39068
  const exportTool = state.tool?.name === "lists.get" && state.input.id ? "lists.export" : state.tool?.name;
38797
39069
  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);
39070
+ const file2 = await open7(path2.trim(), "wx", 384);
38799
39071
  try {
38800
39072
  const format2 = path2.endsWith(".jsonl") ? "jsonl" : "csv";
38801
39073
  if (typeof jobId === "string") await downloadBulkJob(this.api(), jobId, format2, async (text2) => {
@@ -38935,11 +39207,12 @@ async function buildFilters(api, options, kind) {
38935
39207
 
38936
39208
  // src/commands.ts
38937
39209
  init_output();
39210
+ init_search_download2();
38938
39211
 
38939
39212
  // src/pagination.ts
38940
39213
  init_zod();
38941
39214
  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() });
39215
+ 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
39216
  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
39217
  async function pagedTool(api, name, input2, all, exporting = false) {
38945
39218
  if (all && api.settings.idempotencyKey) throw new CliError("An idempotency key applies to one page. Omit --all.");
@@ -38952,7 +39225,7 @@ async function pagedTool(api, name, input2, all, exporting = false) {
38952
39225
  let empty = true;
38953
39226
  while (true) {
38954
39227
  const raw = await api.tool(name, { ...input2, ...cursor === void 0 ? {} : { cursor } });
38955
- const page = (exporting ? exportSchema : pageSchema2).safeParse(raw);
39228
+ const page = (exporting ? exportSchema : pageSchema3).safeParse(raw);
38956
39229
  if (!page.success) throw new CliError("The API returned an invalid cursor page.");
38957
39230
  if ("items" in page.data) records.push(...page.data.items);
38958
39231
  else {
@@ -39182,8 +39455,158 @@ function registerSuppressCommands(program2, helpers) {
39182
39455
 
39183
39456
  // src/commands.ts
39184
39457
  init_install();
39458
+
39459
+ // src/update.ts
39460
+ init_zod();
39461
+ init_api2();
39462
+ init_version_policy();
39463
+ import { execFile as execFile3, spawn } from "child_process";
39464
+ import { realpath } from "fs/promises";
39465
+ import { isAbsolute, join as join4, sep } from "path";
39466
+ import { promisify as promisify2 } from "util";
39467
+ var NpmUpdateError = class extends Error {
39468
+ };
39469
+ function npmSummary(error63) {
39470
+ const value = error63;
39471
+ const lines = typeof value?.stderr === "string" ? value.stderr.split(/\r?\n/u) : [];
39472
+ const line = lines.find((line2) => /^npm (?:error|ERR!) code [A-Z0-9_]+$/u.test(line2.trim())) ?? lines.find((line2) => /^npm (?:error|ERR!) (?:EACCES|EPERM|ENOENT|ETIMEDOUT)\b/u.test(line2.trim()));
39473
+ return line?.trim().slice(0, 200) ?? (typeof value?.code === "string" && /^[A-Z0-9_]+$/u.test(value.code) ? value.code : "npm install failed");
39474
+ }
39475
+ function updateRuntime() {
39476
+ return {
39477
+ stdinTTY: Boolean(process.stdin.isTTY),
39478
+ stdoutTTY: Boolean(process.stdout.isTTY),
39479
+ script: process.argv[1] ?? "",
39480
+ realpath,
39481
+ async npm(args, env) {
39482
+ try {
39483
+ return (await promisify2(execFile3)("npm", args, { env, timeout: args[0] === "install" ? 12e4 : 1500, maxBuffer: 64 * 1024 })).stdout;
39484
+ } catch (error63) {
39485
+ throw new NpmUpdateError(npmSummary(error63));
39486
+ }
39487
+ },
39488
+ rerun(script, args, env) {
39489
+ return new Promise((resolve2, reject) => {
39490
+ const child = spawn(process.execPath, [script, ...args], { env, stdio: "inherit" });
39491
+ child.once("error", reject);
39492
+ child.once("exit", (code, signal) => resolve2(code ?? (signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1)));
39493
+ });
39494
+ }
39495
+ };
39496
+ }
39497
+ async function npmLatest(fetchImpl, timeoutMs = 3e3) {
39498
+ try {
39499
+ const response = await fetchImpl("https://registry.npmjs.org/higherdata/latest", { headers: { accept: "application/json" }, redirect: "error", signal: AbortSignal.timeout(timeoutMs) });
39500
+ if (!response.ok || !response.body) {
39501
+ await response.body?.cancel();
39502
+ throw new Error();
39503
+ }
39504
+ const reader = response.body.getReader(), chunks = [];
39505
+ let size = 0;
39506
+ try {
39507
+ while (true) {
39508
+ const { done, value } = await reader.read();
39509
+ if (done) break;
39510
+ size += value.length;
39511
+ if (size > 16384) {
39512
+ await reader.cancel();
39513
+ throw new Error();
39514
+ }
39515
+ chunks.push(value);
39516
+ }
39517
+ } finally {
39518
+ reader.releaseLock();
39519
+ }
39520
+ return external_exports.object({ version: releaseVersion }).parse(JSON.parse(Buffer.concat(chunks).toString("utf8"))).version;
39521
+ } catch {
39522
+ throw new CliError("Could not check the npm release. Try higherdata update again.");
39523
+ }
39524
+ }
39525
+ var installArgs = (version2) => ["install", "--global", `higherdata@${releaseVersion.parse(version2)}`];
39526
+ var manual = (version2) => `npm install --global higherdata@${releaseVersion.parse(version2)}`;
39527
+ async function updateCli(input2) {
39528
+ const latest = await npmLatest(input2.fetchImpl);
39529
+ if (compareVersions(input2.version, latest) >= 0) {
39530
+ input2.out(`HigherData CLI ${input2.version} is up to date.
39531
+ `);
39532
+ return;
39533
+ }
39534
+ try {
39535
+ await input2.runtime.npm(installArgs(latest), input2.env);
39536
+ } catch (error63) {
39537
+ throw new CliError(`${error63 instanceof NpmUpdateError ? error63.message : "npm install failed"}. Run: ${manual(latest)}`);
39538
+ }
39539
+ input2.out(`Updated HigherData CLI ${input2.version} to ${latest}
39540
+ `);
39541
+ }
39542
+ function primaryCommand(args) {
39543
+ for (let index = 0; index < args.length; index++) {
39544
+ const arg = args[index];
39545
+ if (["--api-url", "--idempotency-key"].includes(arg)) {
39546
+ index++;
39547
+ continue;
39548
+ }
39549
+ if (arg === "--") return args[index + 1];
39550
+ if (!arg.startsWith("-")) return arg;
39551
+ }
39552
+ }
39553
+ function interactiveUpdateAllowed(args, env, runtime) {
39554
+ return runtime.stdinTTY && runtime.stdoutTTY && !Object.hasOwn(env, "CI") && !["mcp", "stdio"].includes(primaryCommand(args) ?? "") && !args.some((arg) => ["--json", "--jsonl", "--stdio", "--help", "-h", "--version", "-V"].includes(arg) || /^(--jsonl?|--stdio)=/u.test(arg));
39555
+ }
39556
+ async function possibleGlobalScript(runtime, env) {
39557
+ try {
39558
+ const script = await runtime.realpath(runtime.script);
39559
+ if (script.split(sep).includes("_npx") || env.npm_command === "exec" || env.npm_lifecycle_event === "npx") return null;
39560
+ return script.endsWith(`${sep}${join4("node_modules", "higherdata", "dist", "index.js")}`) ? script : null;
39561
+ } catch {
39562
+ return null;
39563
+ }
39564
+ }
39565
+ async function globalScript(runtime, env, script) {
39566
+ try {
39567
+ const prefix = (await runtime.npm(["prefix", "-g"], env)).trim();
39568
+ if (!isAbsolute(prefix)) return null;
39569
+ const root = await runtime.realpath(prefix);
39570
+ const expected = join4(root, process.platform === "win32" ? "node_modules" : "lib/node_modules", "higherdata", "dist/index.js");
39571
+ return script === expected ? script : null;
39572
+ } catch {
39573
+ return null;
39574
+ }
39575
+ }
39576
+ async function autoUpdate(input2) {
39577
+ if (!interactiveUpdateAllowed(input2.args, input2.env, input2.runtime) || input2.env.HIGHERDATA_NO_AUTO_UPDATE === "1" || input2.env.HIGHERDATA_UPDATE_RERUN === "1" || ["update", "upgrade", "config"].includes(primaryCommand(input2.args) ?? "")) return null;
39578
+ if ((await input2.readConfig()).autoUpdate === false) return null;
39579
+ const candidate = await possibleGlobalScript(input2.runtime, input2.env);
39580
+ if (!candidate) return null;
39581
+ let latest = null;
39582
+ try {
39583
+ latest = await npmLatest(input2.fetchImpl, 1500);
39584
+ } catch {
39585
+ return null;
39586
+ }
39587
+ if (!latest || compareVersions(input2.version, latest) >= 0) return null;
39588
+ const script = await globalScript(input2.runtime, input2.env, candidate);
39589
+ if (!script) return null;
39590
+ input2.warn(`Updating HigherData CLI to ${latest}...
39591
+ `);
39592
+ try {
39593
+ await input2.runtime.npm(installArgs(latest), input2.env);
39594
+ } catch {
39595
+ input2.warn(`Automatic update failed. Run: ${manual(latest)}
39596
+ `);
39597
+ return null;
39598
+ }
39599
+ try {
39600
+ return await input2.runtime.rerun(script, input2.args, { ...input2.env, HIGHERDATA_UPDATE_RERUN: "1" });
39601
+ } catch {
39602
+ input2.warn("Could not restart HigherData CLI. Run your command again.\n");
39603
+ return 2;
39604
+ }
39605
+ }
39606
+
39607
+ // src/commands.ts
39185
39608
  function cliRuntime(overrides = {}) {
39186
- return { installation: installRuntime(), versionCache: fileVersionCache(), write: overrides.write ?? (overrides.out ? async (text2) => {
39609
+ return { updater: updateRuntime(), installation: installRuntime(), versionCache: fileVersionCache(), write: overrides.write ?? (overrides.out ? async (text2) => {
39187
39610
  overrides.out(text2);
39188
39611
  } : (text2) => new Promise((resolve2, reject) => {
39189
39612
  process.stdout.write(text2, (error63) => error63 ? reject(error63) : resolve2());
@@ -39246,10 +39669,18 @@ function registerCommands(program2, runtime) {
39246
39669
  emit(identity, mode);
39247
39670
  });
39248
39671
  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) => {
39672
+ 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
39673
  const { api, mode } = await session(command);
39674
+ if (options.out) searchDownloadFormat(options.out);
39251
39675
  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);
39676
+ const input2 = { ...filters, limit: options.limit, ...options.cursor ? { cursor: options.cursor } : {}, ...excluded(options) };
39677
+ if (options.out) {
39678
+ const result = await downloadSearch(api, `search.${kind}`, input2, options.out, { all: Boolean(options.all), sleep: runtime.sleep, progress: (text2) => runtime.err(`${text2}
39679
+ `) });
39680
+ if (result.capped) runtime.err(`${SEARCH_DOWNLOAD_CAP_NOTICE}
39681
+ `);
39682
+ emit({ path: options.out, ...result }, mode);
39683
+ } else emit(await pagedTool(api, `search.${kind}`, input2, Boolean(options.all)), mode);
39253
39684
  });
39254
39685
  addFilterOptions(program2.command("count").description("Estimate companies and their contacts")).action(async (options, command) => {
39255
39686
  const { api, mode } = await session(command);
@@ -39339,9 +39770,21 @@ function createProgram(overrides = {}) {
39339
39770
  } else program2.outputHelp();
39340
39771
  });
39341
39772
  program2.hook("preAction", async (_program, command) => {
39342
- if (command === program2 || command.name() === "login" || ["mcp", "skills"].includes(command.parent?.name() ?? "")) return;
39773
+ if (["login", "update", "upgrade"].includes(command.name()) || ["mcp", "skills", "config"].includes(command.parent?.name() ?? "")) return;
39774
+ if (command === program2 && (!runtime.updater.stdinTTY || !runtime.updater.stdoutTTY || Object.hasOwn(runtime.env, "CI") || program2.opts().json || program2.opts().jsonl)) return;
39343
39775
  const settings = resolveSettings(await runtime.readConfig(), command.optsWithGlobals(), runtime.env);
39344
- await checkCliVersion({ apiUrl: settings.apiUrl, version: package_default.version, cache: runtime.versionCache, fetchImpl: runtime.fetchImpl, now: runtime.now, warn: runtime.err });
39776
+ const interactive = runtime.updater.stdinTTY && runtime.updater.stdoutTTY && !Object.hasOwn(runtime.env, "CI") && !command.optsWithGlobals().json && !command.optsWithGlobals().jsonl;
39777
+ await checkCliVersion({ apiUrl: settings.apiUrl, version: package_default.version, cache: runtime.versionCache, fetchImpl: runtime.fetchImpl, now: runtime.now, warn: interactive ? runtime.err : () => {
39778
+ } });
39779
+ });
39780
+ program2.command("update").alias("upgrade").description("Install the latest HigherData CLI release").action(() => updateCli({ version: package_default.version, fetchImpl: runtime.fetchImpl, runtime: runtime.updater, env: runtime.env, out: runtime.out }));
39781
+ program2.command("config").description("Configure the CLI").command("set <key> <value>").action(async (key, value) => {
39782
+ if (key !== "auto-update" || !["true", "false"].includes(value)) throw new CliError("Use higherdata config set auto-update true or false.");
39783
+ const config2 = await runtime.readConfig();
39784
+ config2.autoUpdate = value === "true";
39785
+ await runtime.writeConfig(config2);
39786
+ runtime.out(`Automatic updates ${config2.autoUpdate ? "enabled" : "disabled"}.
39787
+ `);
39345
39788
  });
39346
39789
  registerCommands(program2, runtime);
39347
39790
  registerToolCommands(program2, runtime);
@@ -39352,6 +39795,18 @@ async function runCli(args = process.argv.slice(2), overrides = {}) {
39352
39795
  const runtime = cliRuntime(overrides);
39353
39796
  const program2 = createProgram(runtime);
39354
39797
  try {
39798
+ if (interactiveUpdateAllowed(args, runtime.env, runtime.updater)) {
39799
+ const restarted = await autoUpdate({
39800
+ args,
39801
+ version: package_default.version,
39802
+ env: runtime.env,
39803
+ runtime: runtime.updater,
39804
+ readConfig: runtime.readConfig,
39805
+ fetchImpl: runtime.fetchImpl,
39806
+ warn: runtime.err
39807
+ });
39808
+ if (restarted !== null) return restarted;
39809
+ }
39355
39810
  await program2.parseAsync(args, { from: "user" });
39356
39811
  return EXIT_CODES.OK;
39357
39812
  } catch (error63) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "higherdata",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "higherdata": "dist/index.js"