opencode-agora 0.4.2 → 0.4.3
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 +42 -2
- package/dist/cli/app.d.ts.map +1 -1
- package/dist/cli/app.js +14 -0
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/commands/browse.d.ts.map +1 -1
- package/dist/cli/commands/browse.js +1 -5
- package/dist/cli/commands/browse.js.map +1 -1
- package/dist/cli/commands/capabilities.d.ts +3 -0
- package/dist/cli/commands/capabilities.d.ts.map +1 -0
- package/dist/cli/commands/capabilities.js +146 -0
- package/dist/cli/commands/capabilities.js.map +1 -0
- package/dist/cli/commands/community.d.ts.map +1 -1
- package/dist/cli/commands/community.js.map +1 -1
- package/dist/cli/commands/curate.d.ts +9 -0
- package/dist/cli/commands/curate.d.ts.map +1 -0
- package/dist/cli/commands/curate.js +62 -0
- package/dist/cli/commands/curate.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +77 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/export.d.ts.map +1 -1
- package/dist/cli/commands/export.js +5 -3
- package/dist/cli/commands/export.js.map +1 -1
- package/dist/cli/commands/freeze.d.ts +3 -0
- package/dist/cli/commands/freeze.d.ts.map +1 -0
- package/dist/cli/commands/freeze.js +62 -0
- package/dist/cli/commands/freeze.js.map +1 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +5 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/installed.d.ts +3 -0
- package/dist/cli/commands/installed.d.ts.map +1 -0
- package/dist/cli/commands/installed.js +82 -0
- package/dist/cli/commands/installed.js.map +1 -0
- package/dist/cli/commands/marketplace.d.ts.map +1 -1
- package/dist/cli/commands/marketplace.js.map +1 -1
- package/dist/cli/commands/notify.d.ts.map +1 -1
- package/dist/cli/commands/notify.js.map +1 -1
- package/dist/cli/commands/operations.d.ts.map +1 -1
- package/dist/cli/commands/operations.js +64 -1
- package/dist/cli/commands/operations.js.map +1 -1
- package/dist/cli/commands/ping.js +2 -1
- package/dist/cli/commands/ping.js.map +1 -1
- package/dist/cli/commands/scan.js.map +1 -1
- package/dist/cli/commands/sync.d.ts +3 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +172 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/today.d.ts.map +1 -1
- package/dist/cli/commands/today.js +11 -3
- package/dist/cli/commands/today.js.map +1 -1
- package/dist/cli/commands/try.d.ts +3 -0
- package/dist/cli/commands/try.d.ts.map +1 -0
- package/dist/cli/commands/try.js +157 -0
- package/dist/cli/commands/try.js.map +1 -0
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/commands/watch.js.map +1 -1
- package/dist/cli/commands/welcome.d.ts.map +1 -1
- package/dist/cli/commands/welcome.js +6 -27
- package/dist/cli/commands/welcome.js.map +1 -1
- package/dist/cli/commands-meta.d.ts +1 -1
- package/dist/cli/commands-meta.d.ts.map +1 -1
- package/dist/cli/commands-meta.js +222 -21
- package/dist/cli/commands-meta.js.map +1 -1
- package/dist/cli/completions-gen.d.ts.map +1 -1
- package/dist/cli/completions-gen.js +130 -52
- package/dist/cli/completions-gen.js.map +1 -1
- package/dist/cli/flags.d.ts.map +1 -1
- package/dist/cli/flags.js +7 -0
- package/dist/cli/flags.js.map +1 -1
- package/dist/cli/format.js +1 -1
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/helpers.js.map +1 -1
- package/dist/cli/mcp-server.d.ts +5 -0
- package/dist/cli/mcp-server.d.ts.map +1 -1
- package/dist/cli/mcp-server.js +222 -0
- package/dist/cli/mcp-server.js.map +1 -1
- package/dist/cli/pages/community.d.ts.map +1 -1
- package/dist/cli/pages/community.js +4 -7
- package/dist/cli/pages/community.js.map +1 -1
- package/dist/cli/pages/home.d.ts.map +1 -1
- package/dist/cli/pages/home.js +22 -8
- package/dist/cli/pages/home.js.map +1 -1
- package/dist/cli/pages/marketplace.d.ts.map +1 -1
- package/dist/cli/pages/marketplace.js +12 -9
- package/dist/cli/pages/marketplace.js.map +1 -1
- package/dist/cli/pages/news.d.ts.map +1 -1
- package/dist/cli/pages/news.js.map +1 -1
- package/dist/cli/pages/stack.d.ts +3 -0
- package/dist/cli/pages/stack.d.ts.map +1 -0
- package/dist/cli/pages/stack.js +373 -0
- package/dist/cli/pages/stack.js.map +1 -0
- package/dist/cli/pages/types.d.ts +1 -1
- package/dist/cli/pages/types.d.ts.map +1 -1
- package/dist/cli/shell.d.ts +1 -1
- package/dist/cli/shell.d.ts.map +1 -1
- package/dist/cli/shell.js +52 -2
- package/dist/cli/shell.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +14 -4
- package/dist/cli/tui.js.map +1 -1
- package/dist/community/client.d.ts.map +1 -1
- package/dist/community/client.js +3 -1
- package/dist/community/client.js.map +1 -1
- package/dist/curator/index.d.ts +95 -0
- package/dist/curator/index.d.ts.map +1 -0
- package/dist/curator/index.js +446 -0
- package/dist/curator/index.js.map +1 -0
- package/dist/hubs/enrichment.d.ts.map +1 -1
- package/dist/hubs/enrichment.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/marketplace.d.ts +2 -1
- package/dist/marketplace.d.ts.map +1 -1
- package/dist/marketplace.js +86 -17
- package/dist/marketplace.js.map +1 -1
- package/dist/outdated.d.ts.map +1 -1
- package/dist/outdated.js +25 -4
- package/dist/outdated.js.map +1 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +83 -15
- package/dist/scan.js.map +1 -1
- package/dist/search/catalog-index.d.ts +107 -0
- package/dist/search/catalog-index.d.ts.map +1 -0
- package/dist/search/catalog-index.js +276 -0
- package/dist/search/catalog-index.js.map +1 -0
- package/dist/stack/adapters/claude-code.d.ts +3 -0
- package/dist/stack/adapters/claude-code.d.ts.map +1 -0
- package/dist/stack/adapters/claude-code.js +282 -0
- package/dist/stack/adapters/claude-code.js.map +1 -0
- package/dist/stack/adapters/cursor.d.ts +3 -0
- package/dist/stack/adapters/cursor.d.ts.map +1 -0
- package/dist/stack/adapters/cursor.js +232 -0
- package/dist/stack/adapters/cursor.js.map +1 -0
- package/dist/stack/adapters/opencode.d.ts +3 -0
- package/dist/stack/adapters/opencode.d.ts.map +1 -0
- package/dist/stack/adapters/opencode.js +177 -0
- package/dist/stack/adapters/opencode.js.map +1 -0
- package/dist/stack/adapters/windsurf.d.ts +3 -0
- package/dist/stack/adapters/windsurf.d.ts.map +1 -0
- package/dist/stack/adapters/windsurf.js +218 -0
- package/dist/stack/adapters/windsurf.js.map +1 -0
- package/dist/stack/capability-cache.d.ts +19 -0
- package/dist/stack/capability-cache.d.ts.map +1 -0
- package/dist/stack/capability-cache.js +41 -0
- package/dist/stack/capability-cache.js.map +1 -0
- package/dist/stack/doctor.d.ts +30 -0
- package/dist/stack/doctor.d.ts.map +1 -0
- package/dist/stack/doctor.js +227 -0
- package/dist/stack/doctor.js.map +1 -0
- package/dist/stack/manifest.d.ts +40 -0
- package/dist/stack/manifest.d.ts.map +1 -0
- package/dist/stack/manifest.js +342 -0
- package/dist/stack/manifest.js.map +1 -0
- package/dist/stack/mcp-probe.d.ts +23 -0
- package/dist/stack/mcp-probe.d.ts.map +1 -0
- package/dist/stack/mcp-probe.js +230 -0
- package/dist/stack/mcp-probe.js.map +1 -0
- package/dist/stack/path-resolve.d.ts +7 -0
- package/dist/stack/path-resolve.d.ts.map +1 -0
- package/dist/stack/path-resolve.js +37 -0
- package/dist/stack/path-resolve.js.map +1 -0
- package/dist/stack/registry.d.ts +11 -0
- package/dist/stack/registry.d.ts.map +1 -0
- package/dist/stack/registry.js +42 -0
- package/dist/stack/registry.js.map +1 -0
- package/dist/stack/sync.d.ts +20 -0
- package/dist/stack/sync.d.ts.map +1 -0
- package/dist/stack/sync.js +226 -0
- package/dist/stack/sync.js.map +1 -0
- package/dist/stack/types.d.ts +53 -0
- package/dist/stack/types.d.ts.map +1 -0
- package/dist/stack/types.js +2 -0
- package/dist/stack/types.js.map +1 -0
- package/dist/transcript.d.ts +14 -0
- package/dist/transcript.d.ts.map +1 -1
- package/dist/transcript.js +98 -1
- package/dist/transcript.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline BM25 inverted-index for the Agora marketplace catalog.
|
|
3
|
+
*
|
|
4
|
+
* ## Why BM25?
|
|
5
|
+
* BM25 (Best Match 25) is a probabilistic term-frequency / inverse-document-frequency
|
|
6
|
+
* ranking function that handles two key issues with naive TF-IDF:
|
|
7
|
+
* 1. Term-frequency saturation — a token appearing 100× in a doc is not 100× as
|
|
8
|
+
* relevant as one appearing once; BM25 saturates via (tf / (tf + k1*(1-b+b*dl/avgdl))).
|
|
9
|
+
* 2. Document-length normalization — long documents naturally accumulate more term
|
|
10
|
+
* hits; the `b` parameter (0–1) penalizes them proportionally.
|
|
11
|
+
*
|
|
12
|
+
* ## Field weighting via token repetition
|
|
13
|
+
* Rather than maintaining separate per-field inverted lists (which complicates the
|
|
14
|
+
* index structure), we inflate the term-frequency bag at index time:
|
|
15
|
+
* - name token contributes ×3 to TF (most signal: users search by name)
|
|
16
|
+
* - tags token contributes ×2 (curated semantic labels)
|
|
17
|
+
* - id token contributes ×2 (IDs are precise matches)
|
|
18
|
+
* - description/author/category ×1 (background context)
|
|
19
|
+
*
|
|
20
|
+
* This is equivalent to weighted field merging and requires zero extra complexity in
|
|
21
|
+
* the scoring loop.
|
|
22
|
+
*
|
|
23
|
+
* ## Fully offline, zero dependencies
|
|
24
|
+
* Matches Wave 1 "no external accounts / no network" constraint. The index is
|
|
25
|
+
* rebuilt in-memory from the item list; it never persists to disk and needs no
|
|
26
|
+
* embedding model or API key.
|
|
27
|
+
*/
|
|
28
|
+
export interface IndexableItem {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
author: string;
|
|
33
|
+
category: string;
|
|
34
|
+
tags: string[];
|
|
35
|
+
}
|
|
36
|
+
/** Per-document posting entry stored in the inverted list. */
|
|
37
|
+
interface Posting {
|
|
38
|
+
id: string;
|
|
39
|
+
tf: number;
|
|
40
|
+
}
|
|
41
|
+
export interface CatalogIndex {
|
|
42
|
+
/** postings[term] = array of {id, tf} for every doc containing that term */
|
|
43
|
+
postings: Map<string, Posting[]>;
|
|
44
|
+
/** df[term] = number of distinct documents containing that term */
|
|
45
|
+
df: Map<string, number>;
|
|
46
|
+
/** docLen[id] = sum of all weighted TF values for that document */
|
|
47
|
+
docLen: Map<string, number>;
|
|
48
|
+
/** average document length across all indexed documents */
|
|
49
|
+
avgDocLen: number;
|
|
50
|
+
/** total number of indexed documents */
|
|
51
|
+
N: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Combined English stopwords + intent/filler words.
|
|
55
|
+
* Intent words are stripped so that queries like
|
|
56
|
+
* "find a tool that talks to postgres" reduce to their content terms.
|
|
57
|
+
*/
|
|
58
|
+
export declare const STOPWORDS: Set<string>;
|
|
59
|
+
/**
|
|
60
|
+
* Tokenize text for indexing or querying:
|
|
61
|
+
* - lowercase
|
|
62
|
+
* - split on non-alphanumeric runs
|
|
63
|
+
* - drop tokens shorter than 2 characters
|
|
64
|
+
* - drop STOPWORDS
|
|
65
|
+
*
|
|
66
|
+
* Deterministic: identical input always yields identical output.
|
|
67
|
+
*/
|
|
68
|
+
export declare function tokenize(text: string): string[];
|
|
69
|
+
/**
|
|
70
|
+
* Developer-term synonym map. Applied ONLY during query tokenization, not at
|
|
71
|
+
* index time, so documents are indexed verbatim (no spurious term inflation).
|
|
72
|
+
*
|
|
73
|
+
* When a query token matches a key, its synonyms are added as additional query
|
|
74
|
+
* terms (deduped). This allows short aliases ("db", "k8s") to match their full
|
|
75
|
+
* forms in document text.
|
|
76
|
+
*/
|
|
77
|
+
export declare const SYNONYMS: Record<string, string[]>;
|
|
78
|
+
/**
|
|
79
|
+
* Tokenize a query string and expand synonyms.
|
|
80
|
+
* Returns a deduplicated array of tokens (original + synonym expansions).
|
|
81
|
+
*/
|
|
82
|
+
export declare function tokenizeQuery(text: string): string[];
|
|
83
|
+
/**
|
|
84
|
+
* Build a BM25 inverted index from a list of indexable items.
|
|
85
|
+
*
|
|
86
|
+
* Field weighting is achieved by repeating tokens in the TF bag:
|
|
87
|
+
* name ×3, tags ×2, id ×2, description ×1, author ×1, category ×1
|
|
88
|
+
*/
|
|
89
|
+
export declare function buildIndex(items: IndexableItem[]): CatalogIndex;
|
|
90
|
+
/**
|
|
91
|
+
* Search the index with BM25 scoring.
|
|
92
|
+
*
|
|
93
|
+
* @param index - built by buildIndex()
|
|
94
|
+
* @param query - raw query string (will be tokenized + synonym-expanded)
|
|
95
|
+
* @param opts - BM25 hyperparameters (defaults: k1=1.5, b=0.75)
|
|
96
|
+
* @returns - scored results sorted by score descending; empty array if
|
|
97
|
+
* query is blank or no documents match
|
|
98
|
+
*/
|
|
99
|
+
export declare function searchIndex(index: CatalogIndex, query: string, opts?: {
|
|
100
|
+
k1?: number;
|
|
101
|
+
b?: number;
|
|
102
|
+
}): {
|
|
103
|
+
id: string;
|
|
104
|
+
score: number;
|
|
105
|
+
}[];
|
|
106
|
+
export {};
|
|
107
|
+
//# sourceMappingURL=catalog-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-index.d.ts","sourceRoot":"","sources":["../../src/search/catalog-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,8DAA8D;AAC9D,UAAU,OAAO;IACf,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IACjC,mEAAmE;IACnE,EAAE,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxB,mEAAmE;IACnE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,CAAC,EAAE,MAAM,CAAC;CACX;AAID;;;;GAIG;AACH,eAAO,MAAM,SAAS,EAAE,GAAG,CAAC,MAAM,CAiFhC,CAAC;AAIH;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAK/C;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAsB7C,CAAC;AAEF;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAYpD;AAID;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,YAAY,CAqD/D;AAID;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,CAoCjC"}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline BM25 inverted-index for the Agora marketplace catalog.
|
|
3
|
+
*
|
|
4
|
+
* ## Why BM25?
|
|
5
|
+
* BM25 (Best Match 25) is a probabilistic term-frequency / inverse-document-frequency
|
|
6
|
+
* ranking function that handles two key issues with naive TF-IDF:
|
|
7
|
+
* 1. Term-frequency saturation — a token appearing 100× in a doc is not 100× as
|
|
8
|
+
* relevant as one appearing once; BM25 saturates via (tf / (tf + k1*(1-b+b*dl/avgdl))).
|
|
9
|
+
* 2. Document-length normalization — long documents naturally accumulate more term
|
|
10
|
+
* hits; the `b` parameter (0–1) penalizes them proportionally.
|
|
11
|
+
*
|
|
12
|
+
* ## Field weighting via token repetition
|
|
13
|
+
* Rather than maintaining separate per-field inverted lists (which complicates the
|
|
14
|
+
* index structure), we inflate the term-frequency bag at index time:
|
|
15
|
+
* - name token contributes ×3 to TF (most signal: users search by name)
|
|
16
|
+
* - tags token contributes ×2 (curated semantic labels)
|
|
17
|
+
* - id token contributes ×2 (IDs are precise matches)
|
|
18
|
+
* - description/author/category ×1 (background context)
|
|
19
|
+
*
|
|
20
|
+
* This is equivalent to weighted field merging and requires zero extra complexity in
|
|
21
|
+
* the scoring loop.
|
|
22
|
+
*
|
|
23
|
+
* ## Fully offline, zero dependencies
|
|
24
|
+
* Matches Wave 1 "no external accounts / no network" constraint. The index is
|
|
25
|
+
* rebuilt in-memory from the item list; it never persists to disk and needs no
|
|
26
|
+
* embedding model or API key.
|
|
27
|
+
*/
|
|
28
|
+
// ── Stopwords ─────────────────────────────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* Combined English stopwords + intent/filler words.
|
|
31
|
+
* Intent words are stripped so that queries like
|
|
32
|
+
* "find a tool that talks to postgres" reduce to their content terms.
|
|
33
|
+
*/
|
|
34
|
+
export const STOPWORDS = new Set([
|
|
35
|
+
// English function words
|
|
36
|
+
'the',
|
|
37
|
+
'a',
|
|
38
|
+
'an',
|
|
39
|
+
'to',
|
|
40
|
+
'of',
|
|
41
|
+
'for',
|
|
42
|
+
'and',
|
|
43
|
+
'or',
|
|
44
|
+
'with',
|
|
45
|
+
'that',
|
|
46
|
+
'this',
|
|
47
|
+
'my',
|
|
48
|
+
'i',
|
|
49
|
+
'in',
|
|
50
|
+
'on',
|
|
51
|
+
'is',
|
|
52
|
+
'are',
|
|
53
|
+
'it',
|
|
54
|
+
'by',
|
|
55
|
+
'at',
|
|
56
|
+
'as',
|
|
57
|
+
'be',
|
|
58
|
+
'was',
|
|
59
|
+
'has',
|
|
60
|
+
'have',
|
|
61
|
+
'not',
|
|
62
|
+
'its',
|
|
63
|
+
'from',
|
|
64
|
+
'into',
|
|
65
|
+
'than',
|
|
66
|
+
'but',
|
|
67
|
+
'about',
|
|
68
|
+
// Intent / filler words common in natural-language queries
|
|
69
|
+
'find',
|
|
70
|
+
'search',
|
|
71
|
+
'show',
|
|
72
|
+
'get',
|
|
73
|
+
'need',
|
|
74
|
+
'want',
|
|
75
|
+
'looking',
|
|
76
|
+
'something',
|
|
77
|
+
'anything',
|
|
78
|
+
'tool',
|
|
79
|
+
'tools',
|
|
80
|
+
'thing',
|
|
81
|
+
'things',
|
|
82
|
+
'does',
|
|
83
|
+
'do',
|
|
84
|
+
'help',
|
|
85
|
+
'please',
|
|
86
|
+
'can',
|
|
87
|
+
'give',
|
|
88
|
+
'use',
|
|
89
|
+
'using',
|
|
90
|
+
'used',
|
|
91
|
+
'like',
|
|
92
|
+
'also',
|
|
93
|
+
'which',
|
|
94
|
+
'how',
|
|
95
|
+
'what',
|
|
96
|
+
'where',
|
|
97
|
+
'when',
|
|
98
|
+
'why',
|
|
99
|
+
'who',
|
|
100
|
+
'some',
|
|
101
|
+
'any',
|
|
102
|
+
'all',
|
|
103
|
+
'talks',
|
|
104
|
+
'talk',
|
|
105
|
+
'connect',
|
|
106
|
+
'connects',
|
|
107
|
+
'access',
|
|
108
|
+
'accesses',
|
|
109
|
+
'let',
|
|
110
|
+
'lets',
|
|
111
|
+
'work',
|
|
112
|
+
'works',
|
|
113
|
+
'support',
|
|
114
|
+
'supports'
|
|
115
|
+
]);
|
|
116
|
+
// ── Tokenizer ─────────────────────────────────────────────────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* Tokenize text for indexing or querying:
|
|
119
|
+
* - lowercase
|
|
120
|
+
* - split on non-alphanumeric runs
|
|
121
|
+
* - drop tokens shorter than 2 characters
|
|
122
|
+
* - drop STOPWORDS
|
|
123
|
+
*
|
|
124
|
+
* Deterministic: identical input always yields identical output.
|
|
125
|
+
*/
|
|
126
|
+
export function tokenize(text) {
|
|
127
|
+
return text
|
|
128
|
+
.toLowerCase()
|
|
129
|
+
.split(/[^a-z0-9]+/)
|
|
130
|
+
.filter((t) => t.length >= 2 && !STOPWORDS.has(t));
|
|
131
|
+
}
|
|
132
|
+
// ── Synonyms (query-side expansion only) ─────────────────────────────────────
|
|
133
|
+
/**
|
|
134
|
+
* Developer-term synonym map. Applied ONLY during query tokenization, not at
|
|
135
|
+
* index time, so documents are indexed verbatim (no spurious term inflation).
|
|
136
|
+
*
|
|
137
|
+
* When a query token matches a key, its synonyms are added as additional query
|
|
138
|
+
* terms (deduped). This allows short aliases ("db", "k8s") to match their full
|
|
139
|
+
* forms in document text.
|
|
140
|
+
*/
|
|
141
|
+
export const SYNONYMS = {
|
|
142
|
+
db: ['database'],
|
|
143
|
+
k8s: ['kubernetes'],
|
|
144
|
+
k8: ['kubernetes'],
|
|
145
|
+
postgres: ['postgresql', 'database'],
|
|
146
|
+
pg: ['postgresql', 'database'],
|
|
147
|
+
js: ['javascript'],
|
|
148
|
+
ts: ['typescript'],
|
|
149
|
+
ai: ['llm'],
|
|
150
|
+
auth: ['authentication'],
|
|
151
|
+
vcs: ['git'],
|
|
152
|
+
ml: ['machine', 'learning'],
|
|
153
|
+
api: ['integration'],
|
|
154
|
+
cli: ['command'],
|
|
155
|
+
ui: ['interface'],
|
|
156
|
+
gh: ['github'],
|
|
157
|
+
gl: ['gitlab'],
|
|
158
|
+
aws: ['amazon'],
|
|
159
|
+
gcp: ['google'],
|
|
160
|
+
kv: ['cache'],
|
|
161
|
+
nosql: ['database'],
|
|
162
|
+
sql: ['database']
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Tokenize a query string and expand synonyms.
|
|
166
|
+
* Returns a deduplicated array of tokens (original + synonym expansions).
|
|
167
|
+
*/
|
|
168
|
+
export function tokenizeQuery(text) {
|
|
169
|
+
const base = tokenize(text);
|
|
170
|
+
const expanded = new Set(base);
|
|
171
|
+
for (const token of base) {
|
|
172
|
+
const syns = SYNONYMS[token];
|
|
173
|
+
if (syns) {
|
|
174
|
+
for (const syn of syns) {
|
|
175
|
+
expanded.add(syn);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return Array.from(expanded);
|
|
180
|
+
}
|
|
181
|
+
// ── Index builder ─────────────────────────────────────────────────────────────
|
|
182
|
+
/**
|
|
183
|
+
* Build a BM25 inverted index from a list of indexable items.
|
|
184
|
+
*
|
|
185
|
+
* Field weighting is achieved by repeating tokens in the TF bag:
|
|
186
|
+
* name ×3, tags ×2, id ×2, description ×1, author ×1, category ×1
|
|
187
|
+
*/
|
|
188
|
+
export function buildIndex(items) {
|
|
189
|
+
const postings = new Map();
|
|
190
|
+
const df = new Map();
|
|
191
|
+
const docLen = new Map();
|
|
192
|
+
let totalLen = 0;
|
|
193
|
+
for (const item of items) {
|
|
194
|
+
// Build weighted TF bag for this document
|
|
195
|
+
const tfBag = new Map();
|
|
196
|
+
function addTokens(text, weight) {
|
|
197
|
+
for (const token of tokenize(text)) {
|
|
198
|
+
tfBag.set(token, (tfBag.get(token) ?? 0) + weight);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
addTokens(item.name, 3);
|
|
202
|
+
for (const tag of item.tags) {
|
|
203
|
+
addTokens(tag, 2);
|
|
204
|
+
}
|
|
205
|
+
addTokens(item.id, 2);
|
|
206
|
+
addTokens(item.description, 1);
|
|
207
|
+
addTokens(item.author, 1);
|
|
208
|
+
addTokens(item.category, 1);
|
|
209
|
+
// Document length = sum of weighted TFs
|
|
210
|
+
let dl = 0;
|
|
211
|
+
for (const tf of tfBag.values()) {
|
|
212
|
+
dl += tf;
|
|
213
|
+
}
|
|
214
|
+
docLen.set(item.id, dl);
|
|
215
|
+
totalLen += dl;
|
|
216
|
+
// Update postings and DF
|
|
217
|
+
for (const [term, tf] of tfBag.entries()) {
|
|
218
|
+
// DF: count this document once per term
|
|
219
|
+
df.set(term, (df.get(term) ?? 0) + 1);
|
|
220
|
+
// Postings list
|
|
221
|
+
let list = postings.get(term);
|
|
222
|
+
if (!list) {
|
|
223
|
+
list = [];
|
|
224
|
+
postings.set(term, list);
|
|
225
|
+
}
|
|
226
|
+
list.push({ id: item.id, tf });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const N = items.length;
|
|
230
|
+
const avgDocLen = N > 0 ? totalLen / N : 1;
|
|
231
|
+
return { postings, df, docLen, avgDocLen, N };
|
|
232
|
+
}
|
|
233
|
+
// ── BM25 scorer ───────────────────────────────────────────────────────────────
|
|
234
|
+
/**
|
|
235
|
+
* Search the index with BM25 scoring.
|
|
236
|
+
*
|
|
237
|
+
* @param index - built by buildIndex()
|
|
238
|
+
* @param query - raw query string (will be tokenized + synonym-expanded)
|
|
239
|
+
* @param opts - BM25 hyperparameters (defaults: k1=1.5, b=0.75)
|
|
240
|
+
* @returns - scored results sorted by score descending; empty array if
|
|
241
|
+
* query is blank or no documents match
|
|
242
|
+
*/
|
|
243
|
+
export function searchIndex(index, query, opts) {
|
|
244
|
+
if (!query || !query.trim())
|
|
245
|
+
return [];
|
|
246
|
+
const k1 = opts?.k1 ?? 1.5;
|
|
247
|
+
const b = opts?.b ?? 0.75;
|
|
248
|
+
const { postings, df, docLen, avgDocLen, N } = index;
|
|
249
|
+
const queryTokens = tokenizeQuery(query);
|
|
250
|
+
if (queryTokens.length === 0)
|
|
251
|
+
return [];
|
|
252
|
+
// Accumulate scores per document
|
|
253
|
+
const scores = new Map();
|
|
254
|
+
for (const term of queryTokens) {
|
|
255
|
+
const list = postings.get(term);
|
|
256
|
+
if (!list)
|
|
257
|
+
continue;
|
|
258
|
+
const termDf = df.get(term) ?? 0;
|
|
259
|
+
// IDF with smoothing (Robertson-Sparck Jones):
|
|
260
|
+
// idf = log((N - df + 0.5) / (df + 0.5) + 1)
|
|
261
|
+
const idf = Math.log((N - termDf + 0.5) / (termDf + 0.5) + 1);
|
|
262
|
+
for (const { id, tf } of list) {
|
|
263
|
+
const dl = docLen.get(id) ?? avgDocLen;
|
|
264
|
+
const norm = tf / (tf + k1 * (1 - b + b * (dl / avgDocLen)));
|
|
265
|
+
const contribution = idf * norm;
|
|
266
|
+
scores.set(id, (scores.get(id) ?? 0) + contribution);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (scores.size === 0)
|
|
270
|
+
return [];
|
|
271
|
+
return Array.from(scores.entries())
|
|
272
|
+
.filter(([, score]) => score > 0)
|
|
273
|
+
.map(([id, score]) => ({ id, score }))
|
|
274
|
+
.sort((a, b) => b.score - a.score);
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=catalog-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-index.js","sourceRoot":"","sources":["../../src/search/catalog-index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-code.d.ts","sourceRoot":"","sources":["../../../src/stack/adapters/claude-code.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAKV,WAAW,EAEZ,MAAM,aAAa,CAAC;AA4IrB,eAAO,MAAM,iBAAiB,EAAE,WA+K/B,CAAC"}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { atomicWriteFile } from '../../atomic-write.js';
|
|
5
|
+
function resolveHome(opts) {
|
|
6
|
+
return opts.home ?? homedir();
|
|
7
|
+
}
|
|
8
|
+
function resolveCwd(opts) {
|
|
9
|
+
return opts.cwd ?? process.cwd();
|
|
10
|
+
}
|
|
11
|
+
const REMOTE_TYPES = new Set(['sse', 'http', 'streamable-http']);
|
|
12
|
+
const LOCAL_TYPES = new Set(['stdio']);
|
|
13
|
+
/** Fix 2: robust transport detection */
|
|
14
|
+
function isRemoteEntry(entry) {
|
|
15
|
+
const t = typeof entry['type'] === 'string' ? entry['type'] : undefined;
|
|
16
|
+
const tr = typeof entry['transport'] === 'string' ? entry['transport'] : undefined;
|
|
17
|
+
const hasUrl = 'url' in entry;
|
|
18
|
+
const hasCommand = 'command' in entry;
|
|
19
|
+
if (t && REMOTE_TYPES.has(t))
|
|
20
|
+
return true;
|
|
21
|
+
if (tr && REMOTE_TYPES.has(tr))
|
|
22
|
+
return true;
|
|
23
|
+
if (t && LOCAL_TYPES.has(t))
|
|
24
|
+
return false;
|
|
25
|
+
if (tr && LOCAL_TYPES.has(tr))
|
|
26
|
+
return false;
|
|
27
|
+
if (hasCommand)
|
|
28
|
+
return false;
|
|
29
|
+
return hasUrl;
|
|
30
|
+
}
|
|
31
|
+
function parseEntry(name, entry, tool, scope, configPath) {
|
|
32
|
+
if (isRemoteEntry(entry)) {
|
|
33
|
+
return {
|
|
34
|
+
name,
|
|
35
|
+
tool,
|
|
36
|
+
scope,
|
|
37
|
+
configPath,
|
|
38
|
+
transport: 'remote',
|
|
39
|
+
url: typeof entry['url'] === 'string' ? entry['url'] : undefined,
|
|
40
|
+
enabled: true,
|
|
41
|
+
raw: entry
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const cmd = typeof entry['command'] === 'string' ? entry['command'] : '';
|
|
45
|
+
const args = Array.isArray(entry['args']) ? entry['args'] : [];
|
|
46
|
+
const env = typeof entry['env'] === 'object' && entry['env'] !== null
|
|
47
|
+
? entry['env']
|
|
48
|
+
: undefined;
|
|
49
|
+
const argv = [cmd, ...args];
|
|
50
|
+
return {
|
|
51
|
+
name,
|
|
52
|
+
tool,
|
|
53
|
+
scope,
|
|
54
|
+
configPath,
|
|
55
|
+
transport: 'local',
|
|
56
|
+
command: argv,
|
|
57
|
+
env,
|
|
58
|
+
enabled: true,
|
|
59
|
+
raw: entry
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function readMcpServers(obj) {
|
|
63
|
+
if (typeof obj !== 'object' || obj === null)
|
|
64
|
+
return null;
|
|
65
|
+
const map = obj;
|
|
66
|
+
const result = [];
|
|
67
|
+
for (const [k, v] of Object.entries(map)) {
|
|
68
|
+
if (typeof v === 'object' && v !== null) {
|
|
69
|
+
result.push([k, v]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
function parseJson(path) {
|
|
75
|
+
try {
|
|
76
|
+
const content = readFileSync(path, 'utf8');
|
|
77
|
+
return JSON.parse(content);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Fix 1: Merge a DesiredServer into an existing entry, preserving unknown keys.
|
|
85
|
+
* Returns null when the server should not be written (disabled).
|
|
86
|
+
*/
|
|
87
|
+
function mergeEntry(ds, existing) {
|
|
88
|
+
// claude-code has no enabled field — skip disabled servers
|
|
89
|
+
if (ds.enabled === false)
|
|
90
|
+
return null;
|
|
91
|
+
const base = existing !== undefined ? { ...existing } : {};
|
|
92
|
+
if (ds.url) {
|
|
93
|
+
// REMOTE: set url; preserve existing sse/http type if present; remove local keys
|
|
94
|
+
base['url'] = ds.url;
|
|
95
|
+
delete base['command'];
|
|
96
|
+
delete base['args'];
|
|
97
|
+
delete base['env'];
|
|
98
|
+
const t = typeof base['type'] === 'string' ? base['type'] : undefined;
|
|
99
|
+
if (t && LOCAL_TYPES.has(t))
|
|
100
|
+
delete base['type'];
|
|
101
|
+
const tr = typeof base['transport'] === 'string' ? base['transport'] : undefined;
|
|
102
|
+
if (tr && LOCAL_TYPES.has(tr))
|
|
103
|
+
delete base['transport'];
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// LOCAL: set command/args/env; remove remote keys
|
|
107
|
+
const [cmd, ...args] = ds.command ?? [];
|
|
108
|
+
base['command'] = cmd ?? '';
|
|
109
|
+
if (args.length > 0) {
|
|
110
|
+
base['args'] = args;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
delete base['args'];
|
|
114
|
+
}
|
|
115
|
+
if (ds.env && Object.keys(ds.env).length > 0) {
|
|
116
|
+
base['env'] = ds.env;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
delete base['env'];
|
|
120
|
+
}
|
|
121
|
+
delete base['url'];
|
|
122
|
+
const t = typeof base['type'] === 'string' ? base['type'] : undefined;
|
|
123
|
+
if (t && REMOTE_TYPES.has(t))
|
|
124
|
+
delete base['type'];
|
|
125
|
+
const tr = typeof base['transport'] === 'string' ? base['transport'] : undefined;
|
|
126
|
+
if (tr && REMOTE_TYPES.has(tr))
|
|
127
|
+
delete base['transport'];
|
|
128
|
+
}
|
|
129
|
+
return base;
|
|
130
|
+
}
|
|
131
|
+
function entriesEqual(a, b) {
|
|
132
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
133
|
+
}
|
|
134
|
+
export const claudeCodeAdapter = {
|
|
135
|
+
id: 'claude-code',
|
|
136
|
+
displayName: 'Claude Code',
|
|
137
|
+
locations(opts) {
|
|
138
|
+
const cwd = resolveCwd(opts);
|
|
139
|
+
const home = resolveHome(opts);
|
|
140
|
+
return [
|
|
141
|
+
{ path: join(cwd, '.mcp.json'), scope: 'project' },
|
|
142
|
+
{ path: join(home, '.claude.json'), scope: 'user' }
|
|
143
|
+
];
|
|
144
|
+
},
|
|
145
|
+
writeLocation(opts, scope) {
|
|
146
|
+
const cwd = resolveCwd(opts);
|
|
147
|
+
const home = resolveHome(opts);
|
|
148
|
+
if (scope === 'project') {
|
|
149
|
+
return { path: join(cwd, '.mcp.json'), scope: 'project' };
|
|
150
|
+
}
|
|
151
|
+
return { path: join(home, '.claude.json'), scope: 'user' };
|
|
152
|
+
},
|
|
153
|
+
writeServers(location, desired, opts) {
|
|
154
|
+
const filePath = location.path;
|
|
155
|
+
// Read existing file
|
|
156
|
+
let doc = {};
|
|
157
|
+
if (existsSync(filePath)) {
|
|
158
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
159
|
+
let parsed;
|
|
160
|
+
try {
|
|
161
|
+
parsed = JSON.parse(raw);
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
throw new Error(`claude-code config at ${filePath} is not valid JSON — refusing to overwrite: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
|
|
165
|
+
}
|
|
166
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
167
|
+
doc = parsed;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Preserve all sibling keys; only mutate mcpServers
|
|
171
|
+
const result = { ...doc };
|
|
172
|
+
const existingMcp = typeof doc['mcpServers'] === 'object' && doc['mcpServers'] !== null
|
|
173
|
+
? { ...doc['mcpServers'] }
|
|
174
|
+
: {};
|
|
175
|
+
const added = [];
|
|
176
|
+
const updated = [];
|
|
177
|
+
const removed = [];
|
|
178
|
+
const skippedDisabled = [];
|
|
179
|
+
const desiredMap = new Map(desired.map((d) => [d.name, d]));
|
|
180
|
+
const newMcp = opts.prune ? {} : { ...existingMcp };
|
|
181
|
+
for (const ds of desired) {
|
|
182
|
+
const existingEntry = typeof existingMcp[ds.name] === 'object' && existingMcp[ds.name] !== null
|
|
183
|
+
? existingMcp[ds.name]
|
|
184
|
+
: undefined;
|
|
185
|
+
const mergedEntry = mergeEntry(ds, existingEntry);
|
|
186
|
+
if (mergedEntry === null) {
|
|
187
|
+
// disabled — skip writing
|
|
188
|
+
skippedDisabled.push(ds.name);
|
|
189
|
+
if (opts.prune && existingEntry !== undefined) {
|
|
190
|
+
delete newMcp[ds.name];
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (existingEntry === undefined) {
|
|
195
|
+
added.push(ds.name);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
if (!entriesEqual(existingEntry, mergedEntry)) {
|
|
199
|
+
updated.push(ds.name);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
newMcp[ds.name] = mergedEntry;
|
|
203
|
+
}
|
|
204
|
+
if (opts.prune) {
|
|
205
|
+
for (const name of Object.keys(existingMcp)) {
|
|
206
|
+
if (!desiredMap.has(name) && !skippedDisabled.includes(name)) {
|
|
207
|
+
removed.push(name);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
result['mcpServers'] = newMcp;
|
|
212
|
+
atomicWriteFile(filePath, JSON.stringify(result, null, 2) + '\n', 0o644);
|
|
213
|
+
return { added, updated, removed };
|
|
214
|
+
},
|
|
215
|
+
readServers(opts) {
|
|
216
|
+
const cwd = resolveCwd(opts);
|
|
217
|
+
const home = resolveHome(opts);
|
|
218
|
+
const servers = [];
|
|
219
|
+
// --- Project file: <cwd>/.mcp.json ---
|
|
220
|
+
const projectPath = join(cwd, '.mcp.json');
|
|
221
|
+
if (existsSync(projectPath)) {
|
|
222
|
+
const parsed = parseJson(projectPath);
|
|
223
|
+
if (parsed !== null) {
|
|
224
|
+
const doc = parsed;
|
|
225
|
+
const entries = readMcpServers(doc['mcpServers']);
|
|
226
|
+
if (entries) {
|
|
227
|
+
for (const [name, entry] of entries) {
|
|
228
|
+
servers.push(parseEntry(name, entry, 'claude-code', 'project', projectPath));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// --- User file: ~/.claude.json ---
|
|
234
|
+
const userPath = join(home, '.claude.json');
|
|
235
|
+
if (existsSync(userPath)) {
|
|
236
|
+
const parsed = parseJson(userPath);
|
|
237
|
+
if (parsed !== null) {
|
|
238
|
+
const doc = parsed;
|
|
239
|
+
// Top-level mcpServers (scope: user)
|
|
240
|
+
const topEntries = readMcpServers(doc['mcpServers']);
|
|
241
|
+
if (topEntries) {
|
|
242
|
+
for (const [name, entry] of topEntries) {
|
|
243
|
+
servers.push(parseEntry(name, entry, 'claude-code', 'user', userPath));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// projects[<absCwd>].mcpServers (scope: project)
|
|
247
|
+
const projects = doc['projects'];
|
|
248
|
+
if (typeof projects === 'object' && projects !== null) {
|
|
249
|
+
const projectsMap = projects;
|
|
250
|
+
const cwdEntry = projectsMap[cwd];
|
|
251
|
+
if (typeof cwdEntry === 'object' && cwdEntry !== null) {
|
|
252
|
+
const projectDoc = cwdEntry;
|
|
253
|
+
const projEntries = readMcpServers(projectDoc['mcpServers']);
|
|
254
|
+
if (projEntries) {
|
|
255
|
+
for (const [name, entry] of projEntries) {
|
|
256
|
+
// Dedupe: skip if same name+command already added from top-level
|
|
257
|
+
const isDupe = servers.some((s) => {
|
|
258
|
+
if (s.configPath !== userPath)
|
|
259
|
+
return false;
|
|
260
|
+
if (s.name !== name)
|
|
261
|
+
return false;
|
|
262
|
+
const newArgv = !isRemoteEntry(entry)
|
|
263
|
+
? [
|
|
264
|
+
typeof entry['command'] === 'string' ? entry['command'] : '',
|
|
265
|
+
...(Array.isArray(entry['args']) ? entry['args'] : [])
|
|
266
|
+
]
|
|
267
|
+
: undefined;
|
|
268
|
+
return JSON.stringify(s.command) === JSON.stringify(newArgv);
|
|
269
|
+
});
|
|
270
|
+
if (!isDupe) {
|
|
271
|
+
servers.push(parseEntry(name, entry, 'claude-code', 'project', userPath));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return servers;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
//# sourceMappingURL=claude-code.js.map
|