orangeslice 2.1.2 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,13 +6,16 @@ Orangeslice provides a `services.*` API for B2B research, enrichment, scraping,
6
6
 
7
7
  ```bash
8
8
  npx orangeslice
9
+ bunx orangeslice
10
+ pnpm dlx orangeslice
11
+ yarn dlx orangeslice
9
12
  ```
10
13
 
11
14
  The CLI copies docs to `./orangeslice-docs`, creates `./orangeslice-docs/AGENTS.md`, initializes `package.json` when missing, installs `orangeslice` in the current directory, opens browser auth, and stores your API key in `~/.config/orangeslice/config.json`.
12
15
 
13
16
  ### Auth behavior
14
17
 
15
- - `npx orangeslice` uses browser-based device auth and auto-provisions an API key.
18
+ - Any supported runner (`npx`, `bunx`, `pnpm dlx`, `yarn dlx`) uses browser-based device auth and auto-provisions an API key.
16
19
  - SDK credential precedence:
17
20
  1. `configure({ apiKey })`
18
21
  2. `ORANGESLICE_API_KEY` env var
@@ -21,23 +24,32 @@ The CLI copies docs to `./orangeslice-docs`, creates `./orangeslice-docs/AGENTS.
21
24
  ### CLI auth commands
22
25
 
23
26
  ```bash
24
- # One-time setup (also runs on plain `npx orangeslice` when unauthenticated)
27
+ # One-time setup (also runs on plain `npx orangeslice` / `bunx orangeslice` when unauthenticated)
25
28
  npx orangeslice login
29
+ bunx orangeslice login
30
+ pnpm dlx orangeslice login
31
+ yarn dlx orangeslice login
26
32
 
27
33
  # Force re-auth and replace local stored key
28
34
  npx orangeslice login --force
35
+ bunx orangeslice login --force
29
36
 
30
37
  # Remove locally stored key
31
38
  npx orangeslice logout
39
+ bunx orangeslice logout
32
40
 
33
41
  # Show current auth source (env or config file)
34
42
  npx orangeslice auth status
43
+ bunx orangeslice auth status
35
44
  ```
36
45
 
37
46
  Install as a dependency when writing app code:
38
47
 
39
48
  ```bash
40
49
  npm install orangeslice
50
+ bun add orangeslice
51
+ pnpm add orangeslice
52
+ yarn add orangeslice
41
53
  ```
42
54
 
43
55
  ## Public API (services-first)
@@ -101,7 +113,7 @@ All service calls go through `post()` in `src/api.ts`.
101
113
 
102
114
  ## Docs installed by CLI
103
115
 
104
- After `npx orangeslice`, you should have:
116
+ After running the CLI bootstrap command, you should have:
105
117
 
106
118
  ```text
107
119
  orangeslice-docs/
package/dist/api.js CHANGED
@@ -137,7 +137,7 @@ async function post(endpoint, payload) {
137
137
  const apiKey = resolveApiKey();
138
138
  if (!apiKey) {
139
139
  throw new Error("[orangeslice] No API key configured. " +
140
- "Run `npx orangeslice`, set ORANGESLICE_API_KEY in your environment, or call configure({ apiKey: 'osk_...' }).");
140
+ "Run `npx orangeslice`, `bunx orangeslice`, `pnpm dlx orangeslice`, or `yarn dlx orangeslice`, set ORANGESLICE_API_KEY in your environment, or call configure({ apiKey: 'osk_...' }).");
141
141
  }
142
142
  const url = `${baseUrl}${endpoint}`;
143
143
  const body = JSON.stringify({ ...payload, inlineWaitMs: DEFAULT_INLINE_WAIT_MS });
package/dist/cli.js CHANGED
@@ -47,6 +47,7 @@ const CONFIG_DIR = path.join(os.homedir(), ".config", "orangeslice");
47
47
  const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
48
48
  const AGENTS_IMPORT_LINE = "@orangeslice-docs/AGENTS.md";
49
49
  const CLAUDE_IMPORT_LINE = "@orangeslice-docs/CLAUDE.md";
