pi-bifrost 0.1.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.
@@ -0,0 +1,157 @@
1
+ import { describe, it, before } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { spawn } from "node:child_process";
4
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join, dirname } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const EXTENSION_PATH = join(__dirname, "..", "index.ts");
11
+
12
+ const PI_ARGS = [
13
+ "-e",
14
+ EXTENSION_PATH,
15
+ "--approve",
16
+ "--no-session",
17
+ "--print",
18
+ "-p",
19
+ ];
20
+
21
+ async function runPi(command, cwd = process.cwd()) {
22
+ return new Promise((resolve, reject) => {
23
+ const child = spawn("pi", [...PI_ARGS, command], {
24
+ stdio: ["ignore", "pipe", "pipe"],
25
+ env: process.env,
26
+ cwd,
27
+ });
28
+
29
+ let stdout = "";
30
+ let stderr = "";
31
+ child.stdout.setEncoding("utf8");
32
+ child.stderr.setEncoding("utf8");
33
+ child.stdout.on("data", (chunk) => {
34
+ stdout += chunk;
35
+ });
36
+ child.stderr.on("data", (chunk) => {
37
+ stderr += chunk;
38
+ });
39
+
40
+ const timer = setTimeout(() => {
41
+ child.kill("SIGTERM");
42
+ const error = new Error(`pi timed out: ${command}`);
43
+ error.stdout = stdout;
44
+ error.stderr = stderr;
45
+ reject(error);
46
+ }, 120_000);
47
+
48
+ child.on("error", (err) => {
49
+ clearTimeout(timer);
50
+ reject(err);
51
+ });
52
+
53
+ child.on("close", (code) => {
54
+ clearTimeout(timer);
55
+ if (code !== 0) {
56
+ const error = new Error(`pi exited ${code}: ${command}`);
57
+ error.stdout = stdout;
58
+ error.stderr = stderr;
59
+ reject(error);
60
+ } else {
61
+ resolve({ stdout, stderr });
62
+ }
63
+ });
64
+ });
65
+ }
66
+
67
+ function combined(output) {
68
+ return `${output.stderr}\n${output.stdout}`;
69
+ }
70
+
71
+ describe("bifrost integration", { timeout: 300_000, concurrency: 1 }, () => {
72
+ before(async () => {
73
+ await runPi("/bifrost cache clear");
74
+ });
75
+
76
+ it("reports classifier status", async () => {
77
+ const out = combined(await runPi("/bifrost classifier status"));
78
+ assert.ok(out.includes("enabled=true"));
79
+ assert.ok(out.includes("opencode/mimo-v2.5-free"));
80
+ assert.ok(out.includes("endpoint=registry"));
81
+ });
82
+
83
+ it("classifies hello as economical", async () => {
84
+ const out = combined(await runPi("/bifrost preview hello"));
85
+ assert.ok(out.includes("source: classifier"));
86
+ assert.ok(out.includes("category: economical"));
87
+ });
88
+
89
+ it("classifies architecture prompt as frontier", async () => {
90
+ const out = combined(await runPi("/bifrost preview plan the architecture"));
91
+ assert.ok(out.includes("source: classifier"));
92
+ assert.ok(out.includes("category: frontier"));
93
+ });
94
+
95
+ it("caches classifications", async () => {
96
+ await runPi("/bifrost cache clear");
97
+ let out = combined(await runPi("/bifrost cache stats"));
98
+ assert.ok(out.includes("cache: 0 entries"));
99
+
100
+ // Run a live prompt. Routing to the local economical model may fail,
101
+ // but the classifier result is still cached before routing.
102
+ try {
103
+ await runPi("format this file");
104
+ } catch {
105
+ // acceptable; routing failure is not the concern of this test
106
+ }
107
+
108
+ out = combined(await runPi("/bifrost cache stats"));
109
+ assert.ok(out.includes("cache: 1 entries"));
110
+
111
+ out = combined(await runPi("/bifrost preview format this file"));
112
+ assert.ok(out.includes("source: cache"));
113
+ });
114
+
115
+ it("falls back to regex when classifier is disabled in config", async () => {
116
+ const tempDir = mkdtempSync(join(tmpdir(), "bifrost-test-"));
117
+ mkdirSync(join(tempDir, ".pi"), { recursive: true });
118
+ writeFileSync(
119
+ join(tempDir, ".pi", "bifrost.json"),
120
+ JSON.stringify({ classifier: { enabled: false } }),
121
+ );
122
+
123
+ try {
124
+ const out = combined(
125
+ await runPi("/bifrost preview lint this file", tempDir),
126
+ );
127
+ assert.ok(out.includes("source: regex"));
128
+ } finally {
129
+ rmSync(tempDir, { recursive: true, force: true });
130
+ }
131
+ });
132
+
133
+ it("classifies without endpoint when model is in pi registry", async () => {
134
+ const tempDir = mkdtempSync(join(tmpdir(), "bifrost-test-"));
135
+ mkdirSync(join(tempDir, ".pi"), { recursive: true });
136
+ writeFileSync(
137
+ join(tempDir, ".pi", "bifrost.json"),
138
+ JSON.stringify({
139
+ classifier: { model: "lmstudio/qwen/qwen3-vl-8b" },
140
+ models: {
141
+ economical: "lmstudio/qwen/qwen3-vl-8b",
142
+ frontier: "openai-codex/gpt-5.4",
143
+ },
144
+ }),
145
+ );
146
+
147
+ try {
148
+ const out = combined(
149
+ await runPi("/bifrost preview hello", tempDir),
150
+ );
151
+ assert.ok(out.includes("source: classifier"));
152
+ assert.ok(out.includes("category: economical"));
153
+ } finally {
154
+ rmSync(tempDir, { recursive: true, force: true });
155
+ }
156
+ });
157
+ });
@@ -0,0 +1,269 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import type { ClassifierModel } from "../classifier.ts";
4
+ import { createPipeline, type PipelineDeps } from "../classification-pipeline.ts";
5
+
6
+ function makeModel(provider: string, id: string): ClassifierModel {
7
+ return {
8
+ kind: "registry",
9
+ model: {
10
+ provider,
11
+ id,
12
+ name: id,
13
+ api: "openai-completions",
14
+ baseUrl: "http://localhost:1234/v1",
15
+ reasoning: false,
16
+ input: ["text"],
17
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
18
+ contextWindow: 128000,
19
+ maxTokens: 4096,
20
+ },
21
+ } as ClassifierModel;
22
+ }
23
+
24
+ function deps(overrides: Partial<PipelineDeps> = {}): PipelineDeps {
25
+ return {
26
+ cacheLookup: () => undefined,
27
+ classifierModels: [],
28
+ classifyWithLLM: async () => undefined,
29
+ regexRules: [],
30
+ defaultTier: undefined,
31
+ tiers: ["frontier", "economical"],
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ describe("classification-pipeline", () => {
37
+ describe("unclassified", () => {
38
+ it("returns unclassified when no tiers configured", async () => {
39
+ const p = createPipeline(deps({ tiers: [] }));
40
+ const r = await p.classify("hello");
41
+ assert.equal(r.kind, "unclassified");
42
+ });
43
+
44
+ it("returns unclassified when nothing matches and no default", async () => {
45
+ const p = createPipeline(deps({ defaultTier: undefined }));
46
+ const r = await p.classify("hello");
47
+ assert.equal(r.kind, "unclassified");
48
+ });
49
+ });
50
+
51
+ describe("cache", () => {
52
+ it("returns classified from cache hit", async () => {
53
+ const p = createPipeline(
54
+ deps({ cacheLookup: () => "economical" }),
55
+ );
56
+ const r = await p.classify("hello");
57
+ assert.equal(r.kind, "classified");
58
+ if (r.kind === "classified") {
59
+ assert.equal(r.tier, "economical");
60
+ assert.equal(r.source, "cache");
61
+ }
62
+ });
63
+
64
+ it("skips cache when result is not a known tier", async () => {
65
+ const p = createPipeline(
66
+ deps({ cacheLookup: () => "unknown" }),
67
+ );
68
+ const r = await p.classify("hello");
69
+ // Falls through to default
70
+ assert.notEqual(r.kind, "classified");
71
+ });
72
+ });
73
+
74
+ describe("classifier", () => {
75
+ it("uses first successful classifier model", async () => {
76
+ let calls = 0;
77
+ const p = createPipeline(
78
+ deps({
79
+ classifierModels: [makeModel("a", "m1"), makeModel("b", "m2")],
80
+ classifyWithLLM: async (model) => {
81
+ calls++;
82
+ if (model.kind === "registry" && model.model.id === "m1") return "frontier";
83
+ return undefined;
84
+ },
85
+ }),
86
+ );
87
+ const r = await p.classify("debug this");
88
+ assert.equal(calls, 1); // second model never tried
89
+ assert.equal(r.kind, "classified");
90
+ if (r.kind === "classified") {
91
+ assert.equal(r.tier, "frontier");
92
+ assert.equal(r.source, "classifier");
93
+ }
94
+ });
95
+
96
+ it("tries second model when first fails", async () => {
97
+ let calls: string[] = [];
98
+ const p = createPipeline(
99
+ deps({
100
+ classifierModels: [makeModel("a", "m1"), makeModel("b", "m2")],
101
+ classifyWithLLM: async (model) => {
102
+ calls.push(model.kind === "registry" ? model.model.id : model.id);
103
+ if (model.kind === "registry" && model.model.id === "m2") return "economical";
104
+ return undefined;
105
+ },
106
+ }),
107
+ );
108
+ const r = await p.classify("hello");
109
+ assert.deepEqual(calls, ["m1", "m2"]);
110
+ assert.equal(r.kind, "classified");
111
+ if (r.kind === "classified") {
112
+ assert.equal(r.source, "classifier");
113
+ }
114
+ });
115
+
116
+ it("validates classifier result against known tiers", async () => {
117
+ const p = createPipeline(
118
+ deps({
119
+ classifierModels: [makeModel("a", "m1")],
120
+ classifyWithLLM: async () => "unknown",
121
+ defaultTier: "economical",
122
+ }),
123
+ );
124
+ const r = await p.classify("hello");
125
+ // Unknown tier → falls through to default
126
+ assert.equal(r.kind, "fallback");
127
+ if (r.kind === "fallback") {
128
+ assert.equal(r.tier, "economical");
129
+ }
130
+ });
131
+
132
+ it("skips classifier when classifierModels is empty", async () => {
133
+ let called = false;
134
+ const p = createPipeline(
135
+ deps({
136
+ classifierModels: [],
137
+ classifyWithLLM: async () => { called = true; return "frontier"; },
138
+ regexRules: [{ pattern: "hello", model: "frontier" }],
139
+ }),
140
+ );
141
+ await p.classify("hello");
142
+ assert.equal(called, false);
143
+ });
144
+ });
145
+
146
+ describe("regex", () => {
147
+ it("matches regex rule", async () => {
148
+ const p = createPipeline(
149
+ deps({
150
+ regexRules: [{ pattern: "\\bdebug\\b", model: "frontier" }],
151
+ }),
152
+ );
153
+ const r = await p.classify("debug the thing");
154
+ assert.equal(r.kind, "classified");
155
+ if (r.kind === "classified") {
156
+ assert.equal(r.tier, "frontier");
157
+ assert.equal(r.source, "regex");
158
+ }
159
+ });
160
+
161
+ it("falls through to default when no rule matches", async () => {
162
+ const p = createPipeline(
163
+ deps({
164
+ regexRules: [{ pattern: "\\bdebug\\b", model: "frontier" }],
165
+ defaultTier: "economical",
166
+ }),
167
+ );
168
+ const r = await p.classify("hello world");
169
+ assert.equal(r.kind, "fallback");
170
+ if (r.kind === "fallback") {
171
+ assert.equal(r.tier, "economical");
172
+ }
173
+ });
174
+ });
175
+
176
+ describe("priority order", () => {
177
+ it("cache beats classifier", async () => {
178
+ let classifierCalled = false;
179
+ const p = createPipeline(
180
+ deps({
181
+ cacheLookup: () => "frontier",
182
+ classifierModels: [makeModel("a", "m1")],
183
+ classifyWithLLM: async () => { classifierCalled = true; return "economical"; },
184
+ }),
185
+ );
186
+ const r = await p.classify("test");
187
+ assert.equal(classifierCalled, false);
188
+ assert.equal(r.kind, "classified");
189
+ if (r.kind === "classified") {
190
+ assert.equal(r.source, "cache");
191
+ }
192
+ });
193
+
194
+ it("classifier beats regex", async () => {
195
+ const p = createPipeline(
196
+ deps({
197
+ classifierModels: [makeModel("a", "m1")],
198
+ classifyWithLLM: async () => "economical",
199
+ regexRules: [{ pattern: ".*", model: "frontier" }],
200
+ }),
201
+ );
202
+ const r = await p.classify("test");
203
+ assert.equal(r.kind, "classified");
204
+ if (r.kind === "classified") {
205
+ assert.equal(r.source, "classifier");
206
+ assert.equal(r.tier, "economical");
207
+ }
208
+ });
209
+
210
+ it("regex beats default", async () => {
211
+ const p = createPipeline(
212
+ deps({
213
+ regexRules: [{ pattern: ".*", model: "frontier" }],
214
+ defaultTier: "economical",
215
+ }),
216
+ );
217
+ const r = await p.classify("test");
218
+ assert.equal(r.kind, "classified");
219
+ if (r.kind === "classified") {
220
+ assert.equal(r.source, "regex");
221
+ }
222
+ });
223
+ });
224
+
225
+ describe("fallback", () => {
226
+ it("returns fallback when only default matches", async () => {
227
+ const p = createPipeline(
228
+ deps({ defaultTier: "economical" }),
229
+ );
230
+ const r = await p.classify("hello");
231
+ assert.equal(r.kind, "fallback");
232
+ if (r.kind === "fallback") {
233
+ assert.equal(r.tier, "economical");
234
+ }
235
+ });
236
+ });
237
+
238
+ describe("classifier error resilience", () => {
239
+ it("catches classifier throw and falls through to regex", async () => {
240
+ const p = createPipeline(
241
+ deps({
242
+ classifierModels: [makeModel("a", "m1")],
243
+ classifyWithLLM: async () => { throw new Error("boom"); },
244
+ regexRules: [{ pattern: ".*", model: "frontier" }],
245
+ }),
246
+ );
247
+ const r = await p.classify("hello");
248
+ assert.equal(r.kind, "classified");
249
+ if (r.kind === "classified") {
250
+ assert.equal(r.source, "regex");
251
+ }
252
+ });
253
+
254
+ it("catches classifier throw and falls through to default", async () => {
255
+ const p = createPipeline(
256
+ deps({
257
+ classifierModels: [makeModel("a", "m1")],
258
+ classifyWithLLM: async () => { throw new Error("boom"); },
259
+ defaultTier: "economical",
260
+ }),
261
+ );
262
+ const r = await p.classify("hello");
263
+ assert.equal(r.kind, "fallback");
264
+ if (r.kind === "fallback") {
265
+ assert.equal(r.tier, "economical");
266
+ }
267
+ });
268
+ });
269
+ });
@@ -0,0 +1,224 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import type { Api, Model } from "@earendil-works/pi-ai";
4
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+ import {
6
+ findOneModel,
7
+ findCandidates,
8
+ selectModel,
9
+ resolveModel,
10
+ modelKey,
11
+ modelCost,
12
+ getStrategy,
13
+ classify,
14
+ } from "../routing.ts";
15
+
16
+ function makeModel(
17
+ provider: string,
18
+ id: string,
19
+ inputCost = 0,
20
+ outputCost = 0,
21
+ contextWindow = 128000,
22
+ ): Model<Api> {
23
+ return {
24
+ provider,
25
+ id,
26
+ name: id,
27
+ api: "openai-completions" as Api,
28
+ baseUrl: "http://localhost:1234/v1",
29
+ reasoning: false,
30
+ input: ["text"],
31
+ cost: { input: inputCost, output: outputCost, cacheRead: 0, cacheWrite: 0 },
32
+ contextWindow,
33
+ maxTokens: 4096,
34
+ } as unknown as Model<Api>;
35
+ }
36
+
37
+ function makeRegistry(models: Model<Api>[]) {
38
+ return {
39
+ find: (provider: string, id: string) =>
40
+ models.find((m) => m.provider === provider && m.id === id),
41
+ getAvailable: () => models,
42
+ };
43
+ }
44
+
45
+ function makeCtx(models: Model<Api>[]): ExtensionContext {
46
+ return {
47
+ modelRegistry: makeRegistry(models),
48
+ } as unknown as ExtensionContext;
49
+ }
50
+
51
+ describe("routing", () => {
52
+ describe("modelKey", () => {
53
+ it("returns provider/id", () => {
54
+ const m = makeModel("anthropic", "claude-opus", 15);
55
+ assert.equal(modelKey(m), "anthropic/claude-opus");
56
+ });
57
+
58
+ it("returns none for undefined", () => {
59
+ assert.equal(modelKey(undefined), "none");
60
+ });
61
+ });
62
+
63
+ describe("modelCost", () => {
64
+ it("sums input and output cost", () => {
65
+ const m = makeModel("x", "y", 3);
66
+ m.cost.output = 7;
67
+ assert.equal(modelCost(m), 10);
68
+ });
69
+ });
70
+
71
+ describe("findOneModel", () => {
72
+ it("finds exact provider/id", () => {
73
+ const ctx = makeCtx([makeModel("anthropic", "claude-opus", 15)]);
74
+ const m = findOneModel(ctx, "anthropic/claude-opus");
75
+ assert.ok(m);
76
+ assert.equal(modelKey(m), "anthropic/claude-opus");
77
+ });
78
+
79
+ it("finds ids containing slashes", () => {
80
+ const ctx = makeCtx([makeModel("lmstudio", "qwen/qwen3-vl-8b", 0)]);
81
+ const m = findOneModel(ctx, "lmstudio/qwen/qwen3-vl-8b");
82
+ assert.ok(m);
83
+ assert.equal(modelKey(m), "lmstudio/qwen/qwen3-vl-8b");
84
+ });
85
+
86
+ it("finds by substring", () => {
87
+ const ctx = makeCtx([
88
+ makeModel("anthropic", "claude-sonnet", 3),
89
+ makeModel("anthropic", "claude-opus", 15),
90
+ ]);
91
+ const m = findOneModel(ctx, "opus");
92
+ assert.ok(m);
93
+ assert.equal(modelKey(m), "anthropic/claude-opus");
94
+ });
95
+
96
+ it("returns undefined when not found", () => {
97
+ const ctx = makeCtx([]);
98
+ assert.equal(findOneModel(ctx, "anthropic/missing"), undefined);
99
+ });
100
+ });
101
+
102
+ describe("findCandidates", () => {
103
+ it("returns multiple models for an array", () => {
104
+ const ctx = makeCtx([
105
+ makeModel("anthropic", "claude-opus", 15),
106
+ makeModel("anthropic", "claude-sonnet", 3),
107
+ ]);
108
+ const candidates = findCandidates(ctx, [
109
+ "anthropic/claude-opus",
110
+ "anthropic/claude-sonnet",
111
+ ]);
112
+ assert.equal(candidates.length, 2);
113
+ });
114
+
115
+ it("deduplicates models", () => {
116
+ const ctx = makeCtx([makeModel("anthropic", "claude-opus", 15)]);
117
+ const candidates = findCandidates(ctx, [
118
+ "anthropic/claude-opus",
119
+ "anthropic/claude-opus",
120
+ ]);
121
+ assert.equal(candidates.length, 1);
122
+ });
123
+
124
+ it("matches substring and exact together", () => {
125
+ const ctx = makeCtx([
126
+ makeModel("anthropic", "claude-opus", 15),
127
+ makeModel("lmstudio", "qwen/qwen3-vl-8b", 0),
128
+ ]);
129
+ const candidates = findCandidates(ctx, ["anthropic/claude-opus", "lmstudio"]);
130
+ assert.equal(candidates.length, 2);
131
+ });
132
+ });
133
+
134
+ describe("selectModel", () => {
135
+ it("returns first candidate for first strategy", () => {
136
+ const a = makeModel("a", "a", 5, 10, 32000);
137
+ const b = makeModel("b", "b", 1, 2, 128000);
138
+ const m = selectModel([a, b], "first");
139
+ assert.equal(modelKey(m), "a/a");
140
+ });
141
+
142
+ it("returns cheapest (input+output)", () => {
143
+ const a = makeModel("a", "a", 5, 0);
144
+ const b = makeModel("b", "b", 1, 0);
145
+ const c = makeModel("c", "c", 3, 2);
146
+ const m = selectModel([a, b, c], "cheapest");
147
+ assert.equal(modelKey(m), "b/b");
148
+ });
149
+
150
+ it("returns cheapest input cost", () => {
151
+ const a = makeModel("a", "a", 5, 0);
152
+ const b = makeModel("b", "b", 1, 10);
153
+ const m = selectModel([a, b], "cheapest_input");
154
+ assert.equal(modelKey(m), "b/b");
155
+ });
156
+
157
+ it("returns cheapest output cost", () => {
158
+ const a = makeModel("a", "a", 0, 5);
159
+ const b = makeModel("b", "b", 5, 1);
160
+ const m = selectModel([a, b], "cheapest_output");
161
+ assert.equal(modelKey(m), "b/b");
162
+ });
163
+
164
+ it("returns largest context window", () => {
165
+ const a = makeModel("a", "a", 0, 0, 32000);
166
+ const b = makeModel("b", "b", 0, 0, 256000);
167
+ const m = selectModel([a, b], "largest_context");
168
+ assert.equal(modelKey(m), "b/b");
169
+ });
170
+
171
+ it("returns a random candidate", () => {
172
+ const a = makeModel("a", "a", 0, 0);
173
+ const b = makeModel("b", "b", 0, 0);
174
+ const results = new Set();
175
+ for (let i = 0; i < 20; i++) results.add(selectModel([a, b], "random")!.id);
176
+ assert.ok(results.has("a"));
177
+ assert.ok(results.has("b"));
178
+ });
179
+
180
+ it("returns undefined for empty candidates", () => {
181
+ assert.equal(selectModel([], "first"), undefined);
182
+ });
183
+ });
184
+
185
+ describe("resolveModel", () => {
186
+ it("resolves a single pattern", () => {
187
+ const ctx = makeCtx([makeModel("anthropic", "claude-opus", 15)]);
188
+ const m = resolveModel(ctx, "anthropic/claude-opus", "first");
189
+ assert.equal(modelKey(m), "anthropic/claude-opus");
190
+ });
191
+
192
+ it("resolves array to first available", () => {
193
+ const ctx = makeCtx([
194
+ makeModel("anthropic", "claude-opus", 15),
195
+ makeModel("anthropic", "claude-sonnet", 3),
196
+ ]);
197
+ const m = resolveModel(ctx, ["anthropic/missing", "anthropic/claude-sonnet"], "first");
198
+ assert.equal(modelKey(m), "anthropic/claude-sonnet");
199
+ });
200
+ });
201
+
202
+ describe("getStrategy", () => {
203
+ it("returns category strategy if set", () => {
204
+ const categoryStrategies = { economical: "cheapest" as const };
205
+ assert.equal(getStrategy(categoryStrategies, "first", "economical"), "cheapest");
206
+ assert.equal(getStrategy(categoryStrategies, "first", "frontier"), "first");
207
+ });
208
+
209
+ it("returns global strategy as fallback", () => {
210
+ assert.equal(getStrategy(undefined, "first", "economical"), "first");
211
+ });
212
+
213
+ it("defaults to first", () => {
214
+ assert.equal(getStrategy(undefined, undefined, "economical"), "first");
215
+ });
216
+ });
217
+
218
+ describe("classify (regex)", () => {
219
+ it("returns undefined for invalid regex pattern", () => {
220
+ const result = classify("hello", [{ pattern: "***invalid[", model: "frontier" }]);
221
+ assert.equal(result, undefined);
222
+ });
223
+ });
224
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "types": ["node"],
7
+ "strict": true,
8
+ "noUnusedLocals": true,
9
+ "noUnusedParameters": true,
10
+ "noFallthroughCasesInSwitch": true,
11
+ "noUncheckedSideEffectImports": true,
12
+ "allowImportingTsExtensions": true,
13
+ "noEmit": true,
14
+ "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true
16
+ },
17
+ "include": ["*.ts", "tests/*.ts"],
18
+ "exclude": ["node_modules"]
19
+ }