indiecrm-cli 0.2.2 → 0.2.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/CHANGELOG.md +5 -0
- package/README.md +4 -3
- package/RESEARCH.md +14 -4
- package/browser-extension/background.js +144 -0
- package/browser-extension/manifest.json +11 -0
- package/browser-extension/popup.html +21 -0
- package/browser-extension/popup.js +30 -0
- package/dist/cli.js +18 -2
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.3
|
|
4
|
+
|
|
5
|
+
- Add an IndieCRM Chrome companion that runs Sales Navigator research in an already signed-in tab through the local CLI relay, without copying browser cookies or asking for a second password.
|
|
6
|
+
- Add `browser:extension` setup guidance and make the existing-browser path the first research quickstart option.
|
|
7
|
+
|
|
3
8
|
## 0.2.2
|
|
4
9
|
|
|
5
10
|
- Finish Salesprompter browser login even when the browser proxy keeps a localhost callback connection open.
|
package/README.md
CHANGED
|
@@ -34,11 +34,12 @@ The original Salesprompter research commands are available under the `indiecrm`
|
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
36
|
indiecrm auth:login
|
|
37
|
-
indiecrm browser:
|
|
38
|
-
indiecrm leads:at-companies --brief companies.json --out-dir ./research
|
|
37
|
+
indiecrm browser:extension
|
|
38
|
+
indiecrm leads:at-companies --brief companies.json --out-dir ./research \
|
|
39
|
+
--resolve-companies --all --browser-relay-port 43117
|
|
39
40
|
```
|
|
40
41
|
|
|
41
|
-
For example, `companies.json` can contain `{ "companies": [{ "name": "bizforward GmbH", "maxContacts": 20 }] }`. Use `--dry-run` to preview targeting without collecting data. The existing Salesprompter config, workspaces, saved research checkpoints, and service integrations remain supported. See [RESEARCH.md](RESEARCH.md) for the full research workflow and recovery options.
|
|
42
|
+
For example, `companies.json` can contain `{ "companies": [{ "name": "bizforward GmbH", "maxContacts": 20 }] }`. Use `--dry-run` to preview targeting without collecting data. The Chrome companion reuses a signed-in Sales Navigator tab without asking for a second password; `indiecrm browser:connect` remains available for an isolated research profile. The existing Salesprompter config, workspaces, saved research checkpoints, and service integrations remain supported. See [RESEARCH.md](RESEARCH.md) for the full research workflow and recovery options.
|
|
42
43
|
|
|
43
44
|
## Development
|
|
44
45
|
|
package/RESEARCH.md
CHANGED
|
@@ -54,11 +54,12 @@ Start with company names; no LinkedIn IDs are needed in your brief:
|
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
indiecrm auth:login
|
|
57
|
-
indiecrm browser:
|
|
58
|
-
indiecrm leads:at-companies --brief companies.json --out-dir ./research
|
|
57
|
+
indiecrm browser:extension
|
|
58
|
+
indiecrm leads:at-companies --brief companies.json --out-dir ./research \
|
|
59
|
+
--resolve-companies --all --browser-relay-port 43117
|
|
59
60
|
```
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
Load the companion once in the Chrome profile already signed in to Sales Navigator, then click its popup and connect to the printed port while the research command runs. See the browser instructions below. If you prefer an isolated research Chrome profile, use `indiecrm browser:connect`; that route requires its own Sales Navigator sign-in and is remembered for future research commands. If names are ambiguous, an interactive terminal shows verified candidate locations, websites and evidence links. Choose a number, then confirm; nothing is preselected. The CLI saves the decision and resumes without editing your brief. Use `--review` to review a saved ambiguity later. Enter 0 or press Enter to leave it unresolved.
|
|
62
63
|
|
|
63
64
|
JSON, quiet and non-interactive runs never prompt. Their `outcome` is `complete`, `incomplete` or `needs_review`; `nextCommand` gives the terminal command for pending collection or identity review. `collectionComplete` refers only to collection, so it can be true while role review remains. Add `--require-complete` for exit code 2 whenever collection or review is unfinished. Without it, exit 0 means the command ran, not that all research is complete. `--no-interactive` disables prompts in a terminal too.
|
|
64
65
|
|
|
@@ -121,11 +122,20 @@ indiecrm leads:at-companies --brief companies.json --out-dir ./research \
|
|
|
121
122
|
--resolve-companies --all
|
|
122
123
|
```
|
|
123
124
|
|
|
125
|
+
If you are already signed in to Sales Navigator in your everyday Chrome profile, you can reuse that tab without typing a password or copying the profile. Start the research command in one terminal with a free local port:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
indiecrm leads:at-companies --brief companies.json --out-dir ./research \
|
|
129
|
+
--resolve-companies --all --browser-relay-port 43117
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Run `indiecrm browser:extension` to find the bundled Chrome companion. In the Chrome profile already signed in to Sales Navigator, open `chrome://extensions`, enable Developer mode, choose **Load unpacked**, and select that directory. Return to your Sales Navigator tab, click **IndieCRM Research**, enter `43117`, and click **Connect**. Keep the tab open while research runs; the popup can be closed. The companion asks for access only to the active tab and local `127.0.0.1`, accepts only Sales Navigator company and lead GET requests, sends their results to the CLI, and keeps browser cookies in Chrome. Disconnect in the popup when finished. Rerun the research command with the same output directory to resume saved work. The signed-in tab must have access to Sales Navigator; a regular LinkedIn feed alone is insufficient.
|
|
133
|
+
|
|
124
134
|
`browser:connect` opens a separate Chrome window and waits up to ten minutes for your manual Sales Navigator sign-in (change with `--wait <seconds>`). It confirms a successful native authenticated request, then closes its own browser. Google Chrome must be installed. The private profile lives at `~/.config/salesprompter/chrome-research` (or under `SALESPROMPTER_CONFIG_DIR`). Your everyday Chrome profile stays untouched. The control connection uses a private OS pipe, not a debugging port. No cookies or browser storage are read/exported. The CLI keeps only allowlisted native request headers in memory.
|
|
125
135
|
|
|
126
136
|
Research runs reuse that profile in background Chrome, execute the GET requests themselves, and close their own browser when finished. No Codex task, relay worker, extension, or manual request polling is needed. A first sign-in, expired login, or security challenge still requires you: rerun `browser:connect`, then repeat the research command with the same output directory. Rate limits stop immediately without switching accounts. `--browser chrome`, `--browser-relay-port`, and `--wait-for-session` are mutually exclusive. Chrome connects lazily, so completed checkpoints and report-only runs never launch it.
|
|
127
137
|
|
|
128
|
-
`browser:connect` remembers Chrome only after confirming a successful authenticated request. `browser:connect --check` checks an existing sign-in in background and also remembers it; it does not open a sign-in window. The preference is private and scoped to the CLI configuration directory. Explicit `--browser session` selects extension credentials; without a remembered choice, session remains the fallback. Explicit relay/wait flags take precedence over the preference. Credentials load only for pending requests; on an auth failure the session route tries changed credentials for the same identity once. Add `--wait-for-session 300` for a bounded refresh wait. The
|
|
138
|
+
`browser:connect` remembers Chrome only after confirming a successful authenticated request. `browser:connect --check` checks an existing sign-in in background and also remembers it; it does not open a sign-in window. The preference is private and scoped to the CLI configuration directory. Explicit `--browser session` selects extension credentials; without a remembered choice, session remains the fallback. Explicit relay/wait flags take precedence over the preference. Credentials load only for pending requests; on an auth failure the session route tries changed credentials for the same identity once. Add `--wait-for-session 300` for a bounded refresh wait. The `--browser-relay-port` route works with the signed-in Chrome worker above or a Codex browser worker. Salesprompter workspace authentication applies in every mode.
|
|
129
139
|
|
|
130
140
|
Ctrl+C, SIGTERM and terminal hangup stop Chrome research gracefully: completed searches stay saved, the owned browser closes, and the research lock is released before exit. Repeat the same command to resume only pending work. A forced kill or power loss cannot run cleanup; if a stale lock remains, confirm that its process stopped before removing only `.research-lock` as the CLI error explains.
|
|
131
141
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const STATE_KEY = "indiecrmResearch";
|
|
2
|
+
const ALARM = "indiecrmResearchPoll";
|
|
3
|
+
let pumping = false;
|
|
4
|
+
|
|
5
|
+
function allowedResearchUrl(value) {
|
|
6
|
+
try {
|
|
7
|
+
const url = new URL(value);
|
|
8
|
+
return url.origin === "https://www.linkedin.com" &&
|
|
9
|
+
/^\/sales-api\/(?:salesApiAccountSearch|salesApiLeadSearch|salesApiCompanies)(?:\/\d+)?$/.test(url.pathname) &&
|
|
10
|
+
!url.username && !url.password && !url.hash;
|
|
11
|
+
} catch { return false; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function state() {
|
|
15
|
+
return (await chrome.storage.session.get(STATE_KEY))[STATE_KEY] || null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function setState(value) {
|
|
19
|
+
await chrome.storage.session.set({ [STATE_KEY]: value });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function stop(message) {
|
|
23
|
+
const current = await state();
|
|
24
|
+
await setState({ port: current?.port || null, message, connected: false });
|
|
25
|
+
await chrome.alarms.clear(ALARM);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function requestInTab(url) {
|
|
29
|
+
if (location.origin !== "https://www.linkedin.com" || !location.pathname.startsWith("/sales/")) {
|
|
30
|
+
return { status: 401, body: { error: "Sales Navigator tab is no longer signed in" }, retryAfter: null };
|
|
31
|
+
}
|
|
32
|
+
const session = document.cookie.match(/(?:^|;\s*)JSESSIONID="?([^";]+)/);
|
|
33
|
+
const csrf = session && decodeURIComponent(session[1]);
|
|
34
|
+
if (!csrf) return { status: 401, body: { error: "No Sales Navigator session in this tab" }, retryAfter: null };
|
|
35
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(csrf));
|
|
36
|
+
const fingerprint = Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, "0")).join("");
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(url, {
|
|
39
|
+
method: "GET", credentials: "include", redirect: "error",
|
|
40
|
+
headers: { accept: "*/*", "x-restli-protocol-version": "2.0.0", "csrf-token": csrf },
|
|
41
|
+
signal: AbortSignal.timeout(30000)
|
|
42
|
+
});
|
|
43
|
+
const text = await response.text();
|
|
44
|
+
if (text.length > 50 * 1024 * 1024) return { status: 502, body: { error: "Response exceeded 50 MB" }, retryAfter: null, fingerprint };
|
|
45
|
+
let body;
|
|
46
|
+
try { body = JSON.parse(text); }
|
|
47
|
+
catch { body = { error: "Sales Navigator did not return JSON" }; }
|
|
48
|
+
return { status: response.status, body, retryAfter: response.headers.get("retry-after"), fingerprint };
|
|
49
|
+
} catch {
|
|
50
|
+
return { status: 503, body: { error: "Sales Navigator request failed" }, retryAfter: null, fingerprint };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function pump() {
|
|
55
|
+
if (pumping) return;
|
|
56
|
+
pumping = true;
|
|
57
|
+
try {
|
|
58
|
+
while (true) {
|
|
59
|
+
const current = await state();
|
|
60
|
+
if (!current?.connected) return;
|
|
61
|
+
const relay = `http://127.0.0.1:${current.port}`;
|
|
62
|
+
let task;
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(relay + "/next", { cache: "no-store", signal: AbortSignal.timeout(5000) });
|
|
65
|
+
if (!response.ok) throw new Error("Local relay unavailable");
|
|
66
|
+
task = await response.json();
|
|
67
|
+
} catch {
|
|
68
|
+
await stop("Local CLI relay disconnected. Restart the research command, then connect again.");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (task.status === "task") {
|
|
72
|
+
if (!allowedResearchUrl(task.url) || typeof task.requestId !== "string") {
|
|
73
|
+
await stop("The local relay requested a URL outside the Sales Navigator allowlist.");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let result;
|
|
77
|
+
try {
|
|
78
|
+
const injected = await chrome.scripting.executeScript({
|
|
79
|
+
target: { tabId: current.tabId }, func: requestInTab, args: [task.url]
|
|
80
|
+
});
|
|
81
|
+
result = injected[0]?.result;
|
|
82
|
+
if (!result || typeof result.status !== "number") throw new Error("No result from tab");
|
|
83
|
+
} catch {
|
|
84
|
+
result = { status: 401, body: { error: "Sales Navigator tab was closed or navigated away" }, retryAfter: null };
|
|
85
|
+
}
|
|
86
|
+
if (current.fingerprint && result.fingerprint && current.fingerprint !== result.fingerprint) {
|
|
87
|
+
result = { status: 401, body: { error: "Sales Navigator session changed during research" }, retryAfter: null };
|
|
88
|
+
} else if (!current.fingerprint && result.fingerprint) {
|
|
89
|
+
await setState({ ...current, fingerprint: result.fingerprint });
|
|
90
|
+
}
|
|
91
|
+
delete result.fingerprint;
|
|
92
|
+
try {
|
|
93
|
+
const accepted = await fetch(relay + "/result", {
|
|
94
|
+
method: "POST", headers: { "content-type": "application/json" },
|
|
95
|
+
body: JSON.stringify({ requestId: task.requestId, ...result }),
|
|
96
|
+
signal: AbortSignal.timeout(5000)
|
|
97
|
+
});
|
|
98
|
+
if (!accepted.ok) throw new Error("Local relay rejected result");
|
|
99
|
+
} catch {
|
|
100
|
+
await stop("Local CLI relay disconnected before accepting a result.");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if ([401, 403, 429, 999].includes(result.status)) {
|
|
104
|
+
await stop(`Sales Navigator returned HTTP ${result.status}. Research stopped.`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
109
|
+
}
|
|
110
|
+
} finally { pumping = false; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
chrome.runtime.onMessage.addListener((message, _sender, respond) => {
|
|
114
|
+
(async () => {
|
|
115
|
+
if (message.action === "status") {
|
|
116
|
+
const current = await state();
|
|
117
|
+
return current || { connected: false, message: "Open a signed-in Sales Navigator tab to connect." };
|
|
118
|
+
}
|
|
119
|
+
if (message.action === "disconnect") {
|
|
120
|
+
await stop("Disconnected.");
|
|
121
|
+
return { connected: false, message: "Disconnected." };
|
|
122
|
+
}
|
|
123
|
+
if (message.action === "connect") {
|
|
124
|
+
const port = Number(message.port);
|
|
125
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535 || !Number.isInteger(message.tabId)) {
|
|
126
|
+
return { connected: false, message: "Invalid tab or CLI port." };
|
|
127
|
+
}
|
|
128
|
+
const tab = await chrome.tabs.get(message.tabId);
|
|
129
|
+
if (!tab.url?.startsWith("https://www.linkedin.com/sales/")) {
|
|
130
|
+
return { connected: false, message: "Open Sales Navigator in this tab first." };
|
|
131
|
+
}
|
|
132
|
+
const response = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(5000) });
|
|
133
|
+
if (!response.ok) return { connected: false, message: "Start the CLI research command first." };
|
|
134
|
+
await setState({ connected: true, tabId: message.tabId, port, message: `Connected to local CLI port ${port}. Keep this tab open.` });
|
|
135
|
+
await chrome.alarms.create(ALARM, { periodInMinutes: 0.5 });
|
|
136
|
+
void pump();
|
|
137
|
+
return { connected: true, port, message: `Connected to local CLI port ${port}. Keep this tab open.` };
|
|
138
|
+
}
|
|
139
|
+
return { connected: false, message: "Unknown action." };
|
|
140
|
+
})().then(respond, error => respond({ connected: false, message: error.message || "Connection failed." }));
|
|
141
|
+
return true;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
chrome.alarms.onAlarm.addListener(alarm => { if (alarm.name === ALARM) void pump(); });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "IndieCRM Research",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Use your active Sales Navigator tab for local IndieCRM research without copying browser credentials.",
|
|
6
|
+
"minimum_chrome_version": "120",
|
|
7
|
+
"permissions": ["activeTab", "scripting", "storage", "alarms"],
|
|
8
|
+
"host_permissions": ["http://127.0.0.1/*"],
|
|
9
|
+
"background": { "service_worker": "background.js" },
|
|
10
|
+
"action": { "default_title": "IndieCRM Research", "default_popup": "popup.html" }
|
|
11
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<meta charset="utf-8">
|
|
4
|
+
<title>IndieCRM Research</title>
|
|
5
|
+
<style>
|
|
6
|
+
body { width: 290px; margin: 16px; font: 14px system-ui, sans-serif; color: #14272d; }
|
|
7
|
+
h1 { font-size: 17px; margin: 0 0 8px; }
|
|
8
|
+
p { line-height: 1.4; }
|
|
9
|
+
label { display: block; margin: 16px 0 4px; }
|
|
10
|
+
input { box-sizing: border-box; width: 100%; padding: 8px; font: inherit; }
|
|
11
|
+
button { padding: 8px 12px; margin: 12px 8px 0 0; cursor: pointer; }
|
|
12
|
+
#status { min-height: 2.8em; color: #345; }
|
|
13
|
+
</style>
|
|
14
|
+
<h1>IndieCRM Research</h1>
|
|
15
|
+
<p>Use this signed-in Sales Navigator tab for your local research run.</p>
|
|
16
|
+
<label for="port">CLI relay port</label>
|
|
17
|
+
<input id="port" type="number" min="1" max="65535" value="43117">
|
|
18
|
+
<button id="connect">Connect</button><button id="disconnect">Disconnect</button>
|
|
19
|
+
<p id="status" role="status"></p>
|
|
20
|
+
<script src="popup.js"></script>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const portField = document.getElementById("port");
|
|
2
|
+
const statusField = document.getElementById("status");
|
|
3
|
+
|
|
4
|
+
async function refresh() {
|
|
5
|
+
const state = await chrome.runtime.sendMessage({ action: "status" });
|
|
6
|
+
if (state.port) portField.value = String(state.port);
|
|
7
|
+
statusField.textContent = state.message || "Open a signed-in Sales Navigator tab to connect.";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
document.getElementById("connect").addEventListener("click", async () => {
|
|
11
|
+
const port = Number(portField.value);
|
|
12
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
13
|
+
statusField.textContent = "Enter the port shown by the CLI (1–65535).";
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
17
|
+
if (!tab?.id || !tab.url?.startsWith("https://www.linkedin.com/sales/")) {
|
|
18
|
+
statusField.textContent = "Open a signed-in Sales Navigator tab first.";
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const state = await chrome.runtime.sendMessage({ action: "connect", tabId: tab.id, port });
|
|
22
|
+
statusField.textContent = state.message;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
document.getElementById("disconnect").addEventListener("click", async () => {
|
|
26
|
+
const state = await chrome.runtime.sendMessage({ action: "disconnect" });
|
|
27
|
+
statusField.textContent = state.message;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
void refresh();
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import path from "node:path";
|
|
|
9
9
|
import { emitKeypressEvents } from "node:readline";
|
|
10
10
|
import { createInterface } from "node:readline/promises";
|
|
11
11
|
import { setTimeout as delay } from "node:timers/promises";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
12
13
|
import { gzipSync } from "node:zlib";
|
|
13
14
|
import { createClient } from "@supabase/supabase-js";
|
|
14
15
|
import pg from "pg";
|
|
@@ -796,6 +797,7 @@ const helpVisibleCommandNames = new Set([
|
|
|
796
797
|
"wizard",
|
|
797
798
|
"auth:whoami",
|
|
798
799
|
"browser:connect",
|
|
800
|
+
"browser:extension",
|
|
799
801
|
"llm:ready",
|
|
800
802
|
"leads:download",
|
|
801
803
|
"leads:at-companies",
|
|
@@ -15152,12 +15154,24 @@ program
|
|
|
15152
15154
|
.action(async (options) => {
|
|
15153
15155
|
printOutput(await runSalesNavigatorPeopleCollectCommand(options));
|
|
15154
15156
|
});
|
|
15157
|
+
program
|
|
15158
|
+
.command("browser:extension")
|
|
15159
|
+
.description("Show the bundled Chrome companion for an already signed-in Sales Navigator tab.")
|
|
15160
|
+
.action(() => {
|
|
15161
|
+
const directory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "browser-extension");
|
|
15162
|
+
if (runtimeOutputOptions.json)
|
|
15163
|
+
printOutput({ status: "ok", directory });
|
|
15164
|
+
else
|
|
15165
|
+
process.stdout.write(`Open chrome://extensions in your signed-in Chrome profile, enable Developer mode, choose Load unpacked, and select:\n${directory}\nThen open Sales Navigator, click the IndieCRM Research extension, and connect it to the port printed by your research command.\n`);
|
|
15166
|
+
});
|
|
15155
15167
|
program
|
|
15156
15168
|
.command("browser:connect")
|
|
15157
15169
|
.description("Sign in once to the CLI-owned research Chrome; no external browser worker required.")
|
|
15158
15170
|
.option("--wait <seconds>", "Time allowed for manual LinkedIn sign-in", "600")
|
|
15159
15171
|
.option("--check", "Check the saved sign-in in background without opening a sign-in window", false)
|
|
15160
15172
|
.action(async (options) => {
|
|
15173
|
+
if (!options.check)
|
|
15174
|
+
process.stderr.write("This opens an isolated Chrome profile. Already signed in to Sales Navigator in everyday Chrome? Use indiecrm browser:extension with --browser-relay-port instead.\n");
|
|
15161
15175
|
const browser = createChromeResearchBrowser({ visible: !options.check, loginWaitMs: options.check ? 5000 : z.coerce.number().int().min(1).max(3600).parse(options.wait) * 1000, onProgress: message => process.stderr.write(`${message}\n`) });
|
|
15162
15176
|
try {
|
|
15163
15177
|
const connection = await browser.connect();
|
|
@@ -15186,7 +15200,7 @@ program
|
|
|
15186
15200
|
.option("--continue-partial <mode>", "Resume saved partial pages: quota or exhaustive (2500-result window)")
|
|
15187
15201
|
.option("--company <name>", "Limit recovery to an exact brief company name; repeat for multiple companies", (value, previous) => [...previous, value], [])
|
|
15188
15202
|
.option("--wait-for-session <seconds>", "Wait for a refreshed extension session after an auth failure; never retry rate limits", "0")
|
|
15189
|
-
.option("--browser-relay-port <number>", "Use
|
|
15203
|
+
.option("--browser-relay-port <number>", "Use an already signed-in Sales Navigator tab through a loopback relay")
|
|
15190
15204
|
.option("--browser <mode>", "Override saved connection: chrome or session; browser:connect remembers chrome")
|
|
15191
15205
|
.option("--dry-run", "Preview targeting and unresolved companies without network calls", false)
|
|
15192
15206
|
.action(async (options) => {
|
|
@@ -15242,6 +15256,8 @@ program
|
|
|
15242
15256
|
}
|
|
15243
15257
|
const port = options.browserRelayPort == null ? null : z.coerce.number().int().min(1).max(65535).parse(options.browserRelayPort);
|
|
15244
15258
|
const relay = browserMode === "chrome" ? createChromeResearchBrowser({ onProgress: message => process.stderr.write(`${message}\n`) }) : port == null ? null : await createLocalAccountSearchBrowserRelay(port);
|
|
15259
|
+
if (port !== null)
|
|
15260
|
+
writeProgress(`In the signed-in Sales Navigator tab, click the IndieCRM Research extension and connect port ${port}. Run indiecrm browser:extension for installation instructions.`);
|
|
15245
15261
|
let currentConfig = null;
|
|
15246
15262
|
const refresh = async () => {
|
|
15247
15263
|
if (relay || shouldDisableLinkedInDirectLookupAutodiscovery())
|
|
@@ -15261,7 +15277,7 @@ program
|
|
|
15261
15277
|
}
|
|
15262
15278
|
catch (error) {
|
|
15263
15279
|
if (!waitMs && error instanceof Error && error.message.startsWith("Missing LinkedIn direct lookup session"))
|
|
15264
|
-
throw new Error("No LinkedIn session is available.
|
|
15280
|
+
throw new Error("No LinkedIn session is available. If Sales Navigator is already signed in in Chrome, run indiecrm browser:extension and use --browser-relay-port 43117. Otherwise run indiecrm browser:connect and retry with --browser chrome. Saved work is preserved.");
|
|
15265
15281
|
if (!waitMs || !(error instanceof Error) || !error.message.startsWith("Missing LinkedIn direct lookup session"))
|
|
15266
15282
|
throw error;
|
|
15267
15283
|
currentConfig = await refresh();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "indiecrm-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "IndieCRM CLI for CRM workspaces and Salesprompter research workflows",
|
|
5
5
|
"author": "Daniel Sinewe <hello@danielsinewe.com>",
|
|
6
6
|
"type": "module",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
+
"browser-extension",
|
|
12
13
|
"README.md",
|
|
13
14
|
"RESEARCH.md",
|
|
14
15
|
"CHANGELOG.md",
|