@stackfactor/agent-utils 1.8.0 → 1.9.0

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
@@ -419,6 +419,21 @@ A model that cannot tell it received half a regulation will reason over the half
419
419
  - Per-URL failures are surfaced in the tool result instead of being dropped, so a fetch failure is not mistaken for an absence of content.
420
420
  - Tool calls honour the caller's abort signal, so a cancelled run stops paying for in-flight crawls.
421
421
 
422
+ ### Checking that cited links resolve
423
+
424
+ `checkUrlsReachable(urls, config, usageTracker?)` returns the URLs, exactly as passed, that Tavily could fetch. Use it in place of requesting a link yourself. Cited links come from web pages anyone can publish, so a link, its redirects or its DNS can point inside StackFactor's network; a request this process sends would go there through StackFactor's own egress addresses. Tavily fetches from its own infrastructure.
425
+
426
+ ```js
427
+ const { checkUrlsReachable } = require("@stackfactor/agent-utils");
428
+
429
+ const live = await checkUrlsReachable(references.map((r) => r.url), config, usageTracker);
430
+ const kept = references.filter((r) => !r.url || live.has(r.url));
431
+ ```
432
+
433
+ - Only `http:` and `https:` URLs are sent, 20 per call (Tavily's limit), at basic depth: 1 credit per 5 URLs fetched, failed ones free. Credits are billed like any Tavily call; the URLs are not added to `webSearchSources`.
434
+ - A URL Tavily lists under `failed_results` is unreachable. A batch whose call fails, or a config without `tavilyAPIKey`, confirms none of its URLs. It never throws.
435
+ - No `config.tavily` or `agentic` is needed: it calls Tavily directly, not through the model.
436
+
422
437
  ---
423
438
 
424
439
  ## Supported Models
@@ -7,11 +7,13 @@ import { callAgent, checkAgent } from "./client.js";
7
7
  import * as runtimeContext from "./runtimeContext.js";
8
8
  import { recordUsage, USAGE_UNITS } from "./usage.js";
9
9
  import stackfactorKnowledge from "./stackfactorKnowledge.js";
10
+ import { checkUrlsReachable } from "./tavily.js";
10
11
  export { constants };
11
12
  export { errorHandling, AppError };
12
13
  export type { ParsedError } from "./errorHandling.js";
13
14
  export { langChain };
14
15
  export type { UsageTracker } from "./langChain.js";
16
+ export { checkUrlsReachable };
15
17
  export type { TavilyConfig, TavilyToolName } from "./tavily.js";
16
18
  export { logger };
17
19
  export { serve, runOnce, isRunOnceExecution, EXECUTION_ENV };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,oBAAoB,MAAM,2BAA2B,CAAC;AAE7D,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAEnC,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,CAAC;AAE7D,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC;AAEpC,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExD,OAAO,EAAE,oBAAoB,EAAE,CAAC;AAEhC,YAAY,EACV,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,eAAe,EACf,aAAa,EACb,oBAAoB,GACrB,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,oBAAoB,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAEnC,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAE9B,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,CAAC;AAE7D,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC;AAEpC,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExD,OAAO,EAAE,oBAAoB,EAAE,CAAC;AAEhC,YAAY,EACV,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,eAAe,EACf,aAAa,EACb,oBAAoB,GACrB,MAAM,2BAA2B,CAAC"}
package/dist/cjs/index.js CHANGED
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.stackfactorKnowledge = exports.USAGE_UNITS = exports.recordUsage = exports.runtimeContext = exports.checkAgent = exports.callAgent = exports.EXECUTION_ENV = exports.isRunOnceExecution = exports.runOnce = exports.serve = exports.logger = exports.langChain = exports.AppError = exports.errorHandling = exports.constants = void 0;
39
+ exports.stackfactorKnowledge = exports.USAGE_UNITS = exports.recordUsage = exports.runtimeContext = exports.checkAgent = exports.callAgent = exports.EXECUTION_ENV = exports.isRunOnceExecution = exports.runOnce = exports.serve = exports.logger = exports.checkUrlsReachable = exports.langChain = exports.AppError = exports.errorHandling = exports.constants = void 0;
40
40
  const const_js_1 = __importDefault(require("./const.js"));
41
41
  exports.constants = const_js_1.default;
42
42
  const errorHandling_js_1 = __importStar(require("./errorHandling.js"));
@@ -61,3 +61,5 @@ Object.defineProperty(exports, "recordUsage", { enumerable: true, get: function
61
61
  Object.defineProperty(exports, "USAGE_UNITS", { enumerable: true, get: function () { return usage_js_1.USAGE_UNITS; } });
62
62
  const stackfactorKnowledge_js_1 = __importDefault(require("./stackfactorKnowledge.js"));
63
63
  exports.stackfactorKnowledge = stackfactorKnowledge_js_1.default;
64
+ const tavily_js_1 = require("./tavily.js");
65
+ Object.defineProperty(exports, "checkUrlsReachable", { enumerable: true, get: function () { return tavily_js_1.checkUrlsReachable; } });
@@ -82,9 +82,40 @@ export declare const assertTavilyUsable: (config: any, options: TavilyConfig) =>
82
82
  * @returns LangChain tool instances to append to the agent's tool list
83
83
  */
84
84
  export declare const buildTavilyTools: (config: any, options: TavilyConfig, usageTracker?: UsageTracker | null) => any[];
85
+ /**
86
+ * Which of these URLs resolve, judged by Tavily fetching them rather than by
87
+ * this process connecting to them.
88
+ *
89
+ * An agent that confirms a cited link by requesting it itself sends that
90
+ * request from inside StackFactor's network, through its fixed egress
91
+ * addresses, to wherever the link, a redirect or its DNS points — and cited
92
+ * links come from web pages anyone can publish, so that is a request to an
93
+ * address an outsider chose (server-side request forgery). Tavily fetches from
94
+ * its own infrastructure, so nothing here ever connects to such a host.
95
+ *
96
+ * Only `http:` and `https:` URLs are sent; any other is unreachable by
97
+ * definition. They go in batches of 20, Tavily's limit, at basic depth: 1
98
+ * credit per 5 URLs Tavily fetched, failed ones free. The credits are billed
99
+ * like every other Tavily call, but the URLs are not added to the tracker's
100
+ * sources: a link worth checking was found there in the first place.
101
+ *
102
+ * Tavily accounts for every URL it is given, listing the ones it could not
103
+ * fetch under `failed_results`. So a URL listed there is unreachable, and every
104
+ * other URL of a batch Tavily answered is reachable (the results may name a
105
+ * URL by its address after a redirect, which is why they are not matched). A
106
+ * batch whose call fails outright, or a config without `tavilyAPIKey`,
107
+ * confirms none of its URLs: a link that could not be checked is not trusted.
108
+ *
109
+ * @param urls - The URLs to check; a URL given twice is checked once
110
+ * @param config - The integration config; supplies `tavilyAPIKey`
111
+ * @param usageTracker - Optional accumulator the credits are counted into
112
+ * @returns The given URLs, exactly as passed, that resolve
113
+ */
114
+ export declare const checkUrlsReachable: (urls: readonly string[], config: any, usageTracker?: UsageTracker | null) => Promise<Set<string>>;
85
115
  declare const _default: {
86
116
  assertTavilyUsable: (config: any, options: TavilyConfig) => void;
87
117
  buildTavilyTools: (config: any, options: TavilyConfig, usageTracker?: UsageTracker | null) => any[];
118
+ checkUrlsReachable: (urls: readonly string[], config: any, usageTracker?: UsageTracker | null) => Promise<Set<string>>;
88
119
  getTavilyOptions: (config: any) => TavilyConfig | null;
89
120
  recordTavilyUsage: (tracker: UsageTracker | null | undefined, _config: any, credits: number, sources: {
90
121
  url: string;
@@ -1 +1 @@
1
- {"version":3,"file":"tavily.d.ts","sourceRoot":"","sources":["../../src/tavily.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA0BnD,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,CAAC;AAEpE;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,YAAY,CAAC;IAC3D,kFAAkF;IAClF,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;CAC9B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,GAAG,KAAG,YAAY,GAAG,IAI7D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,SAAS,YAAY,KAAG,IAsBvE,CAAC;AAsLF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,GAAG,EACX,SAAS,YAAY,EACrB,eAAc,YAAY,GAAG,IAAW,KACvC,GAAG,EA2PL,CAAC;;iCA5dyC,GAAG,WAAW,YAAY,KAAG,IAAI;+BA8NlE,GAAG,WACF,YAAY,iBACP,YAAY,GAAG,IAAI,KAChC,GAAG,EAAE;+BAhPiC,GAAG,KAAG,YAAY,GAAG,IAAI;iCAsIvD,YAAY,GAAG,IAAI,GAAG,SAAS,WAC/B,GAAG,WACH,MAAM,WACN;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,KACzC,IAAI;;AAmWP,wBAKE"}
1
+ {"version":3,"file":"tavily.d.ts","sourceRoot":"","sources":["../../src/tavily.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA0BnD,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,CAAC;AAEpE;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,YAAY,CAAC;IAC3D,kFAAkF;IAClF,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;CAC9B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,GAAG,KAAG,YAAY,GAAG,IAI7D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,SAAS,YAAY,KAAG,IAsBvE,CAAC;AAsLF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,GAAG,EACX,SAAS,YAAY,EACrB,eAAc,YAAY,GAAG,IAAW,KACvC,GAAG,EA2PL,CAAC;AAqBF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,kBAAkB,GAC7B,MAAM,SAAS,MAAM,EAAE,EACvB,QAAQ,GAAG,EACX,eAAc,YAAY,GAAG,IAAW,KACvC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CA6DrB,CAAC;;iCA/kByC,GAAG,WAAW,YAAY,KAAG,IAAI;+BA8NlE,GAAG,WACF,YAAY,iBACP,YAAY,GAAG,IAAI,KAChC,GAAG,EAAE;+BA8SA,SAAS,MAAM,EAAE,UACf,GAAG,iBACG,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;+BAjiBkB,GAAG,KAAG,YAAY,GAAG,IAAI;iCAsIvD,YAAY,GAAG,IAAI,GAAG,SAAS,WAC/B,GAAG,WACH,MAAM,WACN;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,KACzC,IAAI;;AAsdP,wBAME"}
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.buildTavilyTools = exports.assertTavilyUsable = exports.getTavilyOptions = void 0;
6
+ exports.checkUrlsReachable = exports.buildTavilyTools = exports.assertTavilyUsable = exports.getTavilyOptions = void 0;
7
7
  const langchain_1 = require("langchain");
8
8
  const zod_1 = require("zod");
9
9
  const const_js_1 = __importDefault(require("./const.js"));
@@ -395,9 +395,96 @@ const buildTavilyTools = (config, options, usageTracker = null) => {
395
395
  return tools;
396
396
  };
397
397
  exports.buildTavilyTools = buildTavilyTools;
398
+ /** Tavily's own limit on URLs per `/extract` request. */
399
+ const EXTRACT_MAX_URLS = 20;
400
+ /** Seconds. A reachability check needs the page fetched, not rendered at length. */
401
+ const REACHABILITY_TIMEOUT_SECONDS = 20;
402
+ /** How a URL is matched against Tavily's answer: trimmed, lowercased, no trailing slash. */
403
+ const urlMatchKey = (url) => url.trim().replace(/\/+$/, "").toLowerCase();
404
+ /** True for an `http:` or `https:` URL; false for anything else, unparsable included. */
405
+ const isWebUrl = (url) => {
406
+ try {
407
+ const { protocol } = new URL(url);
408
+ return protocol === "http:" || protocol === "https:";
409
+ }
410
+ catch {
411
+ return false;
412
+ }
413
+ };
414
+ /**
415
+ * Which of these URLs resolve, judged by Tavily fetching them rather than by
416
+ * this process connecting to them.
417
+ *
418
+ * An agent that confirms a cited link by requesting it itself sends that
419
+ * request from inside StackFactor's network, through its fixed egress
420
+ * addresses, to wherever the link, a redirect or its DNS points — and cited
421
+ * links come from web pages anyone can publish, so that is a request to an
422
+ * address an outsider chose (server-side request forgery). Tavily fetches from
423
+ * its own infrastructure, so nothing here ever connects to such a host.
424
+ *
425
+ * Only `http:` and `https:` URLs are sent; any other is unreachable by
426
+ * definition. They go in batches of 20, Tavily's limit, at basic depth: 1
427
+ * credit per 5 URLs Tavily fetched, failed ones free. The credits are billed
428
+ * like every other Tavily call, but the URLs are not added to the tracker's
429
+ * sources: a link worth checking was found there in the first place.
430
+ *
431
+ * Tavily accounts for every URL it is given, listing the ones it could not
432
+ * fetch under `failed_results`. So a URL listed there is unreachable, and every
433
+ * other URL of a batch Tavily answered is reachable (the results may name a
434
+ * URL by its address after a redirect, which is why they are not matched). A
435
+ * batch whose call fails outright, or a config without `tavilyAPIKey`,
436
+ * confirms none of its URLs: a link that could not be checked is not trusted.
437
+ *
438
+ * @param urls - The URLs to check; a URL given twice is checked once
439
+ * @param config - The integration config; supplies `tavilyAPIKey`
440
+ * @param usageTracker - Optional accumulator the credits are counted into
441
+ * @returns The given URLs, exactly as passed, that resolve
442
+ */
443
+ const checkUrlsReachable = async (urls, config, usageTracker = null) => {
444
+ const reachable = new Set();
445
+ const candidates = [
446
+ ...new Set(urls.filter((url) => typeof url === "string" && isWebUrl(url.trim()))),
447
+ ];
448
+ if (candidates.length === 0)
449
+ return reachable;
450
+ const apiKey = config?.tavilyAPIKey;
451
+ if (!apiKey) {
452
+ logger_js_1.default.log(null, logger_js_1.default.levels.warn, `Tavily reachability: no tavilyAPIKey, so none of ${candidates.length} URL(s) is confirmed`);
453
+ return reachable;
454
+ }
455
+ const batches = [];
456
+ for (let start = 0; start < candidates.length; start += EXTRACT_MAX_URLS) {
457
+ batches.push(candidates.slice(start, start + EXTRACT_MAX_URLS));
458
+ }
459
+ await Promise.all(batches.map(async (batch) => {
460
+ let data;
461
+ try {
462
+ data = await callTavily("extract", {
463
+ urls: batch.map((url) => url.trim()),
464
+ extract_depth: "basic",
465
+ format: "text",
466
+ }, apiKey, REACHABILITY_TIMEOUT_SECONDS);
467
+ }
468
+ catch (err) {
469
+ logger_js_1.default.log(null, logger_js_1.default.levels.warn, `Tavily reachability: the check of ${batch.length} URL(s) failed (${err?.message || String(err)}), so none of them is confirmed`);
470
+ return;
471
+ }
472
+ recordTavilyUsage(usageTracker, config, data?.usage?.credits || 0, []);
473
+ const failed = new Set((data?.failed_results || [])
474
+ .map((entry) => typeof entry?.url === "string" ? urlMatchKey(entry.url) : "")
475
+ .filter(Boolean));
476
+ for (const url of batch) {
477
+ if (!failed.has(urlMatchKey(url)))
478
+ reachable.add(url);
479
+ }
480
+ }));
481
+ return reachable;
482
+ };
483
+ exports.checkUrlsReachable = checkUrlsReachable;
398
484
  exports.default = {
399
485
  assertTavilyUsable: exports.assertTavilyUsable,
400
486
  buildTavilyTools: exports.buildTavilyTools,
487
+ checkUrlsReachable: exports.checkUrlsReachable,
401
488
  getTavilyOptions: exports.getTavilyOptions,
402
489
  recordTavilyUsage,
403
490
  };
@@ -7,11 +7,13 @@ import { callAgent, checkAgent } from "./client.js";
7
7
  import * as runtimeContext from "./runtimeContext.js";
8
8
  import { recordUsage, USAGE_UNITS } from "./usage.js";
9
9
  import stackfactorKnowledge from "./stackfactorKnowledge.js";
10
+ import { checkUrlsReachable } from "./tavily.js";
10
11
  export { constants };
11
12
  export { errorHandling, AppError };
12
13
  export type { ParsedError } from "./errorHandling.js";
13
14
  export { langChain };
14
15
  export type { UsageTracker } from "./langChain.js";
16
+ export { checkUrlsReachable };
15
17
  export type { TavilyConfig, TavilyToolName } from "./tavily.js";
16
18
  export { logger };
17
19
  export { serve, runOnce, isRunOnceExecution, EXECUTION_ENV };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,oBAAoB,MAAM,2BAA2B,CAAC;AAE7D,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAEnC,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,CAAC;AAE7D,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC;AAEpC,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExD,OAAO,EAAE,oBAAoB,EAAE,CAAC;AAEhC,YAAY,EACV,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,eAAe,EACf,aAAa,EACb,oBAAoB,GACrB,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,oBAAoB,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;AAEnC,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAE9B,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,CAAC;AAE7D,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC;AAEpC,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExD,OAAO,EAAE,oBAAoB,EAAE,CAAC;AAEhC,YAAY,EACV,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,eAAe,EACf,aAAa,EACb,oBAAoB,GACrB,MAAM,2BAA2B,CAAC"}
package/dist/esm/index.js CHANGED
@@ -7,9 +7,11 @@ import { callAgent, checkAgent } from "./client.js";
7
7
  import * as runtimeContext from "./runtimeContext.js";
8
8
  import { recordUsage, USAGE_UNITS } from "./usage.js";
9
9
  import stackfactorKnowledge from "./stackfactorKnowledge.js";
10
+ import { checkUrlsReachable } from "./tavily.js";
10
11
  export { constants };
11
12
  export { errorHandling, AppError };
12
13
  export { langChain };
14
+ export { checkUrlsReachable };
13
15
  export { logger };
14
16
  export { serve, runOnce, isRunOnceExecution, EXECUTION_ENV };
15
17
  export { callAgent };
@@ -82,9 +82,40 @@ export declare const assertTavilyUsable: (config: any, options: TavilyConfig) =>
82
82
  * @returns LangChain tool instances to append to the agent's tool list
83
83
  */
84
84
  export declare const buildTavilyTools: (config: any, options: TavilyConfig, usageTracker?: UsageTracker | null) => any[];
85
+ /**
86
+ * Which of these URLs resolve, judged by Tavily fetching them rather than by
87
+ * this process connecting to them.
88
+ *
89
+ * An agent that confirms a cited link by requesting it itself sends that
90
+ * request from inside StackFactor's network, through its fixed egress
91
+ * addresses, to wherever the link, a redirect or its DNS points — and cited
92
+ * links come from web pages anyone can publish, so that is a request to an
93
+ * address an outsider chose (server-side request forgery). Tavily fetches from
94
+ * its own infrastructure, so nothing here ever connects to such a host.
95
+ *
96
+ * Only `http:` and `https:` URLs are sent; any other is unreachable by
97
+ * definition. They go in batches of 20, Tavily's limit, at basic depth: 1
98
+ * credit per 5 URLs Tavily fetched, failed ones free. The credits are billed
99
+ * like every other Tavily call, but the URLs are not added to the tracker's
100
+ * sources: a link worth checking was found there in the first place.
101
+ *
102
+ * Tavily accounts for every URL it is given, listing the ones it could not
103
+ * fetch under `failed_results`. So a URL listed there is unreachable, and every
104
+ * other URL of a batch Tavily answered is reachable (the results may name a
105
+ * URL by its address after a redirect, which is why they are not matched). A
106
+ * batch whose call fails outright, or a config without `tavilyAPIKey`,
107
+ * confirms none of its URLs: a link that could not be checked is not trusted.
108
+ *
109
+ * @param urls - The URLs to check; a URL given twice is checked once
110
+ * @param config - The integration config; supplies `tavilyAPIKey`
111
+ * @param usageTracker - Optional accumulator the credits are counted into
112
+ * @returns The given URLs, exactly as passed, that resolve
113
+ */
114
+ export declare const checkUrlsReachable: (urls: readonly string[], config: any, usageTracker?: UsageTracker | null) => Promise<Set<string>>;
85
115
  declare const _default: {
86
116
  assertTavilyUsable: (config: any, options: TavilyConfig) => void;
87
117
  buildTavilyTools: (config: any, options: TavilyConfig, usageTracker?: UsageTracker | null) => any[];
118
+ checkUrlsReachable: (urls: readonly string[], config: any, usageTracker?: UsageTracker | null) => Promise<Set<string>>;
88
119
  getTavilyOptions: (config: any) => TavilyConfig | null;
89
120
  recordTavilyUsage: (tracker: UsageTracker | null | undefined, _config: any, credits: number, sources: {
90
121
  url: string;
@@ -1 +1 @@
1
- {"version":3,"file":"tavily.d.ts","sourceRoot":"","sources":["../../src/tavily.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA0BnD,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,CAAC;AAEpE;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,YAAY,CAAC;IAC3D,kFAAkF;IAClF,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;CAC9B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,GAAG,KAAG,YAAY,GAAG,IAI7D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,SAAS,YAAY,KAAG,IAsBvE,CAAC;AAsLF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,GAAG,EACX,SAAS,YAAY,EACrB,eAAc,YAAY,GAAG,IAAW,KACvC,GAAG,EA2PL,CAAC;;iCA5dyC,GAAG,WAAW,YAAY,KAAG,IAAI;+BA8NlE,GAAG,WACF,YAAY,iBACP,YAAY,GAAG,IAAI,KAChC,GAAG,EAAE;+BAhPiC,GAAG,KAAG,YAAY,GAAG,IAAI;iCAsIvD,YAAY,GAAG,IAAI,GAAG,SAAS,WAC/B,GAAG,WACH,MAAM,WACN;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,KACzC,IAAI;;AAmWP,wBAKE"}
1
+ {"version":3,"file":"tavily.d.ts","sourceRoot":"","sources":["../../src/tavily.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA0BnD,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,CAAC;AAEpE;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,YAAY,CAAC;IAC3D,kFAAkF;IAClF,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;CAC9B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,GAAG,KAAG,YAAY,GAAG,IAI7D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,SAAS,YAAY,KAAG,IAsBvE,CAAC;AAsLF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,GAAG,EACX,SAAS,YAAY,EACrB,eAAc,YAAY,GAAG,IAAW,KACvC,GAAG,EA2PL,CAAC;AAqBF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,kBAAkB,GAC7B,MAAM,SAAS,MAAM,EAAE,EACvB,QAAQ,GAAG,EACX,eAAc,YAAY,GAAG,IAAW,KACvC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CA6DrB,CAAC;;iCA/kByC,GAAG,WAAW,YAAY,KAAG,IAAI;+BA8NlE,GAAG,WACF,YAAY,iBACP,YAAY,GAAG,IAAI,KAChC,GAAG,EAAE;+BA8SA,SAAS,MAAM,EAAE,UACf,GAAG,iBACG,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;+BAjiBkB,GAAG,KAAG,YAAY,GAAG,IAAI;iCAsIvD,YAAY,GAAG,IAAI,GAAG,SAAS,WAC/B,GAAG,WACH,MAAM,WACN;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,KACzC,IAAI;;AAsdP,wBAME"}
@@ -386,9 +386,95 @@ export const buildTavilyTools = (config, options, usageTracker = null) => {
386
386
  logger.log(null, logger.levels.info, `Tavily tools enabled: ${tools.map((entry) => entry.name).join(", ")}`);
387
387
  return tools;
388
388
  };
389
+ /** Tavily's own limit on URLs per `/extract` request. */
390
+ const EXTRACT_MAX_URLS = 20;
391
+ /** Seconds. A reachability check needs the page fetched, not rendered at length. */
392
+ const REACHABILITY_TIMEOUT_SECONDS = 20;
393
+ /** How a URL is matched against Tavily's answer: trimmed, lowercased, no trailing slash. */
394
+ const urlMatchKey = (url) => url.trim().replace(/\/+$/, "").toLowerCase();
395
+ /** True for an `http:` or `https:` URL; false for anything else, unparsable included. */
396
+ const isWebUrl = (url) => {
397
+ try {
398
+ const { protocol } = new URL(url);
399
+ return protocol === "http:" || protocol === "https:";
400
+ }
401
+ catch {
402
+ return false;
403
+ }
404
+ };
405
+ /**
406
+ * Which of these URLs resolve, judged by Tavily fetching them rather than by
407
+ * this process connecting to them.
408
+ *
409
+ * An agent that confirms a cited link by requesting it itself sends that
410
+ * request from inside StackFactor's network, through its fixed egress
411
+ * addresses, to wherever the link, a redirect or its DNS points — and cited
412
+ * links come from web pages anyone can publish, so that is a request to an
413
+ * address an outsider chose (server-side request forgery). Tavily fetches from
414
+ * its own infrastructure, so nothing here ever connects to such a host.
415
+ *
416
+ * Only `http:` and `https:` URLs are sent; any other is unreachable by
417
+ * definition. They go in batches of 20, Tavily's limit, at basic depth: 1
418
+ * credit per 5 URLs Tavily fetched, failed ones free. The credits are billed
419
+ * like every other Tavily call, but the URLs are not added to the tracker's
420
+ * sources: a link worth checking was found there in the first place.
421
+ *
422
+ * Tavily accounts for every URL it is given, listing the ones it could not
423
+ * fetch under `failed_results`. So a URL listed there is unreachable, and every
424
+ * other URL of a batch Tavily answered is reachable (the results may name a
425
+ * URL by its address after a redirect, which is why they are not matched). A
426
+ * batch whose call fails outright, or a config without `tavilyAPIKey`,
427
+ * confirms none of its URLs: a link that could not be checked is not trusted.
428
+ *
429
+ * @param urls - The URLs to check; a URL given twice is checked once
430
+ * @param config - The integration config; supplies `tavilyAPIKey`
431
+ * @param usageTracker - Optional accumulator the credits are counted into
432
+ * @returns The given URLs, exactly as passed, that resolve
433
+ */
434
+ export const checkUrlsReachable = async (urls, config, usageTracker = null) => {
435
+ const reachable = new Set();
436
+ const candidates = [
437
+ ...new Set(urls.filter((url) => typeof url === "string" && isWebUrl(url.trim()))),
438
+ ];
439
+ if (candidates.length === 0)
440
+ return reachable;
441
+ const apiKey = config?.tavilyAPIKey;
442
+ if (!apiKey) {
443
+ logger.log(null, logger.levels.warn, `Tavily reachability: no tavilyAPIKey, so none of ${candidates.length} URL(s) is confirmed`);
444
+ return reachable;
445
+ }
446
+ const batches = [];
447
+ for (let start = 0; start < candidates.length; start += EXTRACT_MAX_URLS) {
448
+ batches.push(candidates.slice(start, start + EXTRACT_MAX_URLS));
449
+ }
450
+ await Promise.all(batches.map(async (batch) => {
451
+ let data;
452
+ try {
453
+ data = await callTavily("extract", {
454
+ urls: batch.map((url) => url.trim()),
455
+ extract_depth: "basic",
456
+ format: "text",
457
+ }, apiKey, REACHABILITY_TIMEOUT_SECONDS);
458
+ }
459
+ catch (err) {
460
+ logger.log(null, logger.levels.warn, `Tavily reachability: the check of ${batch.length} URL(s) failed (${err?.message || String(err)}), so none of them is confirmed`);
461
+ return;
462
+ }
463
+ recordTavilyUsage(usageTracker, config, data?.usage?.credits || 0, []);
464
+ const failed = new Set((data?.failed_results || [])
465
+ .map((entry) => typeof entry?.url === "string" ? urlMatchKey(entry.url) : "")
466
+ .filter(Boolean));
467
+ for (const url of batch) {
468
+ if (!failed.has(urlMatchKey(url)))
469
+ reachable.add(url);
470
+ }
471
+ }));
472
+ return reachable;
473
+ };
389
474
  export default {
390
475
  assertTavilyUsable,
391
476
  buildTavilyTools,
477
+ checkUrlsReachable,
392
478
  getTavilyOptions,
393
479
  recordTavilyUsage,
394
480
  };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.8.0",
6
+ "version": "1.9.0",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",
@@ -20,7 +20,7 @@
20
20
  "work:release": "bash scripts/release.sh",
21
21
  "release": "bash scripts/release.sh",
22
22
  "release:fanout": "bash scripts/release-fanout.sh",
23
- "ar-auth": "npm config set @stackfactor:registry https://us-central1-npm.pkg.dev/virtual-development-team/sf-devenv-npm-private/; TOKEN=$(gcloud auth print-access-token 2>/dev/null || curl -sfH 'Metadata-Flavor: Google' http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token | sed -n 's/.*\"access_token\":\"\\([^\"]*\\)\".*/\\1/p'); test -n \"$TOKEN\" || { echo 'ar-auth: no credential \u2014 run: gcloud auth login' >&2; exit 1; }; npm config set //us-central1-npm.pkg.dev/virtual-development-team/sf-devenv-npm-private/:_authToken \"$TOKEN\"; npm config set //us-central1-npm.pkg.dev/virtual-development-team/:_authToken \"$TOKEN\"; echo 'ar-auth: Artifact Registry scope + token configured'",
23
+ "ar-auth": "npm config set @stackfactor:registry https://us-central1-npm.pkg.dev/virtual-development-team/sf-devenv-npm-private/; TOKEN=$(gcloud auth print-access-token 2>/dev/null || curl -sfH 'Metadata-Flavor: Google' http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token | sed -n 's/.*\"access_token\":\"\\([^\"]*\\)\".*/\\1/p'); test -n \"$TOKEN\" || { echo 'ar-auth: no credential — run: gcloud auth login' >&2; exit 1; }; npm config set //us-central1-npm.pkg.dev/virtual-development-team/sf-devenv-npm-private/:_authToken \"$TOKEN\"; npm config set //us-central1-npm.pkg.dev/virtual-development-team/:_authToken \"$TOKEN\"; echo 'ar-auth: Artifact Registry scope + token configured'",
24
24
  "reinstall": "npm run ar-auth && npm ci",
25
25
  "test:coverage": "npm run build && node --experimental-test-coverage --test --test-reporter=tap test/*.test.js"
26
26
  },