salesprompter-cli 0.1.3 → 0.1.5
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 +65 -7
- package/dist/auth.js +107 -0
- package/dist/cli.js +81 -4
- package/package.json +27 -6
package/README.md
CHANGED
|
@@ -12,6 +12,59 @@
|
|
|
12
12
|
- Analyze upstream lead-list and domain-enrichment bottlenecks
|
|
13
13
|
- Replace opaque Pipedream logic with deterministic CLI workflows
|
|
14
14
|
|
|
15
|
+
It is built for two users at the same time:
|
|
16
|
+
|
|
17
|
+
- humans working in a terminal
|
|
18
|
+
- coding agents such as Codex, Claude Code, and other LLM-driven shell workflows
|
|
19
|
+
|
|
20
|
+
## Start Here
|
|
21
|
+
|
|
22
|
+
If someone discovers Salesprompter from a vague prompt, the first thing they need is the shortest working path.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx salesprompter-cli@latest --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install it globally:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g salesprompter-cli
|
|
32
|
+
salesprompter --help
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Prompt To Command
|
|
36
|
+
|
|
37
|
+
If the user says something like "I need to determine the ICP of deel.com", there are two different meanings.
|
|
38
|
+
|
|
39
|
+
### 1. They want leads at Deel itself
|
|
40
|
+
|
|
41
|
+
This means Deel is the target account.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
salesprompter account:resolve --domain deel.com --company-name Deel --out ./data/deel-account.json
|
|
45
|
+
salesprompter leads:generate --icp ./data/icp.json --count 5 --domain deel.com --company-name Deel --out ./data/deel-leads.json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. They sell for Deel and need Deel's ideal customer profile
|
|
49
|
+
|
|
50
|
+
This means Deel is the vendor, not the target account.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
salesprompter icp:vendor --vendor deel --market dach --out ./data/deel-icp.json
|
|
54
|
+
salesprompter leads:lookup:bq --icp ./data/deel-icp.json --limit 100 --execute --out ./data/deel-leads-raw.json --lead-out ./data/deel-leads.json
|
|
55
|
+
salesprompter leads:enrich --in ./data/deel-leads.json --out ./data/deel-enriched.json
|
|
56
|
+
salesprompter leads:score --icp ./data/deel-icp.json --in ./data/deel-enriched.json --out ./data/deel-scored.json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. They want the LLM to call the CLI directly
|
|
60
|
+
|
|
61
|
+
Use the same commands, but prefer machine-readable output:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
salesprompter --json icp:vendor --vendor deel --market dach --out ./data/deel-icp.json
|
|
65
|
+
salesprompter --json leads:lookup:bq --icp ./data/deel-icp.json --limit 100 --lead-out ./data/deel-leads.json
|
|
66
|
+
```
|
|
67
|
+
|
|
15
68
|
## Documentation
|
|
16
69
|
|
|
17
70
|
This repository now includes a Mintlify docs site for the wider Salesprompter universe, including the app contract, CLI surface, Chrome extension contract, and the main warehouse-backed workflows.
|
|
@@ -24,6 +77,8 @@ This repository now includes a Mintlify docs site for the wider Salesprompter un
|
|
|
24
77
|
- Chrome extension: `./platform/chrome-extension.mdx`
|
|
25
78
|
- Domain finder: `./workflows/domain-finder.mdx`
|
|
26
79
|
- Command reference: `./reference/cli.mdx`
|
|
80
|
+
- Environment variables: `./reference/environment-variables.mdx`
|
|
81
|
+
- Troubleshooting: `./operations/troubleshooting.mdx`
|
|
27
82
|
|
|
28
83
|
Run the docs locally with:
|
|
29
84
|
|
|
@@ -58,12 +113,12 @@ Global output flags:
|
|
|
58
113
|
The CLI stores a local session file at `~/.config/salesprompter/auth-session.json` (or `SALESPROMPTER_CONFIG_DIR`).
|
|
59
114
|
|
|
60
115
|
```bash
|
|
61
|
-
#
|
|
62
|
-
salesprompter auth:login --token "$SALESPROMPTER_TOKEN" --api-url "https://salesprompter.ai"
|
|
63
|
-
|
|
64
|
-
# Optional: use device flow only if your Salesprompter app exposes it
|
|
116
|
+
# Preferred path: browser/device login
|
|
65
117
|
salesprompter auth:login
|
|
66
118
|
|
|
119
|
+
# Fallback path: generate a short-lived CLI token in the Salesprompter app
|
|
120
|
+
salesprompter auth:login --token "$SALESPROMPTER_TOKEN" --api-url "https://salesprompter.ai"
|
|
121
|
+
|
|
67
122
|
# Verify active identity with backend
|
|
68
123
|
salesprompter auth:whoami --verify
|
|
69
124
|
|
|
@@ -82,9 +137,11 @@ Environment variables:
|
|
|
82
137
|
|
|
83
138
|
App compatibility:
|
|
84
139
|
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
140
|
+
- Salesprompter app should expose `/api/cli/auth/device/start`, `/api/cli/auth/device/poll`, and `/api/cli/auth/me`.
|
|
141
|
+
- `salesprompter auth:login` uses browser/device login and prints the verification URL plus code before polling.
|
|
142
|
+
- `POST /api/cli/auth/token` remains the fallback path when browser/device login is disabled or unavailable.
|
|
143
|
+
|
|
144
|
+
Fallback command:
|
|
88
145
|
|
|
89
146
|
```bash
|
|
90
147
|
salesprompter auth:login --token "<token-from-app>" --api-url "https://salesprompter.ai"
|
|
@@ -94,6 +151,7 @@ salesprompter auth:login --token "<token-from-app>" --api-url "https://salesprom
|
|
|
94
151
|
|
|
95
152
|
- Every command reads and writes plain JSON.
|
|
96
153
|
- Output is machine-readable and composable (`--json` for compact transport).
|
|
154
|
+
- The top-level use cases map ambiguous prompts like "determine the ICP of deel.com" into explicit command paths.
|
|
97
155
|
- Domain contracts are explicit and validated with `zod`.
|
|
98
156
|
- External integrations are behind narrow provider interfaces.
|
|
99
157
|
- Lead generation reports which provider and mode produced the result.
|
package/dist/auth.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
1
3
|
import os from "node:os";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
@@ -106,6 +108,12 @@ function buildDeviceFlowUnavailableMessage(apiBaseUrl) {
|
|
|
106
108
|
`Generate a CLI token in the app and run \`salesprompter auth:login --token <token> --api-url "${apiBaseUrl}"\`.`
|
|
107
109
|
].join(" ");
|
|
108
110
|
}
|
|
111
|
+
function isBrowserConnectUnavailableError(message) {
|
|
112
|
+
if (/request failed \((401|403|404|405|500|501|502|503|504)\)/.test(message)) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return message.includes("invalid localhost callback response");
|
|
116
|
+
}
|
|
109
117
|
async function hasSessionFile() {
|
|
110
118
|
try {
|
|
111
119
|
await access(getSessionPath());
|
|
@@ -202,6 +210,99 @@ export async function loginWithToken(token, apiBaseUrl) {
|
|
|
202
210
|
await writeAuthSession(session);
|
|
203
211
|
return session;
|
|
204
212
|
}
|
|
213
|
+
export async function loginWithBrowserConnect(options) {
|
|
214
|
+
const apiBaseUrl = normalizeApiBaseUrl(options?.apiBaseUrl);
|
|
215
|
+
const timeoutSeconds = options?.timeoutSeconds ?? DEFAULT_DEVICE_TIMEOUT_SECONDS;
|
|
216
|
+
const state = randomBytes(16).toString("hex");
|
|
217
|
+
let resolveToken;
|
|
218
|
+
let rejectToken;
|
|
219
|
+
const tokenPromise = new Promise((resolve, reject) => {
|
|
220
|
+
resolveToken = resolve;
|
|
221
|
+
rejectToken = reject;
|
|
222
|
+
});
|
|
223
|
+
const server = createServer((request, response) => {
|
|
224
|
+
try {
|
|
225
|
+
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
226
|
+
if (requestUrl.pathname !== "/callback") {
|
|
227
|
+
response.statusCode = 404;
|
|
228
|
+
response.end("Not found");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const accessToken = requestUrl.searchParams.get("access_token") ?? "";
|
|
232
|
+
const responseState = requestUrl.searchParams.get("state") ?? "";
|
|
233
|
+
if (accessToken.trim().length === 0 || responseState.trim().length === 0) {
|
|
234
|
+
response.statusCode = 400;
|
|
235
|
+
response.end("Invalid login response");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
response.statusCode = 200;
|
|
239
|
+
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
240
|
+
response.end("<!doctype html><html><body><p>Salesprompter CLI login complete. You can return to the terminal.</p></body></html>");
|
|
241
|
+
resolveToken?.({ accessToken, state: responseState });
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
response.statusCode = 500;
|
|
245
|
+
response.end("Login failed");
|
|
246
|
+
rejectToken?.(error);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
server.once("error", (error) => {
|
|
250
|
+
rejectToken?.(error);
|
|
251
|
+
});
|
|
252
|
+
await new Promise((resolve, reject) => {
|
|
253
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
254
|
+
server.once("error", reject);
|
|
255
|
+
});
|
|
256
|
+
const address = server.address();
|
|
257
|
+
if (!address || typeof address === "string") {
|
|
258
|
+
server.close();
|
|
259
|
+
throw new Error("failed to bind localhost callback server");
|
|
260
|
+
}
|
|
261
|
+
const redirectUri = `http://127.0.0.1:${address.port}/callback`;
|
|
262
|
+
const closeServer = async () => await new Promise((resolve) => {
|
|
263
|
+
server.close(() => resolve());
|
|
264
|
+
});
|
|
265
|
+
const browserUrl = new URL(`${apiBaseUrl}/api/cli/auth/connect`);
|
|
266
|
+
browserUrl.searchParams.set("redirect_uri", redirectUri);
|
|
267
|
+
browserUrl.searchParams.set("state", state);
|
|
268
|
+
let preflightResponse;
|
|
269
|
+
try {
|
|
270
|
+
preflightResponse = await fetch(browserUrl, {
|
|
271
|
+
method: "GET",
|
|
272
|
+
redirect: "manual",
|
|
273
|
+
headers: {
|
|
274
|
+
"X-Salesprompter-Client": CLIENT_HEADER
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
await closeServer();
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
if (preflightResponse.status < 300 || preflightResponse.status >= 400) {
|
|
283
|
+
await closeServer();
|
|
284
|
+
throw new Error(`request failed (${preflightResponse.status}) for ${browserUrl}`);
|
|
285
|
+
}
|
|
286
|
+
await options?.onConnectStart?.({
|
|
287
|
+
browserUrl: browserUrl.toString(),
|
|
288
|
+
redirectUri
|
|
289
|
+
});
|
|
290
|
+
const timeoutHandle = setTimeout(() => {
|
|
291
|
+
rejectToken?.(new Error("timed out waiting for browser login"));
|
|
292
|
+
}, Math.max(timeoutSeconds, 30) * 1000);
|
|
293
|
+
let result;
|
|
294
|
+
try {
|
|
295
|
+
result = await tokenPromise;
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
clearTimeout(timeoutHandle);
|
|
299
|
+
await closeServer();
|
|
300
|
+
}
|
|
301
|
+
if (result.state !== state) {
|
|
302
|
+
throw new Error("invalid localhost callback response");
|
|
303
|
+
}
|
|
304
|
+
return await loginWithToken(result.accessToken, apiBaseUrl);
|
|
305
|
+
}
|
|
205
306
|
export async function loginWithDeviceFlow(options) {
|
|
206
307
|
const apiBaseUrl = normalizeApiBaseUrl(options?.apiBaseUrl);
|
|
207
308
|
const timeoutSeconds = options?.timeoutSeconds ?? DEFAULT_DEVICE_TIMEOUT_SECONDS;
|
|
@@ -227,6 +328,12 @@ export async function loginWithDeviceFlow(options) {
|
|
|
227
328
|
if (verificationUrl === undefined) {
|
|
228
329
|
throw new Error("device start response missing verification url");
|
|
229
330
|
}
|
|
331
|
+
await options?.onDeviceStart?.({
|
|
332
|
+
verificationUrl,
|
|
333
|
+
userCode: start.userCode,
|
|
334
|
+
intervalSeconds: start.intervalSeconds,
|
|
335
|
+
expiresInSeconds: start.expiresInSeconds
|
|
336
|
+
});
|
|
230
337
|
const pollIntervalMs = (start.intervalSeconds ?? DEFAULT_DEVICE_POLL_INTERVAL_SECONDS) * 1000;
|
|
231
338
|
const deadline = Date.now() + Math.max(timeoutSeconds, 30) * 1000;
|
|
232
339
|
while (Date.now() < deadline) {
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import { Command } from "commander";
|
|
4
5
|
import { z } from "zod";
|
|
5
|
-
import { clearAuthSession, loginWithDeviceFlow, loginWithToken, requireAuthSession, shouldBypassAuth, verifySession } from "./auth.js";
|
|
6
|
+
import { clearAuthSession, loginWithBrowserConnect, loginWithDeviceFlow, loginWithToken, requireAuthSession, shouldBypassAuth, verifySession } from "./auth.js";
|
|
6
7
|
import { buildBigQueryLeadLookupSql, executeBigQuerySql, normalizeBigQueryLeadRows, runBigQueryQuery, runBigQueryRows } from "./bigquery.js";
|
|
7
8
|
import { AccountProfileSchema, EnrichedLeadSchema, IcpSchema, LeadSchema, ScoredLeadSchema, SyncTargetSchema } from "./domain.js";
|
|
8
9
|
import { auditDomainDecisions, buildDomainfinderBacklogQueries, buildDomainfinderCandidatesSql, buildDomainfinderInputSql, buildDomainfinderWritebackSql, buildExistingDomainRepairSql, buildExistingDomainAuditQueries, compareDomainSelectionStrategies, selectBestDomains } from "./domainfinder.js";
|
|
@@ -35,6 +36,59 @@ function applyGlobalOutputOptions(actionCommand) {
|
|
|
35
36
|
runtimeOutputOptions.json = Boolean(globalOptions.json);
|
|
36
37
|
runtimeOutputOptions.quiet = Boolean(globalOptions.quiet);
|
|
37
38
|
}
|
|
39
|
+
function openUrlInBrowser(url) {
|
|
40
|
+
if (!process.stderr.isTTY) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const launch = (command, args) => {
|
|
44
|
+
try {
|
|
45
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
46
|
+
child.on("error", () => undefined);
|
|
47
|
+
child.unref();
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
if (process.platform === "darwin") {
|
|
56
|
+
return launch("open", [url]);
|
|
57
|
+
}
|
|
58
|
+
if (process.platform === "win32") {
|
|
59
|
+
return launch("cmd", ["/c", "start", "", url]);
|
|
60
|
+
}
|
|
61
|
+
return launch("xdg-open", [url]);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function writeDeviceLoginInstructions(info) {
|
|
68
|
+
if (runtimeOutputOptions.json || runtimeOutputOptions.quiet) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
process.stderr.write("Starting device login flow. Complete login in the browser.\n");
|
|
72
|
+
process.stderr.write(`Open this URL: ${info.verificationUrl}\n`);
|
|
73
|
+
process.stderr.write(`Enter this code if prompted: ${info.userCode}\n`);
|
|
74
|
+
if (info.expiresInSeconds !== undefined) {
|
|
75
|
+
const expiresInMinutes = Math.max(1, Math.ceil(info.expiresInSeconds / 60));
|
|
76
|
+
process.stderr.write(`Code expires in about ${expiresInMinutes} minute${expiresInMinutes === 1 ? "" : "s"}.\n`);
|
|
77
|
+
}
|
|
78
|
+
if (openUrlInBrowser(info.verificationUrl)) {
|
|
79
|
+
process.stderr.write("Opened the browser for you.\n");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function writeBrowserLoginInstructions(info) {
|
|
83
|
+
if (runtimeOutputOptions.json || runtimeOutputOptions.quiet) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
process.stderr.write("Starting browser login flow. Complete login in the browser.\n");
|
|
87
|
+
process.stderr.write(`Open this URL: ${info.browserUrl}\n`);
|
|
88
|
+
if (openUrlInBrowser(info.browserUrl)) {
|
|
89
|
+
process.stderr.write("Opened the browser for you.\n");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
38
92
|
function buildCliError(error) {
|
|
39
93
|
if (error instanceof z.ZodError) {
|
|
40
94
|
return {
|
|
@@ -193,12 +247,35 @@ program
|
|
|
193
247
|
return;
|
|
194
248
|
}
|
|
195
249
|
const startedAt = new Date().toISOString();
|
|
196
|
-
|
|
197
|
-
|
|
250
|
+
try {
|
|
251
|
+
const session = await loginWithBrowserConnect({
|
|
252
|
+
apiBaseUrl: options.apiUrl,
|
|
253
|
+
timeoutSeconds,
|
|
254
|
+
onConnectStart: writeBrowserLoginInstructions
|
|
255
|
+
});
|
|
256
|
+
printOutput({
|
|
257
|
+
status: "ok",
|
|
258
|
+
method: "browser",
|
|
259
|
+
startedAt,
|
|
260
|
+
apiBaseUrl: session.apiBaseUrl,
|
|
261
|
+
user: session.user,
|
|
262
|
+
expiresAt: session.expiresAt ?? null
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
268
|
+
if (!message.includes("timed out waiting for browser login") && !message.includes("invalid localhost callback response") && !message.includes("request failed")) {
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
if (!/request failed \((401|403|404|405|500|501|502|503|504)\)/.test(message)) {
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
198
274
|
}
|
|
199
275
|
const result = await loginWithDeviceFlow({
|
|
200
276
|
apiBaseUrl: options.apiUrl,
|
|
201
|
-
timeoutSeconds
|
|
277
|
+
timeoutSeconds,
|
|
278
|
+
onDeviceStart: writeDeviceLoginInstructions
|
|
202
279
|
});
|
|
203
280
|
printOutput({
|
|
204
281
|
status: "ok",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "salesprompter-cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI for
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "JSON-first sales prospecting CLI for ICP definition, lead generation, enrichment, scoring, and CRM/outreach sync.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"salesprompter": "dist/cli.js"
|
|
@@ -13,19 +13,39 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsc -p tsconfig.json",
|
|
15
15
|
"check": "tsc --noEmit -p tsconfig.json",
|
|
16
|
-
"docs:dev": "
|
|
17
|
-
"docs:broken-links": "
|
|
18
|
-
"docs:a11y": "
|
|
16
|
+
"docs:dev": "mint dev",
|
|
17
|
+
"docs:broken-links": "mint broken-links",
|
|
18
|
+
"docs:a11y": "mint a11y",
|
|
19
19
|
"start": "node ./dist/cli.js",
|
|
20
20
|
"test": "npm run build && tsc -p tsconfig.test.json && node --test dist-tests/tests/**/*.test.js"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
23
23
|
"sales",
|
|
24
|
+
"salesprompter",
|
|
24
25
|
"cli",
|
|
26
|
+
"icp",
|
|
27
|
+
"ideal-customer-profile",
|
|
28
|
+
"prospecting",
|
|
25
29
|
"lead-generation",
|
|
30
|
+
"lead-enrichment",
|
|
31
|
+
"lead-scoring",
|
|
32
|
+
"sales-intelligence",
|
|
26
33
|
"crm",
|
|
27
|
-
"outreach"
|
|
34
|
+
"outreach",
|
|
35
|
+
"instantly",
|
|
36
|
+
"hubspot",
|
|
37
|
+
"llm",
|
|
38
|
+
"ai-agent",
|
|
39
|
+
"codex"
|
|
28
40
|
],
|
|
41
|
+
"homepage": "https://salesprompter.ai/docs",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/danielsinewe/salesprompter-cli.git"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/danielsinewe/salesprompter-cli/issues"
|
|
48
|
+
},
|
|
29
49
|
"license": "MIT",
|
|
30
50
|
"dependencies": {
|
|
31
51
|
"commander": "^14.0.1",
|
|
@@ -33,6 +53,7 @@
|
|
|
33
53
|
},
|
|
34
54
|
"devDependencies": {
|
|
35
55
|
"@types/node": "^24.3.0",
|
|
56
|
+
"mint": "^4.2.420",
|
|
36
57
|
"typescript": "^5.9.2"
|
|
37
58
|
}
|
|
38
59
|
}
|