@looptech-ai/understand-quickly-mcp 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.
- package/README.md +147 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +134 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +53 -0
- package/dist/registry.js +191 -0
- package/dist/registry.js.map +1 -0
- package/dist/tools/find-graph-for-repo.d.ts +53 -0
- package/dist/tools/find-graph-for-repo.js +167 -0
- package/dist/tools/find-graph-for-repo.js.map +1 -0
- package/dist/tools/get-graph.d.ts +17 -0
- package/dist/tools/get-graph.js +28 -0
- package/dist/tools/get-graph.js.map +1 -0
- package/dist/tools/list-repos.d.ts +24 -0
- package/dist/tools/list-repos.js +51 -0
- package/dist/tools/list-repos.js.map +1 -0
- package/dist/tools/search-concepts.d.ts +43 -0
- package/dist/tools/search-concepts.js +171 -0
- package/dist/tools/search-concepts.js.map +1 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +43 -0
- package/src/index.ts +168 -0
- package/src/registry.ts +272 -0
- package/src/tools/find-graph-for-repo.ts +221 -0
- package/src/tools/get-graph.ts +36 -0
- package/src/tools/list-repos.ts +62 -0
- package/src/tools/search-concepts.ts +239 -0
- package/src/types.ts +101 -0
- package/tests/registry.test.ts +171 -0
- package/tests/tools.test.ts +351 -0
- package/tests/tsconfig.json +9 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
clearCache,
|
|
6
|
+
clearStatsCache,
|
|
7
|
+
} from "../src/registry.ts";
|
|
8
|
+
import {
|
|
9
|
+
findGraphForRepo,
|
|
10
|
+
parseGithubUrl,
|
|
11
|
+
levenshtein,
|
|
12
|
+
} from "../src/tools/find-graph-for-repo.ts";
|
|
13
|
+
import { searchConcepts } from "../src/tools/search-concepts.ts";
|
|
14
|
+
import type {
|
|
15
|
+
FetchImpl,
|
|
16
|
+
Registry,
|
|
17
|
+
RegistryEntry,
|
|
18
|
+
StatsJson,
|
|
19
|
+
} from "../src/types.ts";
|
|
20
|
+
|
|
21
|
+
const REGISTRY_URL = "https://example.invalid/registry.json";
|
|
22
|
+
const STATS_URL = "https://example.invalid/stats.json";
|
|
23
|
+
|
|
24
|
+
function makeRegistry(extras?: Partial<Registry>): Registry {
|
|
25
|
+
const entries: RegistryEntry[] = [
|
|
26
|
+
{
|
|
27
|
+
id: "alice/python-graph",
|
|
28
|
+
format: "understand-anything@1",
|
|
29
|
+
graph_url: "https://example.invalid/alice.json",
|
|
30
|
+
status: "ok",
|
|
31
|
+
tags: ["python"],
|
|
32
|
+
last_synced: "2026-05-07T00:00:00Z",
|
|
33
|
+
last_sha: "abc123",
|
|
34
|
+
source_sha: "abc123",
|
|
35
|
+
head_sha: "def456",
|
|
36
|
+
commits_behind: 17,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "bob/ts-graph",
|
|
40
|
+
format: "understand-anything@1",
|
|
41
|
+
graph_url: "https://example.invalid/bob.json",
|
|
42
|
+
status: "ok",
|
|
43
|
+
tags: ["typescript"],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "carol/rust-graph",
|
|
47
|
+
format: "gitnexus@1",
|
|
48
|
+
graph_url: "https://example.invalid/carol.json",
|
|
49
|
+
status: "ok",
|
|
50
|
+
tags: ["rust"],
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
return {
|
|
54
|
+
schema_version: 1,
|
|
55
|
+
generated_at: "2026-05-07T00:00:00Z",
|
|
56
|
+
entries,
|
|
57
|
+
...extras,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function makeStats(extras?: Partial<StatsJson>): StatsJson {
|
|
62
|
+
return {
|
|
63
|
+
schema_version: 1,
|
|
64
|
+
generated_at: "2026-05-07T00:00:00Z",
|
|
65
|
+
totals: { entries: 3, nodes: 100, edges: 200 },
|
|
66
|
+
kinds: [],
|
|
67
|
+
languages: [],
|
|
68
|
+
concepts: [
|
|
69
|
+
{ term: "parser", entries: 3, samples: ["alice/python-graph", "bob/ts-graph", "carol/rust-graph"] },
|
|
70
|
+
{ term: "tokenizer", entries: 2, samples: ["alice/python-graph", "bob/ts-graph"] },
|
|
71
|
+
{ term: "embedding", entries: 2, samples: ["alice/python-graph", "carol/rust-graph"] },
|
|
72
|
+
],
|
|
73
|
+
...extras,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface FetchSpec {
|
|
78
|
+
body?: unknown;
|
|
79
|
+
ok?: boolean;
|
|
80
|
+
status?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function makeFetch(routes: Record<string, FetchSpec | (() => FetchSpec)>): {
|
|
84
|
+
fetch: FetchImpl;
|
|
85
|
+
calls: string[];
|
|
86
|
+
} {
|
|
87
|
+
const calls: string[] = [];
|
|
88
|
+
const fetch: FetchImpl = async (input) => {
|
|
89
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
90
|
+
calls.push(url);
|
|
91
|
+
const route = routes[url];
|
|
92
|
+
const spec = typeof route === "function" ? route() : route;
|
|
93
|
+
if (!spec) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
status: 404,
|
|
97
|
+
statusText: "Not Found",
|
|
98
|
+
json: async () => ({}),
|
|
99
|
+
text: async () => "",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
ok: spec.ok ?? true,
|
|
104
|
+
status: spec.status ?? 200,
|
|
105
|
+
statusText: "OK",
|
|
106
|
+
json: async () => spec.body,
|
|
107
|
+
text: async () => JSON.stringify(spec.body ?? null),
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
return { fetch, calls };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
describe("parseGithubUrl", () => {
|
|
114
|
+
it("parses bare https URL", () => {
|
|
115
|
+
assert.equal(
|
|
116
|
+
parseGithubUrl("https://github.com/alice/python-graph"),
|
|
117
|
+
"alice/python-graph",
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
it("parses https URL with .git suffix", () => {
|
|
121
|
+
assert.equal(
|
|
122
|
+
parseGithubUrl("https://github.com/alice/python-graph.git"),
|
|
123
|
+
"alice/python-graph",
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
it("parses ssh URL", () => {
|
|
127
|
+
assert.equal(
|
|
128
|
+
parseGithubUrl("git@github.com:alice/python-graph.git"),
|
|
129
|
+
"alice/python-graph",
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
it("parses https URL with branch path", () => {
|
|
133
|
+
assert.equal(
|
|
134
|
+
parseGithubUrl("https://github.com/alice/python-graph/tree/main/src"),
|
|
135
|
+
"alice/python-graph",
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
it("parses https URL with trailing slash", () => {
|
|
139
|
+
assert.equal(
|
|
140
|
+
parseGithubUrl("https://github.com/alice/python-graph/"),
|
|
141
|
+
"alice/python-graph",
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
it("returns undefined for non-github URLs", () => {
|
|
145
|
+
assert.equal(parseGithubUrl("https://gitlab.com/foo/bar"), undefined);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("levenshtein", () => {
|
|
150
|
+
it("returns 0 for identical strings", () => {
|
|
151
|
+
assert.equal(levenshtein("abc", "abc"), 0);
|
|
152
|
+
});
|
|
153
|
+
it("returns the edit distance", () => {
|
|
154
|
+
assert.equal(levenshtein("kitten", "sitting"), 3);
|
|
155
|
+
});
|
|
156
|
+
it("short-circuits when distance exceeds max", () => {
|
|
157
|
+
assert.ok(levenshtein("abcdefghij", "zzzzzzzzzz", 2) > 2);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("find_graph_for_repo", () => {
|
|
162
|
+
beforeEach(() => clearCache());
|
|
163
|
+
|
|
164
|
+
it("finds an entry by id (happy path)", async () => {
|
|
165
|
+
const registry = makeRegistry();
|
|
166
|
+
const { fetch } = makeFetch({ [REGISTRY_URL]: { body: registry } });
|
|
167
|
+
const out = await findGraphForRepo(
|
|
168
|
+
{ id: "alice/python-graph" },
|
|
169
|
+
{ fetchImpl: fetch, source: REGISTRY_URL },
|
|
170
|
+
);
|
|
171
|
+
assert.equal(out.found, true);
|
|
172
|
+
if (out.found) {
|
|
173
|
+
assert.equal(out.id, "alice/python-graph");
|
|
174
|
+
assert.equal(out.graph_url, "https://example.invalid/alice.json");
|
|
175
|
+
assert.equal(out.commits_behind, 17);
|
|
176
|
+
assert.equal(out.drift_summary, "behind by 17 commits");
|
|
177
|
+
assert.equal(out.head_sha, "def456");
|
|
178
|
+
assert.equal(out.source_sha, "abc123");
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("finds an entry by github_url (4 forms)", async () => {
|
|
183
|
+
const registry = makeRegistry();
|
|
184
|
+
const urls = [
|
|
185
|
+
"https://github.com/alice/python-graph",
|
|
186
|
+
"https://github.com/alice/python-graph.git",
|
|
187
|
+
"git@github.com:alice/python-graph.git",
|
|
188
|
+
"https://github.com/alice/python-graph/tree/main/src",
|
|
189
|
+
];
|
|
190
|
+
for (const url of urls) {
|
|
191
|
+
clearCache();
|
|
192
|
+
const { fetch } = makeFetch({ [REGISTRY_URL]: { body: registry } });
|
|
193
|
+
const out = await findGraphForRepo(
|
|
194
|
+
{ github_url: url },
|
|
195
|
+
{ fetchImpl: fetch, source: REGISTRY_URL },
|
|
196
|
+
);
|
|
197
|
+
assert.equal(out.found, true, `should find for url ${url}`);
|
|
198
|
+
if (out.found) {
|
|
199
|
+
assert.equal(out.id, "alice/python-graph");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("returns suggestions when not found (fuzzy match)", async () => {
|
|
205
|
+
const registry = makeRegistry();
|
|
206
|
+
const { fetch } = makeFetch({ [REGISTRY_URL]: { body: registry } });
|
|
207
|
+
// "alice/python-graphs" has Levenshtein 1 from "alice/python-graph"
|
|
208
|
+
const out = await findGraphForRepo(
|
|
209
|
+
{ id: "alice/python-graphs" },
|
|
210
|
+
{ fetchImpl: fetch, source: REGISTRY_URL },
|
|
211
|
+
);
|
|
212
|
+
assert.equal(out.found, false);
|
|
213
|
+
if (!out.found) {
|
|
214
|
+
assert.ok(
|
|
215
|
+
out.suggestions.includes("alice/python-graph"),
|
|
216
|
+
`expected alice/python-graph in suggestions, got ${out.suggestions.join(",")}`,
|
|
217
|
+
);
|
|
218
|
+
assert.ok(out.suggestions.length <= 5);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("rejects invalid id", async () => {
|
|
223
|
+
const registry = makeRegistry();
|
|
224
|
+
const { fetch } = makeFetch({ [REGISTRY_URL]: { body: registry } });
|
|
225
|
+
await assert.rejects(
|
|
226
|
+
() =>
|
|
227
|
+
findGraphForRepo(
|
|
228
|
+
{ id: "no-slash-here" },
|
|
229
|
+
{ fetchImpl: fetch, source: REGISTRY_URL },
|
|
230
|
+
),
|
|
231
|
+
/owner\/repo/,
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("rejects unparseable github_url", async () => {
|
|
236
|
+
const registry = makeRegistry();
|
|
237
|
+
const { fetch } = makeFetch({ [REGISTRY_URL]: { body: registry } });
|
|
238
|
+
await assert.rejects(
|
|
239
|
+
() =>
|
|
240
|
+
findGraphForRepo(
|
|
241
|
+
{ github_url: "not a url at all" },
|
|
242
|
+
{ fetchImpl: fetch, source: REGISTRY_URL },
|
|
243
|
+
),
|
|
244
|
+
/Could not extract/,
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("rejects empty input", async () => {
|
|
249
|
+
await assert.rejects(
|
|
250
|
+
() => findGraphForRepo({}),
|
|
251
|
+
/at least one of/,
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("search_concepts (default: stats.json)", () => {
|
|
257
|
+
beforeEach(() => {
|
|
258
|
+
clearCache();
|
|
259
|
+
clearStatsCache();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("returns matching concepts from stats.json (no id)", async () => {
|
|
263
|
+
const stats = makeStats();
|
|
264
|
+
const { fetch, calls } = makeFetch({
|
|
265
|
+
[STATS_URL]: { body: stats },
|
|
266
|
+
});
|
|
267
|
+
const out = await searchConcepts(
|
|
268
|
+
{ query: "parse" },
|
|
269
|
+
{ fetchImpl: fetch, statsSource: STATS_URL, registrySource: REGISTRY_URL },
|
|
270
|
+
);
|
|
271
|
+
assert.equal(out.source, "stats");
|
|
272
|
+
assert.ok(out.matches);
|
|
273
|
+
assert.equal(out.matches!.length, 1);
|
|
274
|
+
assert.equal(out.matches![0].term, "parser");
|
|
275
|
+
assert.equal(out.matches![0].count, 3);
|
|
276
|
+
assert.deepEqual(out.matches![0].samples, [
|
|
277
|
+
"alice/python-graph",
|
|
278
|
+
"bob/ts-graph",
|
|
279
|
+
"carol/rust-graph",
|
|
280
|
+
]);
|
|
281
|
+
// Stats hit only — no graph or registry calls.
|
|
282
|
+
assert.equal(calls.length, 1);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("falls back to fan-out when stats.json is unavailable", async () => {
|
|
286
|
+
const registry = makeRegistry();
|
|
287
|
+
const aliceGraph = {
|
|
288
|
+
nodes: [
|
|
289
|
+
{ id: "n1", label: "Parser" },
|
|
290
|
+
{ id: "n2", label: "Lexer" },
|
|
291
|
+
],
|
|
292
|
+
};
|
|
293
|
+
const bobGraph = { nodes: [{ id: "n1", label: "Tokenizer" }] };
|
|
294
|
+
const carolGraph = { nodes: [{ id: "n1", label: "EmbeddingsParser" }] };
|
|
295
|
+
const { fetch } = makeFetch({
|
|
296
|
+
[STATS_URL]: { ok: false, status: 404 },
|
|
297
|
+
[REGISTRY_URL]: { body: registry },
|
|
298
|
+
"https://example.invalid/alice.json": { body: aliceGraph },
|
|
299
|
+
"https://example.invalid/bob.json": { body: bobGraph },
|
|
300
|
+
"https://example.invalid/carol.json": { body: carolGraph },
|
|
301
|
+
});
|
|
302
|
+
const out = await searchConcepts(
|
|
303
|
+
{ query: "parser" },
|
|
304
|
+
{ fetchImpl: fetch, statsSource: STATS_URL, registrySource: REGISTRY_URL },
|
|
305
|
+
);
|
|
306
|
+
assert.equal(out.source, "fanout");
|
|
307
|
+
assert.ok(out.results);
|
|
308
|
+
// Two entries should match: alice (Parser) and carol (EmbeddingsParser).
|
|
309
|
+
const matchedIds = out.results!.map((r) => r.id).sort();
|
|
310
|
+
assert.deepEqual(matchedIds, ["alice/python-graph", "carol/rust-graph"]);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("uses single-graph fan-out when id is provided", async () => {
|
|
314
|
+
const registry = makeRegistry();
|
|
315
|
+
const aliceGraph = {
|
|
316
|
+
nodes: [
|
|
317
|
+
{ id: "n1", label: "Parser" },
|
|
318
|
+
{ id: "n2", label: "Lexer" },
|
|
319
|
+
],
|
|
320
|
+
};
|
|
321
|
+
const { fetch, calls } = makeFetch({
|
|
322
|
+
[REGISTRY_URL]: { body: registry },
|
|
323
|
+
"https://example.invalid/alice.json": { body: aliceGraph },
|
|
324
|
+
});
|
|
325
|
+
const out = await searchConcepts(
|
|
326
|
+
{ query: "lexer", id: "alice/python-graph" },
|
|
327
|
+
{ fetchImpl: fetch, statsSource: STATS_URL, registrySource: REGISTRY_URL },
|
|
328
|
+
);
|
|
329
|
+
assert.equal(out.source, "graph");
|
|
330
|
+
assert.equal(out.scanned, 1);
|
|
331
|
+
assert.ok(out.results);
|
|
332
|
+
assert.equal(out.results!.length, 1);
|
|
333
|
+
assert.equal(out.results![0].hits.length, 1);
|
|
334
|
+
assert.equal(out.results![0].hits[0].matched_value, "Lexer");
|
|
335
|
+
// Should not have hit stats.json.
|
|
336
|
+
assert.ok(!calls.includes(STATS_URL), "stats.json should not be fetched in id mode");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("rejects when id is not in registry", async () => {
|
|
340
|
+
const registry = makeRegistry();
|
|
341
|
+
const { fetch } = makeFetch({ [REGISTRY_URL]: { body: registry } });
|
|
342
|
+
await assert.rejects(
|
|
343
|
+
() =>
|
|
344
|
+
searchConcepts(
|
|
345
|
+
{ query: "x", id: "ghost/nope" },
|
|
346
|
+
{ fetchImpl: fetch, statsSource: STATS_URL, registrySource: REGISTRY_URL },
|
|
347
|
+
),
|
|
348
|
+
/No registry entry/,
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
"outDir": "dist",
|
|
14
|
+
"rootDir": "src",
|
|
15
|
+
"types": ["node"]
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*.ts"],
|
|
18
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
19
|
+
}
|