opencode-agora 0.4.1 → 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.
Files changed (200) hide show
  1. package/README.md +54 -6
  2. package/dist/cli/app.d.ts.map +1 -1
  3. package/dist/cli/app.js +18 -0
  4. package/dist/cli/app.js.map +1 -1
  5. package/dist/cli/commands/browse.d.ts.map +1 -1
  6. package/dist/cli/commands/browse.js +1 -5
  7. package/dist/cli/commands/browse.js.map +1 -1
  8. package/dist/cli/commands/capabilities.d.ts +3 -0
  9. package/dist/cli/commands/capabilities.d.ts.map +1 -0
  10. package/dist/cli/commands/capabilities.js +146 -0
  11. package/dist/cli/commands/capabilities.js.map +1 -0
  12. package/dist/cli/commands/community.d.ts.map +1 -1
  13. package/dist/cli/commands/community.js.map +1 -1
  14. package/dist/cli/commands/curate.d.ts +9 -0
  15. package/dist/cli/commands/curate.d.ts.map +1 -0
  16. package/dist/cli/commands/curate.js +62 -0
  17. package/dist/cli/commands/curate.js.map +1 -0
  18. package/dist/cli/commands/doctor.d.ts +3 -0
  19. package/dist/cli/commands/doctor.d.ts.map +1 -0
  20. package/dist/cli/commands/doctor.js +77 -0
  21. package/dist/cli/commands/doctor.js.map +1 -0
  22. package/dist/cli/commands/export.d.ts.map +1 -1
  23. package/dist/cli/commands/export.js +5 -3
  24. package/dist/cli/commands/export.js.map +1 -1
  25. package/dist/cli/commands/freeze.d.ts +3 -0
  26. package/dist/cli/commands/freeze.d.ts.map +1 -0
  27. package/dist/cli/commands/freeze.js +62 -0
  28. package/dist/cli/commands/freeze.js.map +1 -0
  29. package/dist/cli/commands/init.d.ts.map +1 -1
  30. package/dist/cli/commands/init.js +5 -1
  31. package/dist/cli/commands/init.js.map +1 -1
  32. package/dist/cli/commands/installed.d.ts +3 -0
  33. package/dist/cli/commands/installed.d.ts.map +1 -0
  34. package/dist/cli/commands/installed.js +82 -0
  35. package/dist/cli/commands/installed.js.map +1 -0
  36. package/dist/cli/commands/marketplace.d.ts.map +1 -1
  37. package/dist/cli/commands/marketplace.js.map +1 -1
  38. package/dist/cli/commands/notify.d.ts.map +1 -1
  39. package/dist/cli/commands/notify.js.map +1 -1
  40. package/dist/cli/commands/operations.d.ts.map +1 -1
  41. package/dist/cli/commands/operations.js +103 -5
  42. package/dist/cli/commands/operations.js.map +1 -1
  43. package/dist/cli/commands/outdated.d.ts +3 -0
  44. package/dist/cli/commands/outdated.d.ts.map +1 -0
  45. package/dist/cli/commands/outdated.js +48 -0
  46. package/dist/cli/commands/outdated.js.map +1 -0
  47. package/dist/cli/commands/ping.js +2 -1
  48. package/dist/cli/commands/ping.js.map +1 -1
  49. package/dist/cli/commands/scan.d.ts +3 -0
  50. package/dist/cli/commands/scan.d.ts.map +1 -0
  51. package/dist/cli/commands/scan.js +35 -0
  52. package/dist/cli/commands/scan.js.map +1 -0
  53. package/dist/cli/commands/sync.d.ts +3 -0
  54. package/dist/cli/commands/sync.d.ts.map +1 -0
  55. package/dist/cli/commands/sync.js +172 -0
  56. package/dist/cli/commands/sync.js.map +1 -0
  57. package/dist/cli/commands/today.d.ts.map +1 -1
  58. package/dist/cli/commands/today.js +11 -3
  59. package/dist/cli/commands/today.js.map +1 -1
  60. package/dist/cli/commands/try.d.ts +3 -0
  61. package/dist/cli/commands/try.d.ts.map +1 -0
  62. package/dist/cli/commands/try.js +157 -0
  63. package/dist/cli/commands/try.js.map +1 -0
  64. package/dist/cli/commands/watch.d.ts.map +1 -1
  65. package/dist/cli/commands/watch.js.map +1 -1
  66. package/dist/cli/commands/welcome.d.ts.map +1 -1
  67. package/dist/cli/commands/welcome.js +6 -27
  68. package/dist/cli/commands/welcome.js.map +1 -1
  69. package/dist/cli/commands-meta.d.ts +1 -1
  70. package/dist/cli/commands-meta.d.ts.map +1 -1
  71. package/dist/cli/commands-meta.js +242 -12
  72. package/dist/cli/commands-meta.js.map +1 -1
  73. package/dist/cli/completions-gen.d.ts.map +1 -1
  74. package/dist/cli/completions-gen.js +130 -52
  75. package/dist/cli/completions-gen.js.map +1 -1
  76. package/dist/cli/flags.d.ts.map +1 -1
  77. package/dist/cli/flags.js +7 -0
  78. package/dist/cli/flags.js.map +1 -1
  79. package/dist/cli/format.js +1 -1
  80. package/dist/cli/format.js.map +1 -1
  81. package/dist/cli/helpers.d.ts.map +1 -1
  82. package/dist/cli/helpers.js.map +1 -1
  83. package/dist/cli/mcp-server.d.ts +12 -1
  84. package/dist/cli/mcp-server.d.ts.map +1 -1
  85. package/dist/cli/mcp-server.js +292 -2
  86. package/dist/cli/mcp-server.js.map +1 -1
  87. package/dist/cli/pages/community.d.ts.map +1 -1
  88. package/dist/cli/pages/community.js +5 -8
  89. package/dist/cli/pages/community.js.map +1 -1
  90. package/dist/cli/pages/home.d.ts.map +1 -1
  91. package/dist/cli/pages/home.js +22 -8
  92. package/dist/cli/pages/home.js.map +1 -1
  93. package/dist/cli/pages/marketplace.d.ts.map +1 -1
  94. package/dist/cli/pages/marketplace.js +68 -7
  95. package/dist/cli/pages/marketplace.js.map +1 -1
  96. package/dist/cli/pages/news.d.ts.map +1 -1
  97. package/dist/cli/pages/news.js +2 -2
  98. package/dist/cli/pages/news.js.map +1 -1
  99. package/dist/cli/pages/stack.d.ts +3 -0
  100. package/dist/cli/pages/stack.d.ts.map +1 -0
  101. package/dist/cli/pages/stack.js +373 -0
  102. package/dist/cli/pages/stack.js.map +1 -0
  103. package/dist/cli/pages/types.d.ts +1 -1
  104. package/dist/cli/pages/types.d.ts.map +1 -1
  105. package/dist/cli/shell.d.ts +1 -1
  106. package/dist/cli/shell.d.ts.map +1 -1
  107. package/dist/cli/shell.js +70 -4
  108. package/dist/cli/shell.js.map +1 -1
  109. package/dist/cli/tui.d.ts.map +1 -1
  110. package/dist/cli/tui.js +14 -4
  111. package/dist/cli/tui.js.map +1 -1
  112. package/dist/community/client.d.ts.map +1 -1
  113. package/dist/community/client.js +3 -1
  114. package/dist/community/client.js.map +1 -1
  115. package/dist/curator/index.d.ts +95 -0
  116. package/dist/curator/index.d.ts.map +1 -0
  117. package/dist/curator/index.js +446 -0
  118. package/dist/curator/index.js.map +1 -0
  119. package/dist/format.d.ts +0 -2
  120. package/dist/format.d.ts.map +1 -1
  121. package/dist/format.js +0 -2
  122. package/dist/format.js.map +1 -1
  123. package/dist/hubs/enrichment.d.ts.map +1 -1
  124. package/dist/hubs/enrichment.js.map +1 -1
  125. package/dist/index.d.ts.map +1 -1
  126. package/dist/index.js +3 -1
  127. package/dist/index.js.map +1 -1
  128. package/dist/live.js +7 -1
  129. package/dist/live.js.map +1 -1
  130. package/dist/marketplace.d.ts +2 -1
  131. package/dist/marketplace.d.ts.map +1 -1
  132. package/dist/marketplace.js +86 -17
  133. package/dist/marketplace.js.map +1 -1
  134. package/dist/outdated.d.ts +24 -0
  135. package/dist/outdated.d.ts.map +1 -0
  136. package/dist/outdated.js +74 -0
  137. package/dist/outdated.js.map +1 -0
  138. package/dist/scan.d.ts +26 -0
  139. package/dist/scan.d.ts.map +1 -0
  140. package/dist/scan.js +247 -0
  141. package/dist/scan.js.map +1 -0
  142. package/dist/search/catalog-index.d.ts +107 -0
  143. package/dist/search/catalog-index.d.ts.map +1 -0
  144. package/dist/search/catalog-index.js +276 -0
  145. package/dist/search/catalog-index.js.map +1 -0
  146. package/dist/stack/adapters/claude-code.d.ts +3 -0
  147. package/dist/stack/adapters/claude-code.d.ts.map +1 -0
  148. package/dist/stack/adapters/claude-code.js +282 -0
  149. package/dist/stack/adapters/claude-code.js.map +1 -0
  150. package/dist/stack/adapters/cursor.d.ts +3 -0
  151. package/dist/stack/adapters/cursor.d.ts.map +1 -0
  152. package/dist/stack/adapters/cursor.js +232 -0
  153. package/dist/stack/adapters/cursor.js.map +1 -0
  154. package/dist/stack/adapters/opencode.d.ts +3 -0
  155. package/dist/stack/adapters/opencode.d.ts.map +1 -0
  156. package/dist/stack/adapters/opencode.js +177 -0
  157. package/dist/stack/adapters/opencode.js.map +1 -0
  158. package/dist/stack/adapters/windsurf.d.ts +3 -0
  159. package/dist/stack/adapters/windsurf.d.ts.map +1 -0
  160. package/dist/stack/adapters/windsurf.js +218 -0
  161. package/dist/stack/adapters/windsurf.js.map +1 -0
  162. package/dist/stack/capability-cache.d.ts +19 -0
  163. package/dist/stack/capability-cache.d.ts.map +1 -0
  164. package/dist/stack/capability-cache.js +41 -0
  165. package/dist/stack/capability-cache.js.map +1 -0
  166. package/dist/stack/doctor.d.ts +30 -0
  167. package/dist/stack/doctor.d.ts.map +1 -0
  168. package/dist/stack/doctor.js +227 -0
  169. package/dist/stack/doctor.js.map +1 -0
  170. package/dist/stack/manifest.d.ts +40 -0
  171. package/dist/stack/manifest.d.ts.map +1 -0
  172. package/dist/stack/manifest.js +342 -0
  173. package/dist/stack/manifest.js.map +1 -0
  174. package/dist/stack/mcp-probe.d.ts +23 -0
  175. package/dist/stack/mcp-probe.d.ts.map +1 -0
  176. package/dist/stack/mcp-probe.js +230 -0
  177. package/dist/stack/mcp-probe.js.map +1 -0
  178. package/dist/stack/path-resolve.d.ts +7 -0
  179. package/dist/stack/path-resolve.d.ts.map +1 -0
  180. package/dist/stack/path-resolve.js +37 -0
  181. package/dist/stack/path-resolve.js.map +1 -0
  182. package/dist/stack/registry.d.ts +11 -0
  183. package/dist/stack/registry.d.ts.map +1 -0
  184. package/dist/stack/registry.js +42 -0
  185. package/dist/stack/registry.js.map +1 -0
  186. package/dist/stack/sync.d.ts +20 -0
  187. package/dist/stack/sync.d.ts.map +1 -0
  188. package/dist/stack/sync.js +226 -0
  189. package/dist/stack/sync.js.map +1 -0
  190. package/dist/stack/types.d.ts +53 -0
  191. package/dist/stack/types.d.ts.map +1 -0
  192. package/dist/stack/types.js +2 -0
  193. package/dist/stack/types.js.map +1 -0
  194. package/dist/transcript.d.ts +14 -0
  195. package/dist/transcript.d.ts.map +1 -1
  196. package/dist/transcript.js +98 -1
  197. package/dist/transcript.js.map +1 -1
  198. package/dist/types.d.ts +4 -0
  199. package/dist/types.d.ts.map +1 -1
  200. package/package.json +5 -2
