salesprompter-cli 0.1.81 → 0.1.82

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
@@ -64,6 +64,8 @@ Research runs reuse that profile in background Chrome, execute the GET requests
64
64
 
65
65
  The default `--browser session` keeps the extension-credential route. Credentials load only for pending requests; on an auth failure it tries changed credentials for the same identity once. Add `--wait-for-session 300` for a bounded refresh wait. The optional Codex `--browser-relay-port` route still needs its external worker. Salesprompter workspace authentication applies in every mode.
66
66
 
67
+ 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.
68
+
67
69
  Create a JSON brief with verified numeric LinkedIn company IDs. Use each subsidiary's own ID; names alone are not an employer match. Missing IDs stay in the coverage report as unresolved.
68
70
 
69
71
  For a name-only list, the executable end-to-end research mode is:
@@ -12,6 +12,14 @@ export class ChromeResearchError extends Error {
12
12
  this.name = "ChromeResearchError";
13
13
  }
14
14
  }
15
+ export class ChromeResearchInterruptedError extends Error {
16
+ signal;
17
+ constructor(signal) {
18
+ super("Research interrupted. Completed searches remain saved; rerun with the same --out-dir to resume.");
19
+ this.signal = signal;
20
+ this.name = "ChromeResearchInterruptedError";
21
+ }
22
+ }
15
23
  export function validateChromeResearchUrl(value) {
16
24
  const url = new URL(value);
17
25
  if (url.origin !== ORIGIN || url.username || url.password || url.hash ||
@@ -48,6 +56,8 @@ export async function launchResearchChrome(visible = false) {
48
56
  try {
49
57
  return await puppeteer.launch({ channel: "chrome", userDataDir: profile, pipe: true, headless: !visible,
50
58
  timeout: 30000, protocolTimeout: 45000, defaultViewport: null,
59
+ // Let the command unwind its checkpoint/lock finally blocks before exiting.
60
+ handleSIGINT: false, handleSIGTERM: false, handleSIGHUP: false,
51
61
  });
52
62
  }
53
63
  catch {
@@ -79,13 +89,34 @@ export function createChromeResearchBrowser(options = {}, launch = launchResearc
79
89
  let initialization;
80
90
  let closed = false;
81
91
  let busy = false;
92
+ let browserClosing;
93
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
94
+ const signalHandlers = signals.map(signal => () => {
95
+ if (fatal instanceof ChromeResearchInterruptedError)
96
+ return;
97
+ fatal = new ChromeResearchInterruptedError(signal);
98
+ closed = true;
99
+ void closeOwnedBrowser().catch(() => { });
100
+ });
101
+ function closeOwnedBrowser() {
102
+ if (browser)
103
+ browserClosing ??= browser.close();
104
+ return browserClosing ?? Promise.resolve();
105
+ }
82
106
  const waitMs = options.loginWaitMs ?? 30000;
83
107
  if (!Number.isFinite(waitMs) || waitMs < 0 || waitMs > 3600000)
84
108
  throw new Error("Chrome login wait must be between 0 and 3600 seconds.");
85
109
  async function initialize() {
110
+ signals.forEach((signal, index) => process.on(signal, signalHandlers[index]));
86
111
  browser = await launch(options.visible);
112
+ if (closed) {
113
+ await closeOwnedBrowser();
114
+ throw fatal ?? new ChromeResearchError("Research Chrome connection was cancelled.");
115
+ }
87
116
  page = await browser.newPage();
88
117
  page.on("response", (response) => {
118
+ if (fatal)
119
+ return;
89
120
  const url = new URL(response.url());
90
121
  if (url.origin !== ORIGIN)
91
122
  return;
@@ -114,6 +145,8 @@ export function createChromeResearchBrowser(options = {}, launch = launchResearc
114
145
  await page.goto(`${ORIGIN}/sales/search/people`, { waitUntil: "domcontentloaded", timeout: 30000 });
115
146
  }
116
147
  catch {
148
+ if (closed)
149
+ throw fatal ?? new ChromeResearchError("Research Chrome connection was cancelled.");
117
150
  if (!browser.connected)
118
151
  throw new ChromeResearchError("Research Chrome closed before connecting.");
119
152
  }
@@ -121,7 +154,7 @@ export function createChromeResearchBrowser(options = {}, launch = launchResearc
121
154
  let noticeAt = Date.now() + 30000;
122
155
  while (!headers) {
123
156
  if (closed)
124
- throw new ChromeResearchError("Research Chrome connection was cancelled.");
157
+ throw fatal ?? new ChromeResearchError("Research Chrome connection was cancelled.");
125
158
  if (fatal)
126
159
  throw fatal;
127
160
  if (!browser.connected || page.isClosed())
@@ -142,7 +175,7 @@ export function createChromeResearchBrowser(options = {}, launch = launchResearc
142
175
  }
143
176
  async function ready() {
144
177
  if (closed)
145
- throw new Error("Research Chrome is closed.");
178
+ throw fatal ?? new Error("Research Chrome is closed.");
146
179
  initialization ??= initialize();
147
180
  await initialization;
148
181
  if (fatal)
@@ -171,7 +204,7 @@ export function createChromeResearchBrowser(options = {}, launch = launchResearc
171
204
  const text = await response.text();
172
205
  if (text.length > 50 * 1024 * 1024) throw new Error('Research response exceeded 50 MB');
173
206
  return {status: response.status, text, redirected: response.redirected, retryAfter: response.headers.get('retry-after')};
174
- })()`).catch(() => { throw new ChromeResearchError(`Chrome research request could not complete. ${LOGIN_HELP}`); });
207
+ })()`).catch(() => { throw fatal ?? new ChromeResearchError(`Chrome research request could not complete. ${LOGIN_HELP}`); });
175
208
  const body = parseChromeResearchResult(result);
176
209
  if (fatal)
177
210
  throw fatal;
@@ -181,6 +214,18 @@ export function createChromeResearchBrowser(options = {}, launch = launchResearc
181
214
  busy = false;
182
215
  }
183
216
  },
184
- async close() { closed = true; await initialization?.catch(() => { }); await browser?.close(); headers = null; identity = undefined; },
217
+ async close() {
218
+ closed = true;
219
+ try {
220
+ await closeOwnedBrowser();
221
+ await initialization?.catch(() => { });
222
+ await closeOwnedBrowser();
223
+ }
224
+ finally {
225
+ signals.forEach((signal, index) => process.off(signal, signalHandlers[index]));
226
+ headers = null;
227
+ identity = undefined;
228
+ }
229
+ },
185
230
  };
186
231
  }
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import { z } from "zod";
17
17
  import { AffiliateCopyReviewSchema, renderAffiliateCopyReview } from "./affiliate-copy.js";
18
18
  import { CompanyBriefSchema, planCompanySearches, runCompanyResearch } from "./company-leads.js";
19
19
  import { createLazySessionRecovery, waitForFreshSession, isFreshSessionForIdentity } from "./session-recovery.js";
20
- import { createChromeResearchBrowser } from "./chrome-browser.js";
20
+ import { createChromeResearchBrowser, ChromeResearchInterruptedError } from "./chrome-browser.js";
21
21
  import { clearAuthSession, loginWithBrowserConnect, loginWithDeviceFlow, loginWithToken, readAuthSession, requireAuthSession, shouldBypassAuth, verifySession, writeAuthSession } from "./auth.js";
22
22
  import { buildBigQueryLeadLookupSql, executeBigQuerySql, normalizeBigQueryLeadRows, runBigQueryQuery, runBigQueryRows } from "./bigquery.js";
23
23
  import { AccountProfileSchema, EnrichedLeadSchema, IcpSchema, LeadSchema, ScoredLeadSchema, SyncTargetSchema } from "./domain.js";
@@ -19563,6 +19563,11 @@ async function closeGlobalHttpDispatcher() {
19563
19563
  }
19564
19564
  main()
19565
19565
  .catch((error) => {
19566
+ if (error instanceof ChromeResearchInterruptedError) {
19567
+ process.stderr.write(runtimeOutputOptions.json ? `${JSON.stringify({ status: "interrupted", message: error.message, signal: error.signal, resumable: true })}\n` : `${error.message}\n`);
19568
+ process.exitCode = error.signal === "SIGINT" ? 130 : error.signal === "SIGTERM" ? 143 : 129;
19569
+ return;
19570
+ }
19566
19571
  if (error instanceof Error &&
19567
19572
  (error.message === "prompt cancelled" || error.message === "readline was closed")) {
19568
19573
  process.exitCode = 130;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.81",
3
+ "version": "0.1.82",
4
4
  "description": "Sales workflow CLI for guided lead generation, enrichment, scoring, and sync.",
5
5
  "author": "Daniel Sinewe <hello@danielsinewe.com>",
6
6
  "type": "module",