50
+ const SUPPORTED_RUNNERS = ["npm", "bun", "pnpm", "yarn"];
50
51
  function isDir(p) {
51
52
  try {
52
53
  return fs.statSync(p).isDirectory();
@@ -71,6 +72,71 @@ function resolveDocsDir() {
71
72
  }
72
73
  return LEGACY_DOCS_DIR;
73
74
  }
75
+ function hasFile(cwd, fileName) {
76
+ return fs.existsSync(path.join(cwd, fileName));
77
+ }
78
+ function readPackageManagerFromPackageJson(cwd) {
79
+ const packageJsonPath = path.join(cwd, "package.json");
80
+ if (!fs.existsSync(packageJsonPath))
81
+ return null;
82
+ try {
83
+ const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
84
+ const packageManager = parsed.packageManager?.toLowerCase() || "";
85
+ if (packageManager.startsWith("bun@"))
86
+ return "bun";
87
+ if (packageManager.startsWith("pnpm@"))
88
+ return "pnpm";
89
+ if (packageManager.startsWith("yarn@"))
90
+ return "yarn";
91
+ if (packageManager.startsWith("npm@"))
92
+ return "npm";
93
+ }
94
+ catch { }
95
+ return null;
96
+ }
97
+ function detectPackageManager(cwd) {
98
+ const fromPackageJson = readPackageManagerFromPackageJson(cwd);
99
+ if (fromPackageJson)
100
+ return fromPackageJson;
101
+ if (hasFile(cwd, "bun.lock") || hasFile(cwd, "bun.lockb"))
102
+ return "bun";
103
+ if (hasFile(cwd, "pnpm-lock.yaml"))
104
+ return "pnpm";
105
+ if (hasFile(cwd, "yarn.lock"))
106
+ return "yarn";
107
+ if (hasFile(cwd, "package-lock.json") || hasFile(cwd, "npm-shrinkwrap.json"))
108
+ return "npm";
109
+ const userAgent = process.env.npm_config_user_agent?.toLowerCase() || "";
110
+ if (userAgent.includes("bun/"))
111
+ return "bun";
112
+ if (userAgent.includes("pnpm/"))
113
+ return "pnpm";
114
+ if (userAgent.includes("yarn/"))
115
+ return "yarn";
116
+ return "npm";
117
+ }
118
+ function getRunnerCommand(packageManager, suffix = "") {
119
+ const command = packageManager === "bun"
120
+ ? "bunx orangeslice"
121
+ : packageManager === "pnpm"
122
+ ? "pnpm dlx orangeslice"
123
+ : packageManager === "yarn"
124
+ ? "yarn dlx orangeslice"
125
+ : "npx orangeslice";
126
+ return suffix ? `${command} ${suffix}` : command;
127
+ }
128
+ function getInstallCommand(packageManager) {
129
+ if (packageManager === "bun")
130
+ return "bun add orangeslice";
131
+ if (packageManager === "pnpm")
132
+ return "pnpm add orangeslice";
133
+ if (packageManager === "yarn")
134
+ return "yarn add orangeslice";
135
+ return "npm install orangeslice";
136
+ }
137
+ function getSupportedRunnerExamples() {
138
+ return SUPPORTED_RUNNERS.map((packageManager) => getRunnerCommand(packageManager));
139
+ }
74
140
  function copyDirSync(src, dest) {
75
141
  fs.mkdirSync(dest, { recursive: true });
76
142
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
@@ -105,7 +171,7 @@ Use these docs as the source of truth. If there is any conflict between your pri
105
171
  - \`import { services } from "orangeslice"\`
106
172
  - \`import { configure, services } from "orangeslice"\` when setting API key programmatically
107
173
  - Do NOT use \`import { services } from "./orangeslice"\` unless the user explicitly has a local wrapper file at that path.
108
- - \`npx orangeslice\` is a setup/bootstrap command (docs sync, package install, auth). It does NOT execute user app scripts.
174
+ - The orangeslice bootstrap commands (\`npx orangeslice\`, \`bunx orangeslice\`, \`pnpm dlx orangeslice\`, \`yarn dlx orangeslice\`) sync docs, install the package, and handle auth. They do NOT execute user app scripts.
109
175
 
110
176
  ## Runtime Requirements
111
177
  - If writing standalone scripts that use top-level \`await\`, use ESM:
@@ -194,12 +260,21 @@ function ensurePackageJson(cwd) {
194
260
  const packageJsonPath = path.join(cwd, "package.json");
195
261
  if (fs.existsSync(packageJsonPath))
196
262
  return;
197
- console.log(" No package.json found. Initializing npm project...");
198
- (0, child_process_1.execSync)("npm init -y", { stdio: "inherit", cwd });
263
+ console.log(" No package.json found. Creating one...");
264
+ const defaultName = path
265
+ .basename(cwd)
266
+ .toLowerCase()
267
+ .replace(/[^a-z0-9._-]+/g, "-")
268
+ .replace(/^-+|-+$/g, "") || "orangeslice-app";
269
+ fs.writeFileSync(packageJsonPath, JSON.stringify({
270
+ name: defaultName,
271
+ version: "1.0.0",
272
+ private: true
273
+ }, null, 2) + "\n", "utf8");
199
274
  }
200
- function installOrangeslice(cwd) {
201
- console.log(" Installing orangeslice...");
202
- (0, child_process_1.execSync)("npm install orangeslice", { stdio: "inherit", cwd });
275
+ function installOrangeslice(cwd, packageManager) {
276
+ console.log(` Installing orangeslice with ${packageManager}...`);
277
+ (0, child_process_1.execSync)(getInstallCommand(packageManager), { stdio: "inherit", cwd });
203
278
  }
204
279
  function readConfigFile() {
205
280
  try {
@@ -312,13 +387,17 @@ async function setupApiKey(force = false) {
312
387
  saveConfigFile({ apiKey });
313
388
  console.log(` ✓ API key saved to ${CONFIG_PATH}\n`);
314
389
  }
315
- function printHelp() {
390
+ function printHelp(packageManager) {
316
391
  console.log("Usage:");
317
- console.log(" npx orangeslice");
318
- console.log(" npx orangeslice login [--force]");
319
- console.log(" npx orangeslice logout");
320
- console.log(" npx orangeslice auth <API_KEY>");
321
- console.log(" npx orangeslice auth status\n");
392
+ for (const command of getSupportedRunnerExamples()) {
393
+ console.log(` ${command}`);
394
+ }
395
+ console.log("");
396
+ console.log("Commands:");
397
+ console.log(` ${getRunnerCommand(packageManager, "login [--force]")}`);
398
+ console.log(` ${getRunnerCommand(packageManager, "logout")}`);
399
+ console.log(` ${getRunnerCommand(packageManager, "auth <API_KEY>")}`);
400
+ console.log(` ${getRunnerCommand(packageManager, "auth status")}\n`);
322
401
  }
323
402
  async function runLogin(args) {
324
403
  const force = args.includes("--force");
@@ -336,16 +415,16 @@ function runLogout() {
336
415
  console.log(" Note: ORANGESLICE_API_KEY env var is still set in this shell.");
337
416
  }
338
417
  }
339
- function runAuthSet(apiKey) {
418
+ function runAuthSet(apiKey, packageManager) {
340
419
  const trimmed = apiKey.trim();
341
420
  if (!trimmed) {
342
- console.log(" Usage: npx orangeslice auth <API_KEY>");
421
+ console.log(` Usage: ${getRunnerCommand(packageManager, "auth <API_KEY>")}`);
343
422
  return;
344
423
  }
345
424
  saveConfigFile({ apiKey: trimmed });
346
425
  console.log(` ✓ API key saved to ${CONFIG_PATH} (${maskKey(trimmed)})`);
347
426
  }
348
- function runAuthStatus() {
427
+ function runAuthStatus(packageManager) {
349
428
  const envKey = process.env.ORANGESLICE_API_KEY?.trim() || "";
350
429
  if (envKey) {
351
430
  console.log(` Authenticated via env var: ${maskKey(envKey)}`);
@@ -356,11 +435,13 @@ function runAuthStatus() {
356
435
  console.log(` Authenticated via config file (${CONFIG_PATH}): ${maskKey(fileKey)}`);
357
436
  return;
358
437
  }
359
- console.log(" Not authenticated. Run `npx orangeslice login`.");
438
+ console.log(` Not authenticated. Run \`${getRunnerCommand(packageManager, "login")}\`.`);
360
439
  }