package/dist/scan.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { MarketplaceItem } from './marketplace.js';
2
+ import type { FetchLike } from './live.js';
3
+ export type CheckStatus = 'pass' | 'warn' | 'fail';
4
+ export interface ScanCheck {
5
+ name: string;
6
+ label: string;
7
+ status: CheckStatus;
8
+ message: string;
9
+ }
10
+ export interface ScanResult {
11
+ id: string;
12
+ itemKind: 'package' | 'workflow';
13
+ checks: ScanCheck[];
14
+ summary: {
15
+ pass: number;
16
+ warn: number;
17
+ fail: number;
18
+ };
19
+ }
20
+ export interface ScanOptions {
21
+ fetcher?: FetchLike;
22
+ now?: () => Date;
23
+ githubToken?: string;
24
+ }
25
+ export declare function scanItem(item: MarketplaceItem, opts?: ScanOptions): Promise<ScanResult>;
26
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA0B,MAAM,kBAAkB,CAAC;AAEhF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,SAAS,GAAG,UAAU,CAAC;IACjC,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACvD;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAqND,wBAAsB,QAAQ,CAAC,IAAI,EAAE,eAAe,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAkCjG"}
package/dist/scan.js ADDED
@@ -0,0 +1,247 @@
1
+ import { hasPermissions, getInstallKind } from './marketplace.js';
2
+ function tally(checks) {
3
+ let pass = 0, warn = 0, fail = 0;
4
+ for (const c of checks) {
5
+ if (c.status === 'pass')
6
+ pass++;
7
+ else if (c.status === 'warn')
8
+ warn++;
9
+ else
10
+ fail++;
11
+ }
12
+ return { pass, warn, fail };
13
+ }
14
+ function licenseCheck(status, message) {
15
+ return { name: 'license_present', label: 'License declared', status, message };
16
+ }
17
+ // Returns repo_reachable plus (when the repo is reachable) license_present,
18
+ // both derived from a single GitHub repos API call. A missing license is a
19
+ // warning, never a hard fail — many legitimate repos lack a detected license.
20
+ async function checkRepo(item, opts) {
21
+ const base = {
22
+ name: 'repo_reachable',
23
+ label: 'Repository reachable'
24
+ };
25
+ if (!item.repository) {
26
+ return [{ ...base, status: 'pass', message: 'no repository field, skipped' }];
27
+ }
28
+ let url;
29
+ try {
30
+ url = new URL(item.repository);
31
+ }
32
+ catch {
33
+ return [{ ...base, status: 'pass', message: 'non-github repository, skipped' }];
34
+ }
35
+ if (url.hostname !== 'github.com') {
36
+ return [{ ...base, status: 'pass', message: 'non-github repository, skipped' }];
37
+ }
38
+ const path = url.pathname
39
+ .replace(/^\//, '')
40
+ .replace(/\.git$/, '')
41
+ .split('/')
42
+ .slice(0, 2)
43
+ .join('/');
44
+ const apiUrl = `https://api.github.com/repos/${path}`;
45
+ const headers = { Accept: 'application/vnd.github+json' };
46
+ if (opts.githubToken)
47
+ headers.Authorization = `Bearer ${opts.githubToken}`;
48
+ const fetcher = opts.fetcher ?? globalThis.fetch;
49
+ try {
50
+ const res = await fetcher(apiUrl, { headers, signal: AbortSignal.timeout(8000) });
51
+ if (res.status === 200) {
52
+ const reachable = { ...base, status: 'pass', message: `github.com/${path}` };
53
+ let license;
54
+ try {
55
+ const body = (await res.json());
56
+ const spdx = body.license?.spdx_id;
57
+ license =
58
+ spdx && spdx !== 'NOASSERTION'
59
+ ? licenseCheck('pass', spdx)
60
+ : licenseCheck('warn', 'no license detected on the repository');
61
+ }
62
+ catch {
63
+ license = licenseCheck('warn', 'could not read license metadata');
64
+ }
65
+ return [reachable, license];
66
+ }
67
+ if (res.status === 404)
68
+ return [{ ...base, status: 'fail', message: 'repo not found' }];
69
+ return [{ ...base, status: 'warn', message: 'could not verify (rate limited or network)' }];
70
+ }
71
+ catch {
72
+ return [{ ...base, status: 'warn', message: 'could not verify (rate limited or network)' }];
73
+ }
74
+ }
75
+ async function checkNpmExists(item, opts) {
76
+ const base = {
77
+ name: 'npm_exists',
78
+ label: 'npm package exists'
79
+ };
80
+ if (!item.npmPackage)
81
+ return { ...base, status: 'pass', message: 'no npm package, skipped' };
82
+ const encoded = encodeURIComponent(item.npmPackage).replace('%40', '@').replace('%2F', '/');
83
+ const fetcher = opts.fetcher ?? globalThis.fetch;
84
+ try {
85
+ const res = await fetcher(`https://registry.npmjs.org/${encoded}/latest`, {
86
+ signal: AbortSignal.timeout(8000)
87
+ });
88
+ if (res.status === 200) {
89
+ const json = (await res.json());
90
+ return {
91
+ ...base,
92
+ status: 'pass',
93
+ message: `${item.npmPackage}@${json.version ?? 'unknown'}`
94
+ };
95
+ }
96
+ if (res.status === 404)
97
+ return { ...base, status: 'fail', message: 'package not found on npm' };
98
+ return { ...base, status: 'warn', message: 'could not verify (network)' };
99
+ }
100
+ catch {
101
+ return { ...base, status: 'warn', message: 'could not verify (network)' };
102
+ }
103
+ }
104
+ async function scanPackage(item, opts) {
105
+ const checks = [];
106
+ // 1. permissions_declared
107
+ const hasPerm = hasPermissions(item.permissions);
108
+ if (hasPerm) {
109
+ const parts = [];
110
+ if (item.permissions?.fs?.length)
111
+ parts.push('fs');
112
+ if (item.permissions?.net?.length)
113
+ parts.push('net');
114
+ if (item.permissions?.exec?.length)
115
+ parts.push('exec');
116
+ checks.push({
117
+ name: 'permissions_declared',
118
+ label: 'Permissions declared',
119
+ status: 'pass',
120
+ message: parts.join('|')
121
+ });
122
+ }
123
+ else {
124
+ checks.push({
125
+ name: 'permissions_declared',
126
+ label: 'Permissions declared',
127
+ status: 'warn',
128
+ message: 'no permissions manifest declared'
129
+ });
130
+ }
131
+ // 2. permission_consistency
132
+ const kind = getInstallKind(item);
133
+ if (kind === 'git-clone' && !item.permissions?.exec?.length) {
134
+ checks.push({
135
+ name: 'permission_consistency',
136
+ label: 'Permission consistency',
137
+ status: 'warn',
138
+ message: 'git-clone install runs shell commands; declare exec'
139
+ });
140
+ }
141
+ else if (kind === 'mcp-config-patch' && item.npmPackage && !item.permissions?.exec?.length) {
142
+ checks.push({
143
+ name: 'permission_consistency',
144
+ label: 'Permission consistency',
145
+ status: 'warn',
146
+ message: 'npx invocation runs binaries; declare exec'
147
+ });
148
+ }
149
+ else {
150
+ checks.push({
151
+ name: 'permission_consistency',
152
+ label: 'Permission consistency',
153
+ status: 'pass',
154
+ message: 'declared permissions match install kind'
155
+ });
156
+ }
157
+ // 3. repo_reachable (+ license_present when reachable)
158
+ if (item.repository) {
159
+ checks.push(...(await checkRepo(item, opts)));
160
+ }
161
+ // 4. npm_exists
162
+ if (item.npmPackage) {
163
+ checks.push(await checkNpmExists(item, opts));
164
+ }
165
+ // 5. recently_active
166
+ if (item.pushedAt) {
167
+ const now = opts.now ? opts.now() : new Date();
168
+ const days = Math.floor((now.getTime() - new Date(item.pushedAt).getTime()) / 86_400_000);
169
+ if (days <= 365) {
170
+ checks.push({
171
+ name: 'recently_active',
172
+ label: 'Recently active',
173
+ status: 'pass',
174
+ message: `pushed ${days}d ago`
175
+ });
176
+ }
177
+ else {
178
+ checks.push({
179
+ name: 'recently_active',
180
+ label: 'Recently active',
181
+ status: 'warn',
182
+ message: `last push ${days}d ago — may be unmaintained`
183
+ });
184
+ }
185
+ }
186
+ // 6. flag_count_low
187
+ const flags = item.flagCount ?? 0;
188
+ if (flags < 3) {
189
+ checks.push({
190
+ name: 'flag_count_low',
191
+ label: 'Flag count low',
192
+ status: 'pass',
193
+ message: `${flags} flags`
194
+ });
195
+ }
196
+ else if (flags < 10) {
197
+ checks.push({
198
+ name: 'flag_count_low',
199
+ label: 'Flag count low',
200
+ status: 'warn',
201
+ message: `${flags} flags — under review threshold`
202
+ });
203
+ }
204
+ else {
205
+ checks.push({
206
+ name: 'flag_count_low',
207
+ label: 'Flag count low',
208
+ status: 'fail',
209
+ message: `${flags} flags — would auto-hide`
210
+ });
211
+ }
212
+ return checks;
213
+ }
214
+ export async function scanItem(item, opts = {}) {
215
+ let checks;
216
+ if (item.kind === 'workflow') {
217
+ const n = item.flagCount ?? 0;
218
+ checks = [
219
+ {
220
+ name: 'workflow_kind',
221
+ label: 'Workflow kind',
222
+ status: 'pass',
223
+ message: 'Workflow items are inert prompts — no install side effects to scan.'
224
+ },
225
+ {
226
+ name: 'flag_count_low',
227
+ label: 'Flag count low',
228
+ status: n < 3 ? 'pass' : n < 10 ? 'warn' : 'fail',
229
+ message: n < 3
230
+ ? `${n} flags`
231
+ : n < 10
232
+ ? `${n} flags — under review threshold`
233
+ : `${n} flags — would auto-hide`
234
+ }
235
+ ];
236
+ }
237
+ else {
238
+ checks = await scanPackage(item, opts);
239
+ }
240
+ return {
241
+ id: item.id,
242
+ itemKind: item.kind,
243
+ checks,
244
+ summary: tally(checks)
245
+ };
246
+ }
247
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"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"}
@@ -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