@open-mercato/web-research-tavily 0.7.1-develop.7194.1.ab4fc81f82

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.
@@ -0,0 +1,2 @@
1
+ [build:web-research-tavily] found 2 entry points
2
+ [build:web-research-tavily] built successfully
package/build.mjs ADDED
@@ -0,0 +1,10 @@
1
+ import { dirname } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { buildPackage } from '../../scripts/build-package.mjs'
4
+
5
+ const packageDir = dirname(fileURLToPath(import.meta.url))
6
+
7
+ await buildPackage(packageDir, {
8
+ name: 'web-research-tavily',
9
+ entryPoints: 'src/**/*.ts',
10
+ })
@@ -0,0 +1,126 @@
1
+ import {
2
+ CONTRACT_VERSION,
3
+ READY,
4
+ defineAdapterModule,
5
+ notReady,
6
+ searchOk,
7
+ stripTrailingSlashes,
8
+ toOutcomeFailure
9
+ } from "@open-mercato/web-research";
10
+ import { z } from "zod";
11
+ const tavilyOptionsSchema = z.object({
12
+ apiKey: z.string().min(1).optional(),
13
+ baseUrl: z.url().optional(),
14
+ searchDepth: z.enum(["basic", "advanced"]).optional(),
15
+ includeContent: z.boolean().optional(),
16
+ timeoutMs: z.number().int().min(1e3).max(12e4).optional()
17
+ });
18
+ const DEFAULT_BASE_URL = "https://api.tavily.com";
19
+ const DEFAULT_TIMEOUT_MS = 15e3;
20
+ const responseSchema = z.object({
21
+ answer: z.string().nullish(),
22
+ results: z.array(
23
+ z.object({
24
+ url: z.string(),
25
+ title: z.string().nullish(),
26
+ content: z.string().nullish(),
27
+ raw_content: z.string().nullish(),
28
+ score: z.number().nullish(),
29
+ published_date: z.string().nullish()
30
+ })
31
+ ).nullish()
32
+ });
33
+ function parseJson(body) {
34
+ try {
35
+ return JSON.parse(body);
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ function createTavilyAdapter(options) {
41
+ const apiKey = options.apiKey?.trim() ?? "";
42
+ const baseUrl = stripTrailingSlashes(options.baseUrl ?? DEFAULT_BASE_URL);
43
+ const searchDepth = options.searchDepth ?? "basic";
44
+ const includeContent = options.includeContent ?? false;
45
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
46
+ return {
47
+ id: "tavily",
48
+ kind: "api",
49
+ capabilities: {
50
+ search: true,
51
+ fetch: false,
52
+ snippets: true,
53
+ content: includeContent,
54
+ freshness: false,
55
+ siteFilter: true,
56
+ cost: "metered"
57
+ },
58
+ /** `billable` — issues a real /search query. */
59
+ probeCost: "billable",
60
+ readiness: () => apiKey.length > 0 ? READY : notReady("Tavily API key is not set"),
61
+ async search(request, context) {
62
+ if (apiKey.length === 0) return { status: "unavailable", reason: "Tavily API key is not set" };
63
+ try {
64
+ const response = await context.http.request(`${baseUrl}/search`, {
65
+ method: "POST",
66
+ headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
67
+ body: JSON.stringify({
68
+ query: request.site ? `${request.query} site:${request.site}` : request.query,
69
+ max_results: request.limit,
70
+ search_depth: searchDepth,
71
+ include_answer: true,
72
+ include_raw_content: includeContent
73
+ }),
74
+ accept: ["application/json"],
75
+ timeoutMs,
76
+ signal: context.signal
77
+ });
78
+ const parsed = responseSchema.safeParse(parseJson(response.body));
79
+ if (!parsed.success) {
80
+ return { status: "error", reason: "Tavily returned an unrecognized response shape", retriable: false };
81
+ }
82
+ const results = (parsed.data.results ?? []).map((item) => ({
83
+ url: item.url,
84
+ title: item.title ?? null,
85
+ snippet: item.content ?? null,
86
+ content: item.raw_content ?? null,
87
+ publishedAt: item.published_date ?? null,
88
+ score: item.score ?? null
89
+ }));
90
+ context.report("ok", `Tavily returned ${results.length} result(s)`, { resultCount: results.length });
91
+ return searchOk(results, parsed.data.answer ?? void 0);
92
+ } catch (error) {
93
+ return toOutcomeFailure(error);
94
+ }
95
+ },
96
+ async healthCheck(context) {
97
+ if (apiKey.length === 0) return { ok: false, detail: "Tavily API key is not set" };
98
+ try {
99
+ await context.http.request(`${baseUrl}/search`, {
100
+ method: "POST",
101
+ headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
102
+ body: JSON.stringify({ query: "open mercato health check", max_results: 1 }),
103
+ accept: ["application/json"],
104
+ timeoutMs,
105
+ signal: context.signal
106
+ });
107
+ return { ok: true };
108
+ } catch (error) {
109
+ return { ok: false, detail: error instanceof Error ? error.message : String(error) };
110
+ }
111
+ }
112
+ };
113
+ }
114
+ const tavilyAdapterModule = defineAdapterModule({
115
+ id: "tavily",
116
+ kind: "api",
117
+ contractVersion: CONTRACT_VERSION,
118
+ optionsSchema: tavilyOptionsSchema,
119
+ createAdapter: createTavilyAdapter
120
+ });
121
+ export {
122
+ createTavilyAdapter,
123
+ tavilyAdapterModule,
124
+ tavilyOptionsSchema
125
+ };
126
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/adapter.ts"],
4
+ "sourcesContent": ["import {\n CONTRACT_VERSION,\n READY,\n defineAdapterModule,\n notReady,\n searchOk,\n stripTrailingSlashes,\n toOutcomeFailure,\n type RawResult,\n type SearchAdapter,\n type SearchOutcome,\n} from '@open-mercato/web-research'\nimport { z } from 'zod'\n\nexport const tavilyOptionsSchema = z.object({\n apiKey: z.string().min(1).optional(),\n baseUrl: z.url().optional(),\n searchDepth: z.enum(['basic', 'advanced']).optional(),\n includeContent: z.boolean().optional(),\n timeoutMs: z.number().int().min(1000).max(120_000).optional(),\n})\n\nexport type TavilyOptions = z.infer<typeof tavilyOptionsSchema>\n\nconst DEFAULT_BASE_URL = 'https://api.tavily.com'\nconst DEFAULT_TIMEOUT_MS = 15_000\n\nconst responseSchema = z.object({\n answer: z.string().nullish(),\n results: z\n .array(\n z.object({\n url: z.string(),\n title: z.string().nullish(),\n content: z.string().nullish(),\n raw_content: z.string().nullish(),\n score: z.number().nullish(),\n published_date: z.string().nullish(),\n }),\n )\n .nullish(),\n})\n\nfunction parseJson(body: string): unknown {\n try {\n return JSON.parse(body)\n } catch {\n return null\n }\n}\n\nexport function createTavilyAdapter(options: TavilyOptions): SearchAdapter {\n const apiKey = options.apiKey?.trim() ?? ''\n const baseUrl = stripTrailingSlashes(options.baseUrl ?? DEFAULT_BASE_URL)\n const searchDepth = options.searchDepth ?? 'basic'\n const includeContent = options.includeContent ?? false\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS\n\n return {\n id: 'tavily',\n kind: 'api',\n capabilities: {\n search: true,\n fetch: false,\n snippets: true,\n content: includeContent,\n freshness: false,\n siteFilter: true,\n cost: 'metered',\n },\n /** `billable` \u2014 issues a real /search query. */\n probeCost: 'billable' as const,\n readiness: () => (apiKey.length > 0 ? READY : notReady('Tavily API key is not set')),\n\n async search(request, context): Promise<SearchOutcome> {\n if (apiKey.length === 0) return { status: 'unavailable', reason: 'Tavily API key is not set' }\n try {\n const response = await context.http.request(`${baseUrl}/search`, {\n method: 'POST',\n headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },\n body: JSON.stringify({\n query: request.site ? `${request.query} site:${request.site}` : request.query,\n max_results: request.limit,\n search_depth: searchDepth,\n include_answer: true,\n include_raw_content: includeContent,\n }),\n accept: ['application/json'],\n timeoutMs,\n signal: context.signal,\n })\n const parsed = responseSchema.safeParse(parseJson(response.body))\n if (!parsed.success) {\n return { status: 'error', reason: 'Tavily returned an unrecognized response shape', retriable: false }\n }\n const results: RawResult[] = (parsed.data.results ?? []).map((item) => ({\n url: item.url,\n title: item.title ?? null,\n snippet: item.content ?? null,\n content: item.raw_content ?? null,\n publishedAt: item.published_date ?? null,\n score: item.score ?? null,\n }))\n context.report('ok', `Tavily returned ${results.length} result(s)`, { resultCount: results.length })\n return searchOk(results, parsed.data.answer ?? undefined)\n } catch (error) {\n return toOutcomeFailure(error)\n }\n },\n\n async healthCheck(context) {\n if (apiKey.length === 0) return { ok: false, detail: 'Tavily API key is not set' }\n try {\n await context.http.request(`${baseUrl}/search`, {\n method: 'POST',\n headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },\n body: JSON.stringify({ query: 'open mercato health check', max_results: 1 }),\n accept: ['application/json'],\n timeoutMs,\n signal: context.signal,\n })\n return { ok: true }\n } catch (error) {\n return { ok: false, detail: error instanceof Error ? error.message : String(error) }\n }\n },\n }\n}\n\nexport const tavilyAdapterModule = defineAdapterModule({\n id: 'tavily',\n kind: 'api',\n contractVersion: CONTRACT_VERSION,\n optionsSchema: tavilyOptionsSchema,\n createAdapter: createTavilyAdapter,\n})\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import { createTavilyAdapter, tavilyAdapterModule, tavilyOptionsSchema } from "./adapter.js";
2
+ import { tavilyAdapterModule as tavilyAdapterModule2 } from "./adapter.js";
3
+ export {
4
+ createTavilyAdapter,
5
+ tavilyAdapterModule2 as default,
6
+ tavilyAdapterModule,
7
+ tavilyOptionsSchema
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": ["export { createTavilyAdapter, tavilyAdapterModule, tavilyOptionsSchema, type TavilyOptions } from './adapter'\n\nexport { tavilyAdapterModule as default } from './adapter'\n"],
5
+ "mappings": "AAAA,SAAS,qBAAqB,qBAAqB,2BAA+C;AAElG,SAAgC,uBAAvBA,4BAAsC;",
6
+ "names": ["tavilyAdapterModule"]
7
+ }
@@ -0,0 +1,28 @@
1
+ /** @type {import('jest').Config} */
2
+ const base = require('../../jest.config.base.cjs')
3
+
4
+ module.exports = {
5
+ ...base,
6
+ preset: 'ts-jest',
7
+ testEnvironment: 'node',
8
+ watchman: false,
9
+ rootDir: '.',
10
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
11
+ moduleNameMapper: {
12
+ '^@open-mercato/web-research/(.*)$': '<rootDir>/../web-research/src/$1',
13
+ '^@open-mercato/web-research$': '<rootDir>/../web-research/src/index.ts',
14
+ },
15
+ transform: {
16
+ '^.+\\.(t|j)sx?$': [
17
+ 'ts-jest',
18
+ {
19
+ tsconfig: {
20
+ rootDir: '.',
21
+ ignoreDeprecations: '6.0',
22
+ },
23
+ },
24
+ ],
25
+ },
26
+ testMatch: ['<rootDir>/src/**/__tests__/**/*.test.(ts|tsx)'],
27
+ passWithNoTests: true,
28
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@open-mercato/web-research-tavily",
3
+ "version": "0.7.1-develop.7194.1.ab4fc81f82",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "openMercato": {
8
+ "webResearchAdapter": {
9
+ "id": "tavily"
10
+ }
11
+ },
12
+ "scripts": {
13
+ "build": "node build.mjs",
14
+ "watch": "node watch.mjs",
15
+ "test": "jest --config jest.config.cjs",
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "types": "./src/index.ts",
21
+ "default": "./dist/index.js"
22
+ },
23
+ "./*.ts": {
24
+ "types": "./src/*.ts",
25
+ "default": "./dist/*.js"
26
+ },
27
+ "./*": {
28
+ "types": [
29
+ "./src/*.ts"
30
+ ],
31
+ "default": "./dist/*.js"
32
+ }
33
+ },
34
+ "peerDependencies": {
35
+ "@open-mercato/web-research": "^0.6.0"
36
+ },
37
+ "dependencies": {
38
+ "zod": "^4.4.3"
39
+ },
40
+ "devDependencies": {
41
+ "@open-mercato/web-research": "0.7.1-develop.7194.1.ab4fc81f82",
42
+ "@types/jest": "^30.0.0",
43
+ "@types/node": "^26.0.1",
44
+ "esbuild": "^0.28.1",
45
+ "glob": "^13.0.6",
46
+ "jest": "^30.4.2",
47
+ "ts-jest": "^29.4.11",
48
+ "typescript": "^6.0.3"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "https://github.com/open-mercato/open-mercato",
56
+ "directory": "packages/web-research-tavily"
57
+ },
58
+ "stableVersion": "0.7.0"
59
+ }
@@ -0,0 +1,73 @@
1
+ import { createStubHttpClient, createTestContext, describeAdapterContract } from '@open-mercato/web-research/testing'
2
+ import { createTavilyAdapter, tavilyAdapterModule } from '../adapter'
3
+
4
+ const BODY = JSON.stringify({
5
+ answer: 'Open Mercato is a modular commerce platform.',
6
+ results: [
7
+ { url: 'https://a.com/1', title: 'Alpha', content: 'Alpha snippet', score: 0.9 },
8
+ { url: 'https://b.com/2', title: 'Beta', content: 'Beta snippet', score: 0.5 },
9
+ ],
10
+ })
11
+
12
+ describe('tavily adapter', () => {
13
+ it('is unavailable without a key and makes no call', async () => {
14
+ const http = createStubHttpClient(() => ({ body: BODY }))
15
+ const outcome = await createTavilyAdapter({}).search({ query: 'x', limit: 5 }, createTestContext({ http }))
16
+ expect(outcome).toMatchObject({ status: 'unavailable' })
17
+ expect(http.calls).toHaveLength(0)
18
+ })
19
+
20
+ it('maps results and keeps the vendor answer', async () => {
21
+ const http = createStubHttpClient(() => ({ body: BODY }))
22
+ const outcome = await createTavilyAdapter({ apiKey: 'k' }).search(
23
+ { query: 'x', limit: 5 },
24
+ createTestContext({ http }),
25
+ )
26
+ expect(outcome.status).toBe('ok')
27
+ if (outcome.status !== 'ok') return
28
+ expect(outcome.results[0]).toMatchObject({ url: 'https://a.com/1', snippet: 'Alpha snippet', score: 0.9 })
29
+ expect(outcome.answer).toContain('modular commerce platform')
30
+ })
31
+
32
+ it('sends the key as a bearer token and honours the site filter', async () => {
33
+ const seen: Array<{ headers?: Record<string, string>; body?: string }> = []
34
+ const http = createStubHttpClient((_url, init) => {
35
+ seen.push({ headers: init?.headers as Record<string, string>, body: init?.body })
36
+ return { body: BODY }
37
+ })
38
+ await createTavilyAdapter({ apiKey: 'secret' }).search(
39
+ { query: 'pricing', limit: 5, site: 'example.com' },
40
+ createTestContext({ http }),
41
+ )
42
+ expect(seen[0].headers?.authorization).toBe('Bearer secret')
43
+ expect(JSON.parse(seen[0].body ?? '{}').query).toBe('pricing site:example.com')
44
+ })
45
+
46
+ it('reports an unrecognized payload as an error', async () => {
47
+ const http = createStubHttpClient(() => ({ body: '{"results":"nope"}' }))
48
+ const outcome = await createTavilyAdapter({ apiKey: 'k' }).search(
49
+ { query: 'x', limit: 5 },
50
+ createTestContext({ http }),
51
+ )
52
+ expect(outcome).toMatchObject({ status: 'error' })
53
+ })
54
+
55
+ it('returns empty when the vendor found nothing and offered no answer', async () => {
56
+ const http = createStubHttpClient(() => ({ body: JSON.stringify({ results: [] }) }))
57
+ const outcome = await createTavilyAdapter({ apiKey: 'k' }).search(
58
+ { query: 'x', limit: 5 },
59
+ createTestContext({ http }),
60
+ )
61
+ expect(outcome.status).toBe('empty')
62
+ })
63
+ })
64
+
65
+ describeAdapterContract(
66
+ tavilyAdapterModule,
67
+ {
68
+ unconfiguredOptions: {},
69
+ configuredOptions: { apiKey: 'test-key' },
70
+ context: () => createTestContext({ http: createStubHttpClient(() => ({ body: BODY })) }),
71
+ },
72
+ { describe, it, expect: expect as never },
73
+ )
package/src/adapter.ts ADDED
@@ -0,0 +1,136 @@
1
+ import {
2
+ CONTRACT_VERSION,
3
+ READY,
4
+ defineAdapterModule,
5
+ notReady,
6
+ searchOk,
7
+ stripTrailingSlashes,
8
+ toOutcomeFailure,
9
+ type RawResult,
10
+ type SearchAdapter,
11
+ type SearchOutcome,
12
+ } from '@open-mercato/web-research'
13
+ import { z } from 'zod'
14
+
15
+ export const tavilyOptionsSchema = z.object({
16
+ apiKey: z.string().min(1).optional(),
17
+ baseUrl: z.url().optional(),
18
+ searchDepth: z.enum(['basic', 'advanced']).optional(),
19
+ includeContent: z.boolean().optional(),
20
+ timeoutMs: z.number().int().min(1000).max(120_000).optional(),
21
+ })
22
+
23
+ export type TavilyOptions = z.infer<typeof tavilyOptionsSchema>
24
+
25
+ const DEFAULT_BASE_URL = 'https://api.tavily.com'
26
+ const DEFAULT_TIMEOUT_MS = 15_000
27
+
28
+ const responseSchema = z.object({
29
+ answer: z.string().nullish(),
30
+ results: z
31
+ .array(
32
+ z.object({
33
+ url: z.string(),
34
+ title: z.string().nullish(),
35
+ content: z.string().nullish(),
36
+ raw_content: z.string().nullish(),
37
+ score: z.number().nullish(),
38
+ published_date: z.string().nullish(),
39
+ }),
40
+ )
41
+ .nullish(),
42
+ })
43
+
44
+ function parseJson(body: string): unknown {
45
+ try {
46
+ return JSON.parse(body)
47
+ } catch {
48
+ return null
49
+ }
50
+ }
51
+
52
+ export function createTavilyAdapter(options: TavilyOptions): SearchAdapter {
53
+ const apiKey = options.apiKey?.trim() ?? ''
54
+ const baseUrl = stripTrailingSlashes(options.baseUrl ?? DEFAULT_BASE_URL)
55
+ const searchDepth = options.searchDepth ?? 'basic'
56
+ const includeContent = options.includeContent ?? false
57
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
58
+
59
+ return {
60
+ id: 'tavily',
61
+ kind: 'api',
62
+ capabilities: {
63
+ search: true,
64
+ fetch: false,
65
+ snippets: true,
66
+ content: includeContent,
67
+ freshness: false,
68
+ siteFilter: true,
69
+ cost: 'metered',
70
+ },
71
+ /** `billable` — issues a real /search query. */
72
+ probeCost: 'billable' as const,
73
+ readiness: () => (apiKey.length > 0 ? READY : notReady('Tavily API key is not set')),
74
+
75
+ async search(request, context): Promise<SearchOutcome> {
76
+ if (apiKey.length === 0) return { status: 'unavailable', reason: 'Tavily API key is not set' }
77
+ try {
78
+ const response = await context.http.request(`${baseUrl}/search`, {
79
+ method: 'POST',
80
+ headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
81
+ body: JSON.stringify({
82
+ query: request.site ? `${request.query} site:${request.site}` : request.query,
83
+ max_results: request.limit,
84
+ search_depth: searchDepth,
85
+ include_answer: true,
86
+ include_raw_content: includeContent,
87
+ }),
88
+ accept: ['application/json'],
89
+ timeoutMs,
90
+ signal: context.signal,
91
+ })
92
+ const parsed = responseSchema.safeParse(parseJson(response.body))
93
+ if (!parsed.success) {
94
+ return { status: 'error', reason: 'Tavily returned an unrecognized response shape', retriable: false }
95
+ }
96
+ const results: RawResult[] = (parsed.data.results ?? []).map((item) => ({
97
+ url: item.url,
98
+ title: item.title ?? null,
99
+ snippet: item.content ?? null,
100
+ content: item.raw_content ?? null,
101
+ publishedAt: item.published_date ?? null,
102
+ score: item.score ?? null,
103
+ }))
104
+ context.report('ok', `Tavily returned ${results.length} result(s)`, { resultCount: results.length })
105
+ return searchOk(results, parsed.data.answer ?? undefined)
106
+ } catch (error) {
107
+ return toOutcomeFailure(error)
108
+ }
109
+ },
110
+
111
+ async healthCheck(context) {
112
+ if (apiKey.length === 0) return { ok: false, detail: 'Tavily API key is not set' }
113
+ try {
114
+ await context.http.request(`${baseUrl}/search`, {
115
+ method: 'POST',
116
+ headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
117
+ body: JSON.stringify({ query: 'open mercato health check', max_results: 1 }),
118
+ accept: ['application/json'],
119
+ timeoutMs,
120
+ signal: context.signal,
121
+ })
122
+ return { ok: true }
123
+ } catch (error) {
124
+ return { ok: false, detail: error instanceof Error ? error.message : String(error) }
125
+ }
126
+ },
127
+ }
128
+ }
129
+
130
+ export const tavilyAdapterModule = defineAdapterModule({
131
+ id: 'tavily',
132
+ kind: 'api',
133
+ contractVersion: CONTRACT_VERSION,
134
+ optionsSchema: tavilyOptionsSchema,
135
+ createAdapter: createTavilyAdapter,
136
+ })
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { createTavilyAdapter, tavilyAdapterModule, tavilyOptionsSchema, type TavilyOptions } from './adapter'
2
+
3
+ export { tavilyAdapterModule as default } from './adapter'
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "../../tsconfig.base.json",
4
+ "compilerOptions": {
5
+ "noEmit": true,
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["node_modules", "dist", "**/__tests__/**"]
10
+ }
package/watch.mjs ADDED
@@ -0,0 +1,7 @@
1
+ import { dirname } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { watch } from '../../scripts/watch.mjs'
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url))
6
+
7
+ watch(__dirname)