361
440
  async function main() {
362
441
  const args = process.argv.slice(2);
363
442
  const command = args[0];
443
+ const cwd = process.cwd();
444
+ const packageManager = detectPackageManager(cwd);
364
445
  if (command === "login") {
365
446
  await runLogin(args.slice(1));
366
447
  return;
@@ -372,18 +453,18 @@ async function main() {
372
453
  if (command === "auth") {
373
454
  const subcommand = args[1];
374
455
  if (subcommand === "status") {
375
- runAuthStatus();
456
+ runAuthStatus(packageManager);
376
457
  return;
377
458
  }
378
459
  if (subcommand && subcommand !== "--help" && subcommand !== "-h") {
379
- runAuthSet(subcommand);
460
+ runAuthSet(subcommand, packageManager);
380
461
  return;
381
462
  }
382
- printHelp();
463
+ printHelp(packageManager);
383
464
  return;
384
465
  }
385
466
  if (command && (command === "--help" || command === "-h" || command === "help")) {
386
- printHelp();
467
+ printHelp(packageManager);
387
468
  return;
388
469
  }
389
470
  console.log("\norangeslice\n");
@@ -396,11 +477,10 @@ async function main() {
396
477
  writeClaudeGuide(TARGET_DIR);
397
478
  console.log(` ✓ Docs (${path.basename(docsDir)}) → ./orangeslice-docs/\n`);
398
479
  // Always set up package installation in the current directory.
399
- const cwd = process.cwd();
400
480
  ensureAgentsImport(cwd);
401
481
  ensureClaudeImport(cwd);
402
482
  ensurePackageJson(cwd);
403
- installOrangeslice(cwd);
483
+ installOrangeslice(cwd, packageManager);
404
484
  console.log(" ✓ Package installed in current directory\n");
405
485
  // API key setup
406
486
  await setupApiKey();
@@ -0,0 +1,52 @@
1
+ # getList
2
+
3
+ Get a specific HubSpot list by ID.
4
+
5
+ ```typescript
6
+ // Fetch a list by ID
7
+ const list = await integrations.hubspot.getList("123456");
8
+
9
+ // Include the filter definition in the response
10
+ const detailedList = await integrations.hubspot.getList("123456", {
11
+ includeFilters: true
12
+ });
13
+
14
+ console.log(list.name); // "Target Accounts"
15
+ console.log(list.processingType); // "DYNAMIC"
16
+ ```
17
+
18
+ ## Input
19
+
20
+ | Parameter | Type | Description |
21
+ | ------------------------ | --------- | --------------------------------------------------------------------- |
22
+ | `listId` | `string` | The HubSpot list ID to retrieve |
23
+ | `options.includeFilters` | `boolean` | Include the list's filter definition in the response (default: false) |
24
+
25
+ ## Output
26
+
27
+ ```typescript
28
+ {
29
+ listId: string;
30
+ listVersion: number;
31
+ name: string;
32
+ objectTypeId: string;
33
+ processingStatus: string;
34
+ processingType: string;
35
+ createdAt?: string;
36
+ updatedAt?: string;
37
+ deletedAt?: string;
38
+ createdById?: string;
39
+ updatedById?: string;
40
+ filtersUpdatedAt?: string;
41
+ size?: number;
42
+ membershipSettings?: Record<string, unknown>;
43
+ listPermissions?: Record<string, unknown>;
44
+ filterBranch?: Record<string, unknown>;
45
+ }
46
+ ```
47
+
48
+ ## Notes
49
+
50
+ - Set `includeFilters: true` when you need the list's filter definition
51
+ - `objectTypeId` identifies the CRM object type the list contains, such as contacts or companies
52
+ - `processingType` commonly indicates whether the list is dynamic, manual, or snapshot-based
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: HubSpot CRM - contacts, companies, deals, workflows
2
+ description: HubSpot CRM - contacts, companies, deals, lists, workflows
3
3
  ---
4
4
 
5
5
  # HubSpot Integration
@@ -41,6 +41,10 @@ Typed functions for HubSpot CRM operations.
41
41
  - `integrations.hubspot.updateFlow(flowId, input)` - Update an existing workflow
42
42
  - `integrations.hubspot.deleteFlow(flowId)` - Delete a workflow
43
43
 
44
+ ## Lists
45
+
46
+ - `integrations.hubspot.getList(listId, options?)` - Get list by ID
47
+
44
48
  ## Properties
45
49
 
46
50
  - `integrations.hubspot.listProperties(objectType, options?)` - List all property definitions for an object type
@@ -0,0 +1,154 @@
1
+ ---
2
+ name: lookalike-search
3
+ description: Find companies and people similar to a user's best customers or seed list using Ocean.io. Mandatory read for any "find me companies like X" or "lookalike" request.
4
+ ---
5
+
6
+ # Mode: Lookalike Search
7
+
8
+ **Goal:** Find companies and people that are similar to a user's existing customers, seed domains, or ideal customer profile — using Ocean.io's lookalike engine.
9
+
10
+ ---
11
+
12
+ ## When to Use Lookalike Search
13
+
14
+ Use Ocean.io lookalike search when the user:
15
+
16
+ - Has **example companies** (domains) and wants to find more like them
17
+ - Says "companies like", "similar to", "lookalike", "companies that look like my customers"
18
+ - Wants to **expand a seed list** of known-good accounts into a larger pipeline
19
+ - Needs **account-based prospecting** starting from a handful of reference accounts
20
+ - Wants to find the **right people** at companies matching a lookalike profile
21
+
22
+ **Not a good fit when:**
23
+
24
+ - User wants keyword/niche discovery ("AI CRM companies") → use `web.search`
25
+ - User wants funding-based filtering → use `crunchbase.search`
26
+ - User wants a specific company lookup → use LinkedIn B2B DB
27
+ - User only has descriptions, not example domains → use `web.search` or `crunchbase.search` first to build a seed list
28
+
29
+ ---
30
+
31
+ ## The Two Endpoints
32
+
33
+ ### 1. `services.ocean.search.companies` — Find Lookalike Companies
34
+
35
+ Provide seed domains and filters, get back similar companies with rich firmographic data.
36
+
37
+ ```ts
38
+ const results = await services.ocean.search.companies({
39
+ companiesFilters: {
40
+ lookalikeDomains: ["stripe.com", "brex.com", "ramp.com"],
41
+ companySizes: ["51-200", "201-500"],
42
+ countries: ["us"]
43
+ },
44
+ size: 50
45
+ });
46
+
47
+ // results.companies → array of { company, relevance }
48
+ // results.total → total matches
49
+ // results.searchAfter → cursor for next page
50
+ ```
51
+
52
+ **Key filters:**
53
+
54
+ | Filter | Example | Notes |
55
+ | ------------------ | -------------------------------- | ------------------------------------------------ |
56
+ | `lookalikeDomains` | `["stripe.com", "plaid.com"]` | Core input — seed domains to find lookalikes for |
57
+ | `companySizes` | `["51-200", "201-500"]` | Filter by employee count range |
58
+ | `countries` | `["us", "gb", "de"]` | Two-letter ISO country codes |
59
+ | `industries` | `["SaaS", "Fintech"]` | Industry category names |
60
+ | `technologies` | `["React", "Salesforce"]` | Filter by tech stack |
61
+ | `revenueRanges` | `["1M-10M", "10M-50M"]` | Revenue band filter |
62
+ | `keywords` | `["payments", "infrastructure"]` | Keyword filter |
63
+ | `ecommerce` | `true` | E-commerce companies only |
64
+ | `minScore` | `0.5` | Minimum similarity score (0–1) |
65
+
66
+ **Pagination:** Use `searchAfter` from the previous response for efficient cursor-based pagination. Max `size` per request is 100.
67
+
68
+ ### 2. `services.ocean.search.people` — Find People at Companies
69
+
70
+ Combine company filters with people filters to find the right contacts.
71
+
72
+ ```ts
73
+ const results = await services.ocean.search.people({
74
+ companiesFilters: {
75
+ lookalikeDomains: ["stripe.com", "brex.com"]
76
+ },
77
+ peopleFilters: {
78
+ seniorities: ["C-Level", "VP"],
79
+ departments: ["Engineering", "Product"]
80
+ },
81
+ size: 50,
82
+ enableEmailSearch: true
83
+ });
84
+
85
+ // results.people → array of OceanPersonResult
86
+ // Each person has: name, jobTitle, linkedinUrl, email, phone, company info
87
+ ```
88
+
89
+ **People filters:**
90
+
91
+ | Filter | Example | Notes |
92
+ | -------------------- | -------------------------------- | ------------------------------------------------ |
93
+ | `seniorities` | `["C-Level", "VP", "Director"]` | Job seniority levels |
94
+ | `departments` | `["Engineering", "Sales"]` | Department targeting |
95
+ | `jobTitleKeywords` | `["CTO", "Head of Engineering"]` | Title keyword matching |
96
+ | `countries` | `["us"]` | People location filter |
97
+ | `lookalikePeopleIds` | `["id1", "id2"]` | Find people similar to these Ocean.io person IDs |
98
+
99
+ **Contact data:**
100
+
101
+ - Set `enableEmailSearch: true` to get verified email addresses
102
+ - Set `enablePhoneSearch: true` to get verified phone numbers
103
+ - Both cost additional Ocean.io credits
104
+
105
+ ---
106
+
107
+ ## Standard Workflow
108
+
109
+ ### Company-first (most common)
110
+
111
+ 1. **Collect seed domains** — from user input, an existing sheet column, or a prior prospecting step
112
+ 2. **Search lookalike companies** — `services.ocean.search.companies` with seed domains + filters
113
+ 3. **Add to sheet** — populate a "Companies" sheet with domain, name, size, industry, etc.
114
+ 4. **Qualify** — add enrichment columns (LinkedIn enrich, tech stack, etc.) to score/filter
115
+ 5. **Find people** — add a column using `services.ocean.search.people` or `services.company.getEmployeesFromLinkedin`
116
+
117
+ ### People-direct (when you already know the company profile)
118
+
119
+ 1. **Search people directly** — `services.ocean.search.people` with company + people filters
120
+ 2. **Add to sheet** — populate with name, title, email, LinkedIn URL, company
121
+ 3. **Qualify** — add verification/enrichment columns
122
+
123
+ ---
124
+
125
+ ## Pagination Pattern
126
+
127
+ Ocean.io returns up to `size` results per call (max 100). For larger result sets, paginate with `searchAfter`:
128
+
129
+ ```ts
130
+ let allCompanies = [];
131
+ let searchAfter = undefined;
132
+
133
+ for (let page = 0; page < 5; page++) {
134
+ const results = await services.ocean.search.companies({
135
+ companiesFilters: { lookalikeDomains: ["stripe.com"] },
136
+ size: 100,
137
+ searchAfter
138
+ });
139
+
140
+ allCompanies.push(...results.companies);
141
+ searchAfter = results.searchAfter;
142
+
143
+ if (!searchAfter || allCompanies.length >= results.total) break;
144
+ }
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Tips
150
+
151
+ - **More seed domains = better results.** 3-5 seeds produce much better lookalikes than a single domain.
152
+ - **Combine with enrichment.** Ocean.io returns firmographic data, but you can enrich further with LinkedIn, BuiltWith, or PredictLeads.
153
+ - **Use `minScore`** to control result quality — higher = more similar but fewer results.
154
+ - **Credits:** 5 credits per result. A search returning 50 companies = 250 credits. Reserve is based on the requested `size`.