@jhizzard/termdeck 0.11.0 → 0.13.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/package.json +4 -2
- package/packages/cli/src/init-rumen.js +95 -54
- package/packages/client/public/app.js +17 -4
- package/packages/client/public/flashback-history.html +331 -0
- package/packages/client/public/flashback-history.js +258 -0
- package/packages/client/public/graph-controls.js +217 -0
- package/packages/client/public/graph.html +36 -0
- package/packages/client/public/graph.js +131 -15
- package/packages/client/public/index.html +1 -0
- package/packages/client/public/style.css +55 -0
- package/packages/server/src/agent-adapters/claude.js +158 -0
- package/packages/server/src/agent-adapters/index.js +55 -0
- package/packages/server/src/database.js +49 -1
- package/packages/server/src/flashback-diag.js +187 -13
- package/packages/server/src/index.js +58 -2
- package/packages/server/src/session.js +62 -31
- package/packages/server/src/setup/migrations.js +44 -4
- package/packages/server/src/setup/rumen/functions/graph-inference/index.ts +381 -0
- package/packages/server/src/setup/rumen/functions/graph-inference/tsconfig.json +14 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// Sprint 38 T2 — graph-inference Supabase Edge Function.
|
|
2
|
+
//
|
|
3
|
+
// Runs daily via pg_cron (see TermDeck migration 003_graph_inference_schedule.sql).
|
|
4
|
+
// Scans memory_items for pairs above GRAPH_INFERENCE_THRESHOLD cosine
|
|
5
|
+
// similarity, inserts/refreshes edges in memory_relationships, and
|
|
6
|
+
// optionally classifies edge types via Haiku 4.5.
|
|
7
|
+
//
|
|
8
|
+
// Coexists with the rag-system MCP-side ingest classifier — this cron
|
|
9
|
+
// fills cross-project edges and refreshes stale ingest-time edges that
|
|
10
|
+
// have NULL weight. Per-edge inferred_by = 'cron-YYYY-MM-DD' for audit.
|
|
11
|
+
//
|
|
12
|
+
// Deno runtime, NOT Node. Excluded from root tsconfig; canonical type
|
|
13
|
+
// check is `deno check` and `supabase functions deploy`.
|
|
14
|
+
//
|
|
15
|
+
// Deployment:
|
|
16
|
+
// supabase functions deploy graph-inference
|
|
17
|
+
// supabase secrets set DATABASE_URL="$DATABASE_URL"
|
|
18
|
+
// # Optional, gates LLM classification of new edges:
|
|
19
|
+
// supabase secrets set GRAPH_LLM_CLASSIFY=1 ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY"
|
|
20
|
+
// # Optional tuning (defaults shown):
|
|
21
|
+
// supabase secrets set GRAPH_INFERENCE_THRESHOLD=0.85
|
|
22
|
+
// supabase secrets set GRAPH_INFERENCE_MAX_LLM_CALLS=200
|
|
23
|
+
// supabase secrets set GRAPH_INFERENCE_MAX_PAIRS=5000
|
|
24
|
+
// supabase secrets set GRAPH_INFERENCE_PER_ROW_K=8 # Sprint 42: HNSW LATERAL top-K width
|
|
25
|
+
|
|
26
|
+
// @ts-ignore Deno std import resolved at runtime.
|
|
27
|
+
import { serve } from 'https://deno.land/std@0.224.0/http/server.ts';
|
|
28
|
+
// @ts-ignore npm specifier resolved at runtime.
|
|
29
|
+
// Deno-friendly postgres client (postgres.js). npm:pg@8.x has Node-native
|
|
30
|
+
// crypto/net deps that don't bundle in the Supabase Edge Runtime — caused
|
|
31
|
+
// BOOT_ERROR on first deploy 2026-04-27 ~19:35 ET. postgres.js is pure JS,
|
|
32
|
+
// works in Deno without polyfills.
|
|
33
|
+
import postgres from 'npm:postgres@3.4.4';
|
|
34
|
+
|
|
35
|
+
// Minimal API surface we use, typed loosely so the @ts-ignore stays narrow.
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
type Sql = any;
|
|
38
|
+
|
|
39
|
+
// @ts-ignore Deno global available at runtime.
|
|
40
|
+
declare const Deno: { env: { get: (k: string) => string | undefined } };
|
|
41
|
+
|
|
42
|
+
const VALID_TYPES = new Set([
|
|
43
|
+
'supersedes',
|
|
44
|
+
'relates_to',
|
|
45
|
+
'contradicts',
|
|
46
|
+
'elaborates',
|
|
47
|
+
'caused_by',
|
|
48
|
+
'blocks',
|
|
49
|
+
'inspired_by',
|
|
50
|
+
'cross_project_link',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const HAIKU_MODEL = 'claude-haiku-4-5-20251001';
|
|
54
|
+
|
|
55
|
+
interface InferenceSummary {
|
|
56
|
+
ok: boolean;
|
|
57
|
+
since: string | null;
|
|
58
|
+
candidates_scanned: number;
|
|
59
|
+
edges_inserted: number;
|
|
60
|
+
edges_refreshed: number;
|
|
61
|
+
llm_classifications: number;
|
|
62
|
+
llm_failures: number;
|
|
63
|
+
ms_total: number;
|
|
64
|
+
error?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function inferredByTag(now: Date): string {
|
|
68
|
+
const yyyy = now.getUTCFullYear();
|
|
69
|
+
const mm = String(now.getUTCMonth() + 1).padStart(2, '0');
|
|
70
|
+
const dd = String(now.getUTCDate()).padStart(2, '0');
|
|
71
|
+
return `cron-${yyyy}-${mm}-${dd}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseFloatEnv(name: string, fallback: number): number {
|
|
75
|
+
const raw = Deno.env.get(name);
|
|
76
|
+
if (!raw) return fallback;
|
|
77
|
+
const parsed = Number.parseFloat(raw);
|
|
78
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseIntEnv(name: string, fallback: number): number {
|
|
82
|
+
const raw = Deno.env.get(name);
|
|
83
|
+
if (!raw) return fallback;
|
|
84
|
+
const parsed = Number.parseInt(raw, 10);
|
|
85
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface CandidatePair {
|
|
89
|
+
source_id: string;
|
|
90
|
+
target_id: string;
|
|
91
|
+
similarity: number;
|
|
92
|
+
source_content: string;
|
|
93
|
+
target_content: string;
|
|
94
|
+
source_project: string | null;
|
|
95
|
+
target_project: string | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function fetchSince(sql: Sql): Promise<string | null> {
|
|
99
|
+
const result = await sql.unsafe(
|
|
100
|
+
`SELECT max(inferred_at) AS since FROM memory_relationships WHERE inferred_by ILIKE 'cron-%'`,
|
|
101
|
+
);
|
|
102
|
+
const row = result[0];
|
|
103
|
+
if (!row || !row.since) return null;
|
|
104
|
+
return new Date(row.since).toISOString();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function fetchCandidatePairs(
|
|
108
|
+
sql: Sql,
|
|
109
|
+
threshold: number,
|
|
110
|
+
since: string | null,
|
|
111
|
+
maxPairs: number,
|
|
112
|
+
perRowK: number,
|
|
113
|
+
): Promise<CandidatePair[]> {
|
|
114
|
+
// Sprint 42 T1 rewrite — HNSW-accelerated pairwise self-join.
|
|
115
|
+
//
|
|
116
|
+
// The pre-Sprint 42 query (`m1 JOIN m2 ON m1.id < m2.id AND (m1.embedding
|
|
117
|
+
// <=> m2.embedding) <= cutoff`) timed out at the 150s Edge Function
|
|
118
|
+
// wall-clock on >5K memory_items because cosine-distance constraints in
|
|
119
|
+
// a join's ON/WHERE clause cannot engage HNSW — they're post-join
|
|
120
|
+
// filters, evaluated for every candidate pair (~3.5M for 5K rows).
|
|
121
|
+
//
|
|
122
|
+
// The fix: switch to a CROSS JOIN LATERAL with `ORDER BY m2.embedding
|
|
123
|
+
// <=> m1.embedding LIMIT K` inside the lateral. HNSW serves the per-row
|
|
124
|
+
// top-K query in ~2ms each, so the work is O(N log K) ≈ N × HNSW-lookup
|
|
125
|
+
// rather than O(N²) cosine evaluations.
|
|
126
|
+
//
|
|
127
|
+
// Symmetry: each pair (A, B) may be found twice (once as A's neighbor
|
|
128
|
+
// of B, once as B's neighbor of A). LEAST/GREATEST canonicalizes the
|
|
129
|
+
// orientation; DISTINCT ON dedupes. This is more correct than filtering
|
|
130
|
+
// `m1.id < nbr.id` outside the lateral, which would lose pairs where
|
|
131
|
+
// only one direction's top-K contained the other.
|
|
132
|
+
//
|
|
133
|
+
// `since` filter: applied only to the outer m1. If m1 is old but m2
|
|
134
|
+
// was recently updated, the pair is still found on the iteration where
|
|
135
|
+
// m2 is the outer m1 (which IS recent). So filtering only m1 by `since`
|
|
136
|
+
// is sufficient and saves ~99% of work in steady state.
|
|
137
|
+
//
|
|
138
|
+
// EXPLAIN ANALYZE on petvetbid corpus (5,822 active rows, 2026-04-28):
|
|
139
|
+
// 13.5s cold start (since=NULL), HNSW correctly engaged, 718 raw
|
|
140
|
+
// matches → 359 unique pairs at threshold 0.85.
|
|
141
|
+
const result = await sql.unsafe(
|
|
142
|
+
`
|
|
143
|
+
SELECT DISTINCT ON (LEAST(m1.id, nbr.id), GREATEST(m1.id, nbr.id))
|
|
144
|
+
LEAST(m1.id, nbr.id) AS source_id,
|
|
145
|
+
GREATEST(m1.id, nbr.id) AS target_id,
|
|
146
|
+
1 - (m1.embedding <=> nbr.embedding) AS similarity,
|
|
147
|
+
CASE WHEN m1.id < nbr.id THEN m1.content ELSE nbr.content END AS source_content,
|
|
148
|
+
CASE WHEN m1.id < nbr.id THEN nbr.content ELSE m1.content END AS target_content,
|
|
149
|
+
CASE WHEN m1.id < nbr.id THEN m1.project ELSE nbr.project END AS source_project,
|
|
150
|
+
CASE WHEN m1.id < nbr.id THEN nbr.project ELSE m1.project END AS target_project
|
|
151
|
+
FROM memory_items m1
|
|
152
|
+
CROSS JOIN LATERAL (
|
|
153
|
+
SELECT id, embedding, content, project, updated_at
|
|
154
|
+
FROM memory_items m2
|
|
155
|
+
WHERE m2.is_active = true
|
|
156
|
+
AND m2.archived = false
|
|
157
|
+
AND m2.superseded_by IS NULL
|
|
158
|
+
AND m2.id <> m1.id
|
|
159
|
+
ORDER BY m2.embedding <=> m1.embedding
|
|
160
|
+
LIMIT $4
|
|
161
|
+
) nbr
|
|
162
|
+
WHERE m1.is_active = true
|
|
163
|
+
AND m1.archived = false
|
|
164
|
+
AND m1.superseded_by IS NULL
|
|
165
|
+
AND 1 - (m1.embedding <=> nbr.embedding) >= $1
|
|
166
|
+
AND ($2::timestamptz IS NULL OR m1.updated_at > $2::timestamptz)
|
|
167
|
+
ORDER BY LEAST(m1.id, nbr.id),
|
|
168
|
+
GREATEST(m1.id, nbr.id),
|
|
169
|
+
1 - (m1.embedding <=> nbr.embedding) DESC
|
|
170
|
+
LIMIT $3
|
|
171
|
+
`,
|
|
172
|
+
[threshold, since, maxPairs, perRowK],
|
|
173
|
+
);
|
|
174
|
+
return result as unknown as CandidatePair[];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function classifyPair(
|
|
178
|
+
apiKey: string,
|
|
179
|
+
pair: CandidatePair,
|
|
180
|
+
): Promise<string | null> {
|
|
181
|
+
const prompt = `You are classifying the relationship between two memories from the same developer.
|
|
182
|
+
|
|
183
|
+
Memory A: ${pair.source_content}
|
|
184
|
+
Memory B: ${pair.target_content}
|
|
185
|
+
|
|
186
|
+
Classify their relationship as exactly ONE of:
|
|
187
|
+
- supersedes — A replaces B (B is older/wrong/outdated)
|
|
188
|
+
- relates_to — A and B are about the same topic/system
|
|
189
|
+
- contradicts — A and B claim conflicting facts
|
|
190
|
+
- elaborates — A provides more detail about something B mentions
|
|
191
|
+
- caused_by — A is a consequence of something described in B
|
|
192
|
+
- blocks — A's resolution depends on B
|
|
193
|
+
- inspired_by — A's idea originated from B
|
|
194
|
+
- cross_project_link — A and B are in different projects but reference shared infrastructure
|
|
195
|
+
|
|
196
|
+
Return ONLY the type token, no explanation.`;
|
|
197
|
+
|
|
198
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
headers: {
|
|
201
|
+
'Content-Type': 'application/json',
|
|
202
|
+
'x-api-key': apiKey,
|
|
203
|
+
'anthropic-version': '2023-06-01',
|
|
204
|
+
},
|
|
205
|
+
body: JSON.stringify({
|
|
206
|
+
model: HAIKU_MODEL,
|
|
207
|
+
max_tokens: 32,
|
|
208
|
+
messages: [{ role: 'user', content: prompt }],
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (!response.ok) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const payload = await response.json();
|
|
217
|
+
const block = payload?.content?.[0];
|
|
218
|
+
if (!block || block.type !== 'text') return null;
|
|
219
|
+
const token = String(block.text).trim().toLowerCase().split(/\s+/)[0];
|
|
220
|
+
return VALID_TYPES.has(token) ? token : null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function upsertEdge(
|
|
224
|
+
sql: Sql,
|
|
225
|
+
pair: CandidatePair,
|
|
226
|
+
relationshipType: string,
|
|
227
|
+
inferredBy: string,
|
|
228
|
+
): Promise<'inserted' | 'refreshed' | 'skipped'> {
|
|
229
|
+
const result = await sql.unsafe(
|
|
230
|
+
`
|
|
231
|
+
INSERT INTO memory_relationships (
|
|
232
|
+
source_id, target_id, relationship_type, weight, inferred_at, inferred_by
|
|
233
|
+
) VALUES ($1, $2, $3, $4, now(), $5)
|
|
234
|
+
ON CONFLICT (source_id, target_id, relationship_type) DO UPDATE
|
|
235
|
+
SET weight = EXCLUDED.weight,
|
|
236
|
+
inferred_at = EXCLUDED.inferred_at,
|
|
237
|
+
inferred_by = EXCLUDED.inferred_by
|
|
238
|
+
WHERE memory_relationships.weight IS NULL
|
|
239
|
+
OR memory_relationships.inferred_at IS NULL
|
|
240
|
+
OR memory_relationships.inferred_at < now() - interval '7 days'
|
|
241
|
+
RETURNING (xmax = 0) AS inserted
|
|
242
|
+
`,
|
|
243
|
+
[pair.source_id, pair.target_id, relationshipType, pair.similarity, inferredBy],
|
|
244
|
+
);
|
|
245
|
+
if (result.length === 0) return 'skipped';
|
|
246
|
+
return result[0].inserted ? 'inserted' : 'refreshed';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isMissingColumnError(err: unknown): boolean {
|
|
250
|
+
if (!(err instanceof Error)) return false;
|
|
251
|
+
const message = err.message.toLowerCase();
|
|
252
|
+
return (
|
|
253
|
+
message.includes('column') &&
|
|
254
|
+
(message.includes('inferred_at') ||
|
|
255
|
+
message.includes('inferred_by') ||
|
|
256
|
+
message.includes('weight'))
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function runGraphInference(sql: Sql): Promise<InferenceSummary> {
|
|
261
|
+
const start = Date.now();
|
|
262
|
+
const summary: InferenceSummary = {
|
|
263
|
+
ok: false,
|
|
264
|
+
since: null,
|
|
265
|
+
candidates_scanned: 0,
|
|
266
|
+
edges_inserted: 0,
|
|
267
|
+
edges_refreshed: 0,
|
|
268
|
+
llm_classifications: 0,
|
|
269
|
+
llm_failures: 0,
|
|
270
|
+
ms_total: 0,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const threshold = parseFloatEnv('GRAPH_INFERENCE_THRESHOLD', 0.85);
|
|
274
|
+
const maxPairs = parseIntEnv('GRAPH_INFERENCE_MAX_PAIRS', 5000);
|
|
275
|
+
const maxLlmCalls = parseIntEnv('GRAPH_INFERENCE_MAX_LLM_CALLS', 200);
|
|
276
|
+
// GRAPH_INFERENCE_PER_ROW_K — top-K HNSW lookup width for the LATERAL
|
|
277
|
+
// self-join (Sprint 42 T1 rewrite). 8 is a recall/perf sweet spot at
|
|
278
|
+
// threshold 0.85: it captures the high-similarity tail without paying
|
|
279
|
+
// for many post-filter rejections. Raise to 12 if recall drops.
|
|
280
|
+
const perRowK = parseIntEnv('GRAPH_INFERENCE_PER_ROW_K', 8);
|
|
281
|
+
const llmEnabled = Deno.env.get('GRAPH_LLM_CLASSIFY') === '1';
|
|
282
|
+
const apiKey = Deno.env.get('ANTHROPIC_API_KEY') ?? '';
|
|
283
|
+
const inferredBy = inferredByTag(new Date());
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
summary.since = await fetchSince(sql);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
if (isMissingColumnError(err)) {
|
|
289
|
+
summary.error = 'awaiting migration 009';
|
|
290
|
+
summary.ms_total = Date.now() - start;
|
|
291
|
+
return summary;
|
|
292
|
+
}
|
|
293
|
+
throw err;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const candidates = await fetchCandidatePairs(sql, threshold, summary.since, maxPairs, perRowK);
|
|
297
|
+
summary.candidates_scanned = candidates.length;
|
|
298
|
+
|
|
299
|
+
for (const pair of candidates) {
|
|
300
|
+
let relationshipType = 'relates_to';
|
|
301
|
+
let isNewEdge = false;
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const outcome = await upsertEdge(sql, pair, relationshipType, inferredBy);
|
|
305
|
+
if (outcome === 'skipped') continue;
|
|
306
|
+
isNewEdge = outcome === 'inserted';
|
|
307
|
+
if (outcome === 'inserted') summary.edges_inserted++;
|
|
308
|
+
if (outcome === 'refreshed') summary.edges_refreshed++;
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (isMissingColumnError(err)) {
|
|
311
|
+
summary.error = 'awaiting migration 009';
|
|
312
|
+
summary.ms_total = Date.now() - start;
|
|
313
|
+
return summary;
|
|
314
|
+
}
|
|
315
|
+
throw err;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (
|
|
319
|
+
llmEnabled &&
|
|
320
|
+
apiKey &&
|
|
321
|
+
isNewEdge &&
|
|
322
|
+
summary.llm_classifications + summary.llm_failures < maxLlmCalls
|
|
323
|
+
) {
|
|
324
|
+
const classified = await classifyPair(apiKey, pair);
|
|
325
|
+
if (classified && classified !== relationshipType) {
|
|
326
|
+
try {
|
|
327
|
+
await upsertEdge(sql, pair, classified, inferredBy);
|
|
328
|
+
summary.llm_classifications++;
|
|
329
|
+
} catch {
|
|
330
|
+
summary.llm_failures++;
|
|
331
|
+
}
|
|
332
|
+
} else if (classified) {
|
|
333
|
+
summary.llm_classifications++;
|
|
334
|
+
} else {
|
|
335
|
+
summary.llm_failures++;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
summary.ok = true;
|
|
341
|
+
summary.ms_total = Date.now() - start;
|
|
342
|
+
return summary;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
serve(async (_req: Request) => {
|
|
346
|
+
const url = Deno.env.get('DATABASE_URL');
|
|
347
|
+
if (!url) {
|
|
348
|
+
console.error('[graph-inference] DATABASE_URL not set in Edge Function secrets');
|
|
349
|
+
return new Response(
|
|
350
|
+
JSON.stringify({ ok: false, error: 'DATABASE_URL not set' }),
|
|
351
|
+
{ status: 500, headers: { 'Content-Type': 'application/json' } },
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const sql = postgres(url, { max: 4, prepare: false });
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
console.log('[graph-inference] tick starting');
|
|
359
|
+
const summary = await runGraphInference(sql);
|
|
360
|
+
console.log(
|
|
361
|
+
`[graph-inference] tick complete inserted=${summary.edges_inserted} refreshed=${summary.edges_refreshed} ms=${summary.ms_total}`,
|
|
362
|
+
);
|
|
363
|
+
return new Response(JSON.stringify(summary), {
|
|
364
|
+
status: summary.ok ? 200 : 500,
|
|
365
|
+
headers: { 'Content-Type': 'application/json' },
|
|
366
|
+
});
|
|
367
|
+
} catch (err) {
|
|
368
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
369
|
+
console.error('[graph-inference] tick threw:', err);
|
|
370
|
+
return new Response(
|
|
371
|
+
JSON.stringify({ ok: false, error: message }),
|
|
372
|
+
{ status: 500, headers: { 'Content-Type': 'application/json' } },
|
|
373
|
+
);
|
|
374
|
+
} finally {
|
|
375
|
+
try {
|
|
376
|
+
await sql.end();
|
|
377
|
+
} catch (err) {
|
|
378
|
+
console.error('[graph-inference] sql.end() failed:', err);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"allowImportingTsExtensions": false,
|
|
11
|
+
"types": []
|
|
12
|
+
},
|
|
13
|
+
"include": ["index.ts"]
|
|
14
|
+
}
|