dsh-search-enhance 0.1.4 → 0.1.5

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 CHANGED
@@ -14,11 +14,11 @@ DSH Agent
14
14
  │
15
15
  └─ 固定模型工具 surface(schema 与顺序不随披露状态变化)
16
16
  ├─ web_search ──────> Grok 主搜索
17
- │ ├─ 按需要补充 Context7 / Exa
17
+ │ ├─ 按需要补充 Exa 文档来源
18
18
  │ ├─ 按需要补充 Tavily / Firecrawl
19
19
  │ └─ 返回回答、来源;有 source_ref 时追加 search_sources manifest
20
20
  │
21
- ├─ docs_search ─────> Context7 / Exa 文档检索
21
+ ├─ docs_search ─────> 显式 Context7 库身份 / Exa 文档发现
22
22
  │ └─ 返回文档片段、来源;有 source_ref 时追加 search_sources manifest
23
23
  │
24
24
  ├─ web_extract ─────> Tavily → Firecrawl → smart_direct → direct
@@ -45,7 +45,7 @@ DSH Agent
45
45
  | 工具 | 调用方式与用途 |
46
46
  | --- | --- |
47
47
  | `web_search` | 直接调用。使用 Grok 生成通用搜索的主要回答,并按搜索类型补充其他来源 |
48
- | `docs_search` | 直接调用。检索库、框架、SDK、API 和源码仓库文档 |
48
+ | `docs_search` | 直接调用。已知精确 `/org/project` 时传 `library_id`;已知包或产品名时传 `library_name`;不知道库身份时用 `provider: auto` 进行 Exa 发现 |
49
49
  | `web_extract` | 直接调用。读取指定网页正文,用于核对搜索摘要中的重要内容 |
50
50
  | `search_tools` | 直接调用。按需返回延迟能力的 operation manifest,不注册新的模型工具 |
51
51
  | `search_call` | 固定网关。通过 `search_call({ operation, arguments })` 调用已经激活的延迟 operation |
@@ -78,11 +78,13 @@ DSH Agent
78
78
  ### 一次完整搜索如何进行
79
79
 
80
80
  1. 通用问题直接调用 `web_search`,文档问题直接调用 `docs_search`。
81
- 2. 当搜索产生来源时,结果会包含可见来源、`source_ref` 和追加的 `search_sources` manifest,同时自动激活 `sources`。
82
- 3. 在下一 step 需要更多来源时,调用 `search_call({ operation: 'search_sources', arguments: { source_ref, offset: 0, limit: 20, format: 'compact' } })` 分页读取,而不是直接调用 `search_sources`。
83
- 4. 对重要结论,选择权威链接并直接调用 `web_extract` 获取网页正文。
84
- 5. 如果任务需要站点内发现、研究计划、精细 Context7 查询或连接检查,先调用例如 `search_tools({ capabilities: ['site_map'] })` 取得 manifest;`progressive` 模式从下一 step、`all` 模式立即通过 `search_call({ operation: 'web_map', arguments: { url: 'https://example.com' } })` 调用相应 operation。
85
- 6. 最终回答综合主搜索、补充来源和已经读取的网页正文,并保留来源链接。
81
+ 2. 调用 `docs_search` 时保持任务问题与库身份分离。例如 `{"query":"JWT authentication middleware API","library_name":"FastAPI"}`。同时提供 `library_id` 和 `library_name` 时,合法 `library_id` 优先并跳过 resolve。
82
+ 3. `provider: auto` 没有 `library_id`/`library_name` 时只使用 Exa;`provider: context7` 或 `all` 缺少库身份会在凭据、缓存和网络前失败。普通 `web_search` 的文档增强也只使用 Exa,不猜测 Context7 库。
83
+ 4. 当搜索产生来源时,结果会包含可见来源、`source_ref` 和追加的 `search_sources` manifest,同时自动激活 `sources`。
84
+ 5. 在下一 step 需要更多来源时,调用 `search_call({ operation: 'search_sources', arguments: { source_ref, offset: 0, limit: 20, format: 'compact' } })` 分页读取,而不是直接调用 `search_sources`。
85
+ 6. 对重要结论,选择权威链接并直接调用 `web_extract` 获取网页正文。
86
+ 7. 如果任务需要站点内发现、研究计划、精细 Context7 查询或连接检查,先调用例如 `search_tools({ capabilities: ['site_map'] })` 取得 manifest;`progressive` 模式从下一 step、`all` 模式立即通过 `search_call({ operation: 'web_map', arguments: { url: 'https://example.com' } })` 调用相应 operation。
87
+ 8. 最终回答综合主搜索、补充来源和已经读取的网页正文,并保留来源链接。
86
88
 
87
89
  `source_ref` 只是完整来源列表的引用,不等同于网页正文;重要事实仍应通过 `web_extract` 读取原页面后再下结论。
88
90
 
@@ -135,7 +137,7 @@ dsh web
135
137
 
136
138
  | 服务 | 用途 | 默认密钥名称 |
137
139
  | --- | --- | --- |
138
- | Context7 | 查找库和框架文档 | `CONTEXT7_API_KEY` |
140
+ | Context7 | 使用显式 `library_name` 或 `library_id` 查找库和框架文档 | `CONTEXT7_API_KEY` |
139
141
  | Exa | 补充文档和网页结果 | `EXA_API_KEY` |
140
142
  | Tavily | 补充搜索、读取网页和发现站点页面 | `TAVILY_API_KEY` |
141
143
  | Firecrawl | 补充搜索和读取网页 | `FIRECRAWL_API_KEY` |
@@ -4,7 +4,8 @@ import type { BoundedSourceProvider } from '../providers/types.js';
4
4
  import type { DiagnosticCapability, DiagnosticProbe, DiagnosticProbeInput, DiagnosticProbeResult, DiagnosticProviderName } from './types.js';
5
5
  /** Fixed, non-user-controlled diagnostic targets. */
6
6
  export declare const DIAGNOSTIC_SEARCH_QUERY = "search-enhance fixed connectivity diagnostic";
7
- export declare const DIAGNOSTIC_CONTEXT7_QUERY = "react documentation";
7
+ export declare const DIAGNOSTIC_CONTEXT7_LIBRARY_NAME = "React";
8
+ export declare const DIAGNOSTIC_CONTEXT7_QUERY = "documentation connectivity diagnostic";
8
9
  export declare const DIAGNOSTIC_RESULT_LIMIT = 1;
9
10
  /** Uses the public bounded model-list GET; it never calls Search API main search. */
10
11
  export declare class SearchApiModelListDiagnosticProbe implements DiagnosticProbe {
@@ -1,7 +1,8 @@
1
1
  import { throwIfAborted } from '../provider-runtime/index.js';
2
2
  /** Fixed, non-user-controlled diagnostic targets. */
3
3
  export const DIAGNOSTIC_SEARCH_QUERY = 'search-enhance fixed connectivity diagnostic';
4
- export const DIAGNOSTIC_CONTEXT7_QUERY = 'react documentation';
4
+ export const DIAGNOSTIC_CONTEXT7_LIBRARY_NAME = 'React';
5
+ export const DIAGNOSTIC_CONTEXT7_QUERY = 'documentation connectivity diagnostic';
5
6
  export const DIAGNOSTIC_RESULT_LIMIT = 1;
6
7
  const COMPLETE = Object.freeze({ state: 'complete' });
7
8
  const NOT_CONFIGURED = Object.freeze({ state: 'not_configured' });
@@ -38,6 +39,7 @@ export class Context7ResolveDiagnosticProbe {
38
39
  await this.context7.resolve({
39
40
  config: input.config,
40
41
  limit: DIAGNOSTIC_RESULT_LIMIT,
42
+ libraryName: DIAGNOSTIC_CONTEXT7_LIBRARY_NAME,
41
43
  onDispatch: input.onDispatch,
42
44
  query: DIAGNOSTIC_CONTEXT7_QUERY,
43
45
  signal: input.signal,
@@ -49,6 +49,7 @@ export interface Context7CacheRepository {
49
49
  /** Cache identity includes every bounded input that can change a successful resolve value. */
50
50
  export declare function context7ResolveCacheKey(input: {
51
51
  readonly baseUrl: string;
52
+ readonly libraryName: string;
52
53
  readonly query: string;
53
54
  readonly maxResults: number;
54
55
  readonly maxLibraryTextCharacters: number;
@@ -40,11 +40,15 @@ export function context7ResolveCacheKey(input) {
40
40
  positiveSafeInteger(input.maxResults, 'maxResults');
41
41
  positiveSafeInteger(input.maxLibraryTextCharacters, 'maxLibraryTextCharacters');
42
42
  positiveSafeInteger(input.maxEntryBytes, 'maxEntryBytes');
43
+ const libraryName = input.libraryName.trim();
43
44
  const query = input.query.trim();
45
+ if (libraryName.length === 0)
46
+ throw new RangeError('Context7 library name must not be empty');
44
47
  if (query.length === 0)
45
48
  throw new RangeError('Context7 resolve query must not be empty');
46
49
  return `ctx7r_${digestIdentity('resolve', {
47
50
  baseUrl: normalizedBaseUrl(input.baseUrl),
51
+ libraryName,
48
52
  maxEntryBytes: input.maxEntryBytes,
49
53
  maxLibraryTextCharacters: input.maxLibraryTextCharacters,
50
54
  maxResults: input.maxResults,
@@ -39,6 +39,7 @@ export interface Context7CacheClock {
39
39
  now(): number;
40
40
  }
41
41
  export interface Context7ResolveCachedInput {
42
+ readonly libraryName: string;
42
43
  readonly query: string;
43
44
  readonly maxResults: number;
44
45
  readonly forceRefresh: boolean;
@@ -174,6 +174,7 @@ export class Context7CachedOperations {
174
174
  throwIfAborted(input.signal);
175
175
  const cacheKey = context7ResolveCacheKey({
176
176
  baseUrl: input.config.providers.context7.baseUrl,
177
+ libraryName: input.libraryName,
177
178
  maxEntryBytes: input.config.cache.context7EntryMaxBytes,
178
179
  maxLibraryTextCharacters: input.config.cache.context7LibraryTextMaxCharacters,
179
180
  maxResults: input.maxResults,
@@ -1,5 +1,5 @@
1
1
  export { CONTEXT7_CACHE_DOMAIN_NAME, CONTEXT7_CACHE_DOMAIN_SPEC, CONTEXT7_CACHE_FORMAT_VERSION, CONTEXT7_CACHE_KEY_PATTERN, CONTEXT7_CACHE_TABLE_NAME, CONTEXT7_DOC_REF_PATTERN, CachedContext7LibrarySchema, CachedDocumentationSnippetSchema, Context7CacheEntrySchema, Context7CacheKeySchema, Context7DocsCacheEntrySchema, Context7DocRefSchema, Context7ResolveCacheEntrySchema, type CachedContext7Library, type CachedDocumentationSnippet, type Context7CacheDomain, type Context7CacheEntry, type Context7CacheKey, type Context7DocsCacheEntry, type Context7DocRef, type Context7ResolveCacheEntry, } from './cache-domain.js';
2
2
  export { CONTEXT7_CACHE_QUERY_MAX_SCAN_RECORDS, CONTEXT7_CACHE_QUERY_MAX_TERMS, CONTEXT7_CACHE_QUERY_MAX_TEXT_CHARACTERS, Context7CacheError, Context7CacheStore, PersistentContext7Cache, context7DocsCacheKey, context7ResolveCacheKey, isContext7DocRef, type Context7CacheErrorCode, type Context7CacheLimits, type Context7CacheRepository, type Context7CacheWriteResult, type Context7CachedDocFound, type Context7CachedDocLookup, type Context7CachedDocMatch, type Context7CachedDocMatchFound, type Context7CachedDocMatchNotFound, type Context7CachedDocNotFound, type Context7CachedDocQuery, } from './cache.js';
3
3
  export { CONTEXT7_CACHE_STATES, Context7CachedOperations, Context7OperationFailure, context7CacheEntryIsFresh, isContext7LibraryId, normalizeContext7LibraryId, permitsContext7StaleFallback, type CachedContext7Operation, type Context7CacheClock, type Context7CacheState, type Context7DocsCachedInput, type Context7DocsRemoteResult, type Context7OperationPath, type Context7RemoteDiagnostics, type Context7ResolveCachedInput, type Context7ResolveRemoteResult, } from './context7-cache.js';
4
- export { DOCUMENTATION_CACHE_PATH_STATES, DOCUMENTATION_CACHE_SKIP_REASONS, DOCUMENTATION_PROVIDER_STATES, DOCUMENTATION_RESULT_PROVIDERS, DOCUMENTATION_SEARCH_PROVIDERS, DOCUMENTATION_SEARCH_SERVICE_KEY, DOCUMENTATION_WARNING_CODES, DocumentationContext7Provider, DocumentationSearchInfrastructureError, DocumentationSearchService, type Context7CachedDocSearchInput, type Context7DocsInput, type Context7DocsResult, type Context7ResolveInput, type Context7ResolveResult, type DocumentationCachePath, type DocumentationCachePathState, type DocumentationCacheReport, type DocumentationCacheSkipReason, type DocumentationProviderState, type DocumentationProviderStatus, type DocumentationResultProvider, type DocumentationSearchDependencies, type DocumentationSearchInput, type DocumentationSearchProvider, type DocumentationSearchResult, type DocumentationWarning, type DocumentationWarningCode, } from './service.js';
4
+ export { DOCUMENTATION_CACHE_PATH_STATES, DOCUMENTATION_CACHE_SKIP_REASONS, DOCUMENTATION_PROVIDER_STATES, DOCUMENTATION_RESULT_PROVIDERS, DOCUMENTATION_SEARCH_PROVIDERS, DOCUMENTATION_SEARCH_SERVICE_KEY, DOCUMENTATION_WARNING_CODES, DocumentationSearchInfrastructureError, DocumentationSearchService, type Context7CachedDocSearchInput, type Context7DocsInput, type Context7DocsResult, type Context7ResolveInput, type Context7ResolveResult, type DocumentationCachePath, type DocumentationCachePathState, type DocumentationCacheReport, type DocumentationCacheSkipReason, type DocumentationProviderState, type DocumentationProviderStatus, type DocumentationResultProvider, type DocumentationSearchDependencies, type DocumentationSearchInput, type DocumentationSearchProvider, type DocumentationSearchResult, type DocumentationWarning, type DocumentationWarningCode, } from './service.js';
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,5 @@
1
1
  export { CONTEXT7_CACHE_DOMAIN_NAME, CONTEXT7_CACHE_DOMAIN_SPEC, CONTEXT7_CACHE_FORMAT_VERSION, CONTEXT7_CACHE_KEY_PATTERN, CONTEXT7_CACHE_TABLE_NAME, CONTEXT7_DOC_REF_PATTERN, CachedContext7LibrarySchema, CachedDocumentationSnippetSchema, Context7CacheEntrySchema, Context7CacheKeySchema, Context7DocsCacheEntrySchema, Context7DocRefSchema, Context7ResolveCacheEntrySchema, } from './cache-domain.js';
2
2
  export { CONTEXT7_CACHE_QUERY_MAX_SCAN_RECORDS, CONTEXT7_CACHE_QUERY_MAX_TERMS, CONTEXT7_CACHE_QUERY_MAX_TEXT_CHARACTERS, Context7CacheError, Context7CacheStore, PersistentContext7Cache, context7DocsCacheKey, context7ResolveCacheKey, isContext7DocRef, } from './cache.js';
3
3
  export { CONTEXT7_CACHE_STATES, Context7CachedOperations, Context7OperationFailure, context7CacheEntryIsFresh, isContext7LibraryId, normalizeContext7LibraryId, permitsContext7StaleFallback, } from './context7-cache.js';
4
- export { DOCUMENTATION_CACHE_PATH_STATES, DOCUMENTATION_CACHE_SKIP_REASONS, DOCUMENTATION_PROVIDER_STATES, DOCUMENTATION_RESULT_PROVIDERS, DOCUMENTATION_SEARCH_PROVIDERS, DOCUMENTATION_SEARCH_SERVICE_KEY, DOCUMENTATION_WARNING_CODES, DocumentationContext7Provider, DocumentationSearchInfrastructureError, DocumentationSearchService, } from './service.js';
4
+ export { DOCUMENTATION_CACHE_PATH_STATES, DOCUMENTATION_CACHE_SKIP_REASONS, DOCUMENTATION_PROVIDER_STATES, DOCUMENTATION_RESULT_PROVIDERS, DOCUMENTATION_SEARCH_PROVIDERS, DOCUMENTATION_SEARCH_SERVICE_KEY, DOCUMENTATION_WARNING_CODES, DocumentationSearchInfrastructureError, DocumentationSearchService, } from './service.js';
5
5
  //# sourceMappingURL=index.js.map
@@ -3,7 +3,7 @@ import type { Config } from '../config.js';
3
3
  import type { CanonicalSource, SourceRecordCandidate } from '../contracts/index.js';
4
4
  import { type ProviderAttemptRecord, type ProviderErrorKind } from '../provider-runtime/index.js';
5
5
  import { type Context7RemoteClient, type Context7Library } from '../providers/context7.js';
6
- import type { BoundedSourceProvider, DocumentationSnippet, SourceProviderSearchInput, SourceProviderSearchOutcome } from '../providers/types.js';
6
+ import type { BoundedSourceProvider, DocumentationSnippet } from '../providers/types.js';
7
7
  import type { Context7DocsCacheEntry, Context7DocRef } from './cache-domain.js';
8
8
  import { type Context7CachedDocLookup, type Context7CachedDocMatch } from './cache.js';
9
9
  import { Context7CachedOperations, type Context7OperationPath } from './context7-cache.js';
@@ -43,7 +43,10 @@ export interface DocumentationWarning {
43
43
  export interface DocumentationSearchInput {
44
44
  readonly query: string;
45
45
  readonly provider?: DocumentationSearchProvider;
46
+ /** Exact Context7 id; when present it takes priority and bypasses resolve. */
46
47
  readonly libraryId?: string;
48
+ /** Explicit Context7 package/product identity; never inferred from query. */
49
+ readonly libraryName?: string;
47
50
  readonly maxResults: number;
48
51
  readonly forceRefresh?: boolean;
49
52
  readonly signal: AbortSignal;
@@ -138,8 +141,8 @@ declare module '@deepseek-ai/cordis' {
138
141
  }
139
142
  }
140
143
  /**
141
- * Lifecycle-bound documentation core shared by the high-level docs Consumer,
142
- * enhancement routing, and the deferred granular Context7 Consumers.
144
+ * Lifecycle-bound documentation core shared by the high-level docs Consumer
145
+ * and the deferred granular Context7 Consumers.
143
146
  */
144
147
  export declare class DocumentationSearchService extends Service {
145
148
  private readonly context7;
@@ -167,13 +170,4 @@ export declare class DocumentationSearchService extends Service {
167
170
  private runContext7;
168
171
  private runExa;
169
172
  }
170
- /** Context7-only adapter used by web_search while sharing the high-level service/cache. */
171
- export declare class DocumentationContext7Provider implements BoundedSourceProvider {
172
- private readonly documentation;
173
- readonly capability: "docs_search";
174
- readonly provider: "context7";
175
- constructor(documentation: DocumentationSearchService);
176
- configured(): Promise<boolean>;
177
- search(input: SourceProviderSearchInput): Promise<SourceProviderSearchOutcome>;
178
- }
179
173
  //# sourceMappingURL=service.d.ts.map
@@ -1,6 +1,5 @@
1
1
  import { Service } from '@deepseek-ai/cordis';
2
2
  import { createProviderAttemptRecord, isAbortError, isProviderError, ProviderError, throwIfAborted, } from '../provider-runtime/index.js';
3
- import { boundSourceProviderResult } from '../providers/bounded-result.js';
4
3
  import { context7LibrarySource, selectContext7Library, } from '../providers/context7.js';
5
4
  import { applySourceQuality } from '../search/index.js';
6
5
  import { CONTEXT7_CACHE_QUERY_MAX_SCAN_RECORDS, } from './cache.js';
@@ -190,17 +189,34 @@ function validateInput(input, config) {
190
189
  provider: 'documentation-search',
191
190
  });
192
191
  }
193
- if (input.libraryId !== undefined && !isContext7LibraryId(input.libraryId)) {
192
+ let libraryId;
193
+ if (provider !== 'exa' && input.libraryId !== undefined) {
194
+ if (!isContext7LibraryId(input.libraryId)) {
195
+ throw new ProviderError({
196
+ capability: 'docs_search',
197
+ kind: 'invalid_request',
198
+ provider: 'context7',
199
+ });
200
+ }
201
+ libraryId = normalizeContext7LibraryId(input.libraryId);
202
+ }
203
+ const libraryName = provider !== 'exa'
204
+ && libraryId === undefined
205
+ && input.libraryName !== undefined
206
+ ? context7Text(input.libraryName, config)
207
+ : undefined;
208
+ if ((provider === 'context7' || provider === 'all')
209
+ && libraryId === undefined
210
+ && libraryName === undefined) {
194
211
  throw new ProviderError({
195
212
  capability: 'docs_search',
196
213
  kind: 'invalid_request',
197
- provider: 'context7',
214
+ provider: 'context7-library-name-or-id-required',
198
215
  });
199
216
  }
200
217
  return {
201
- ...(input.libraryId === undefined
202
- ? {}
203
- : { libraryId: normalizeContext7LibraryId(input.libraryId) }),
218
+ ...(libraryId === undefined ? {} : { libraryId }),
219
+ ...(libraryName === undefined ? {} : { libraryName }),
204
220
  provider,
205
221
  query,
206
222
  };
@@ -239,8 +255,8 @@ function frozenPathOutcome(outcome) {
239
255
  });
240
256
  }
241
257
  /**
242
- * Lifecycle-bound documentation core shared by the high-level docs Consumer,
243
- * enhancement routing, and the deferred granular Context7 Consumers.
258
+ * Lifecycle-bound documentation core shared by the high-level docs Consumer
259
+ * and the deferred granular Context7 Consumers.
244
260
  */
245
261
  export class DocumentationSearchService extends Service {
246
262
  context7;
@@ -361,11 +377,13 @@ export class DocumentationSearchService extends Service {
361
377
  dispatches += 1;
362
378
  input.onDispatch?.();
363
379
  },
364
- query: input.libraryName,
380
+ libraryName: input.libraryName,
381
+ query: input.query,
365
382
  signal: input.signal,
366
383
  }),
367
384
  maxResults: candidateLimit,
368
- query: input.libraryName,
385
+ libraryName: input.libraryName,
386
+ query: input.query,
369
387
  signal: input.signal,
370
388
  });
371
389
  const candidates = Object.freeze(resolved.entry.libraries.slice(0, input.maxResults));
@@ -419,8 +437,12 @@ export class DocumentationSearchService extends Service {
419
437
  const config = input.config ?? this.getConfig();
420
438
  const validated = validateInput(input, config);
421
439
  const forceRefresh = input.forceRefresh === true;
422
- const runContext7 = validated.provider !== 'exa';
423
- const autoExa = validated.provider === 'auto' && config.fallbackMode === 'auto';
440
+ const hasContext7Identity = validated.libraryId !== undefined || validated.libraryName !== undefined;
441
+ const runContext7 = validated.provider === 'context7'
442
+ || validated.provider === 'all'
443
+ || (validated.provider === 'auto' && hasContext7Identity);
444
+ const autoExa = validated.provider === 'auto'
445
+ && (!hasContext7Identity || config.fallbackMode === 'auto');
424
446
  const considerExa = validated.provider === 'exa' || validated.provider === 'all' || autoExa;
425
447
  let exaConfigured = false;
426
448
  let exaProbeError;
@@ -440,6 +462,7 @@ export class DocumentationSearchService extends Service {
440
462
  config,
441
463
  forceRefresh,
442
464
  ...(validated.libraryId === undefined ? {} : { libraryId: validated.libraryId }),
465
+ ...(validated.libraryName === undefined ? {} : { libraryName: validated.libraryName }),
443
466
  maxResults: input.maxResults,
444
467
  query: validated.query,
445
468
  signal,
@@ -633,12 +656,19 @@ export class DocumentationSearchService extends Service {
633
656
  attempts.push(skippedAttempt('context7-resolve'));
634
657
  }
635
658
  else {
659
+ if (input.libraryName === undefined) {
660
+ throw new ProviderError({
661
+ capability: 'docs_search',
662
+ kind: 'invalid_request',
663
+ provider: 'context7-library-name-or-id-required',
664
+ });
665
+ }
636
666
  const startedAt = safeClockValue(this.now);
637
667
  try {
638
668
  const resolved = await this.executeContext7Resolve({
639
669
  config: input.config,
640
670
  forceRefresh: input.forceRefresh,
641
- libraryName: input.query,
671
+ libraryName: input.libraryName,
642
672
  maxResults: input.maxResults,
643
673
  candidateLimit: Math.min(CONTEXT7_RESOLVE_CANDIDATE_LIMIT, input.config.retention.providerMaxSources),
644
674
  onDispatch: () => { resolveDispatches += 1; },
@@ -838,65 +868,4 @@ export class DocumentationSearchService extends Service {
838
868
  }
839
869
  }
840
870
  }
841
- /** Context7-only adapter used by web_search while sharing the high-level service/cache. */
842
- export class DocumentationContext7Provider {
843
- documentation;
844
- capability = 'docs_search';
845
- provider = 'context7';
846
- constructor(documentation) {
847
- this.documentation = documentation;
848
- }
849
- async configured() {
850
- return true;
851
- }
852
- async search(input) {
853
- let result;
854
- try {
855
- result = await this.documentation.search({
856
- config: input.config,
857
- forceRefresh: false,
858
- maxResults: input.limit,
859
- provider: 'context7',
860
- query: input.query,
861
- signal: input.signal,
862
- });
863
- }
864
- catch (error) {
865
- if (error instanceof DocumentationSearchInfrastructureError && error.cause !== undefined) {
866
- throw error.cause;
867
- }
868
- throw error;
869
- }
870
- const warnings = result.warnings.flatMap(warning => {
871
- if (warning.code !== 'cache_stale'
872
- && warning.code !== 'cache_evicted'
873
- && warning.code !== 'provider_failed')
874
- return [];
875
- return [Object.freeze({
876
- code: warning.code,
877
- ...(warning.errorKind === undefined ? {} : { errorKind: warning.errorKind }),
878
- provider: warning.provider ?? 'context7',
879
- })];
880
- });
881
- const attempts = result.attempts
882
- .filter(attempt => !attempt.provider.startsWith('context7-cache-'))
883
- .reduce((sum, attempt) => sum + attempt.attempts, 0);
884
- return Object.freeze({
885
- attempts: Math.max(1, attempts),
886
- result: boundSourceProviderResult({
887
- capability: 'docs_search',
888
- config: input.config,
889
- inputTruncated: result.truncated,
890
- provider: 'context7',
891
- requestedSources: input.limit,
892
- responseBytes: result.providerResponseBytes,
893
- snippets: result.snippets,
894
- sources: result.sources,
895
- }),
896
- state: 'complete',
897
- totalDelayMs: 0,
898
- ...(warnings.length === 0 ? {} : { warnings: Object.freeze(warnings) }),
899
- });
900
- }
901
- }
902
871
  //# sourceMappingURL=service.js.map
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Context7ResolveDiagnosticProbe, SearchApiModelListDiagnosticProbe, SearchDiagnostics, SourceSearchDiagnosticProbe, } from './diagnostics/index.js';
2
2
  import { Config as SearchEnhanceConfig, SEARCH_ENHANCE_SETTINGS_NAMESPACE, } from './config.js';
3
- import { Context7CachedOperations, DocumentationContext7Provider, DocumentationSearchService, PersistentContext7Cache, } from './documentation/index.js';
3
+ import { Context7CachedOperations, DocumentationSearchService, PersistentContext7Cache, } from './documentation/index.js';
4
4
  import { SearchOrchestrator } from './orchestration/index.js';
5
5
  import { registerToolDiscoveryGuidance } from './prompt/tool-discovery.js';
6
6
  import { Context7RemoteClient } from './providers/context7.js';
@@ -82,7 +82,6 @@ export async function apply(ctx, config) {
82
82
  ],
83
83
  });
84
84
  const orchestrator = new SearchOrchestrator({
85
- context7: new DocumentationContext7Provider(documentation),
86
85
  exa,
87
86
  firecrawl,
88
87
  getConfig,
@@ -16,7 +16,6 @@ export declare function mergeCanonicalSources(...groups: ReadonlyArray<readonly
16
16
  export declare class SearchOrchestrator {
17
17
  private readonly getConfig;
18
18
  private readonly mainSearch;
19
- private readonly context7;
20
19
  private readonly exa;
21
20
  private readonly tavily;
22
21
  private readonly firecrawl;
@@ -294,7 +294,6 @@ function sourceAttempt(slot, mainFailed) {
294
294
  export class SearchOrchestrator {
295
295
  getConfig;
296
296
  mainSearch;
297
- context7;
298
297
  exa;
299
298
  tavily;
300
299
  firecrawl;
@@ -302,7 +301,6 @@ export class SearchOrchestrator {
302
301
  constructor(dependencies) {
303
302
  this.getConfig = dependencies.getConfig;
304
303
  this.mainSearch = dependencies.mainSearch;
305
- this.context7 = dependencies.context7;
306
304
  this.exa = dependencies.exa;
307
305
  this.tavily = dependencies.tavily;
308
306
  this.firecrawl = dependencies.firecrawl;
@@ -347,17 +345,6 @@ export class SearchOrchestrator {
347
345
  const relayAbort = () => fanout.abort(signal.reason);
348
346
  signal.addEventListener('abort', relayAbort, { once: true });
349
347
  const slots = [
350
- {
351
- available: false,
352
- capability: 'docs_search',
353
- key: 'context7',
354
- limit: 0,
355
- planningDurationMs: 0,
356
- planningError: undefined,
357
- provider: this.context7,
358
- skipReason: undefined,
359
- track: undefined,
360
- },
361
348
  {
362
349
  available: false,
363
350
  capability: 'docs_search',
@@ -438,8 +425,8 @@ export class SearchOrchestrator {
438
425
  }
439
426
  throwIfAborted(signal);
440
427
  throwIfAborted(fanout.signal);
441
- const tavilySlot = slots[2];
442
- const firecrawlSlot = slots[3];
428
+ const tavilySlot = slots[1];
429
+ const firecrawlSlot = slots[2];
443
430
  if (tavilySlot === undefined || firecrawlSlot === undefined) {
444
431
  throw new Error('discovery slots are incomplete');
445
432
  }
@@ -65,7 +65,6 @@ export interface MainSearchProvider {
65
65
  export interface SearchOrchestratorDependencies {
66
66
  readonly getConfig: () => Config;
67
67
  readonly mainSearch: MainSearchProvider;
68
- readonly context7: BoundedSourceProvider;
69
68
  readonly exa: BoundedSourceProvider;
70
69
  readonly tavily: BoundedSourceProvider;
71
70
  readonly firecrawl: BoundedSourceProvider;
@@ -1,4 +1,8 @@
1
1
  import type { DocsSearchOutput, WebSearchOutput, SearchDiagnosticsOutput, SearchSourcesOutput, WebExtractOutput, WebMapOutput } from '../tools/schemas.js';
2
+ export declare function sourceDisplayLabel(source: {
3
+ readonly title?: string;
4
+ readonly url: string;
5
+ }): string;
2
6
  /**
3
7
  * Pure Native projection in the product-defined order. The operation-selected
4
8
  * limit is carried in the canonical value, so replay never consults Settings,
@@ -3,6 +3,11 @@ const DISCOVERY_NOTICE = 'Evidence level: discovery. Snippets are discovery meta
3
3
  function inline(value) {
4
4
  return value.replace(/\s+/gu, ' ').trim();
5
5
  }
6
+ export function sourceDisplayLabel(source) {
7
+ if (source.title !== undefined && source.title.trim().length > 0)
8
+ return source.title;
9
+ return new URL(source.url).hostname;
10
+ }
6
11
  function renderWithBoundedNotice(text, notice, maximumBytes, fallbackNotice) {
7
12
  if (notice === undefined || notice.length === 0) {
8
13
  return truncateUtf8(text, maximumBytes).text;
@@ -77,7 +82,7 @@ function sourceSection(value) {
77
82
  const source = value.sources[index];
78
83
  if (source === undefined)
79
84
  continue;
80
- lines.push(`${index + 1}. ${inline(source.title ?? 'Untitled source')}`);
85
+ lines.push(`${index + 1}. ${inline(sourceDisplayLabel(source))}`);
81
86
  lines.push(` URL: ${source.url}`);
82
87
  if (source.publishedAt !== undefined) {
83
88
  lines.push(` Date: ${inline(source.publishedAt)}`);
@@ -190,7 +195,7 @@ function docsSourceSection(value) {
190
195
  const source = value.sources[index];
191
196
  if (source === undefined)
192
197
  continue;
193
- lines.push(`${index + 1}. ${inline(source.title ?? 'Untitled source')}`);
198
+ lines.push(`${index + 1}. ${inline(sourceDisplayLabel(source))}`);
194
199
  lines.push(` URL: ${source.url}`);
195
200
  if (source.publishedAt !== undefined)
196
201
  lines.push(` Date: ${inline(source.publishedAt)}`);
@@ -247,7 +252,7 @@ function renderPageSources(page) {
247
252
  const source = page.sources[index];
248
253
  if (source === undefined)
249
254
  continue;
250
- lines.push(`${page.offset + index + 1}. ${inline(source.title ?? 'Untitled source')}`);
255
+ lines.push(`${page.offset + index + 1}. ${inline(sourceDisplayLabel(source))}`);
251
256
  lines.push(` URL: ${source.url}`);
252
257
  lines.push(` Category: ${source.category}`);
253
258
  if (source.date !== undefined)
@@ -1,5 +1,7 @@
1
1
  export const TOOL_DISCOVERY_GUIDANCE = [
2
2
  'Search Enhance keeps a fixed model-facing surface: web_search, docs_search, web_extract, search_tools, and search_call.',
3
+ 'For docs_search, pass library_id when an exact /org/project id is known, or pass library_name directly when the package or product is known; for example docs_search({ query: "JWT authentication middleware API", library_name: "FastAPI" }). Do not activate granular Context7 merely to resolve first.',
4
+ 'For broad, cross-project, or unknown-library documentation discovery, use docs_search with provider: "auto" and no library identity; this uses Exa instead of guessing a Context7 library.',
3
5
  'Use search_tools only when the resident search tools cannot complete the task. It returns append-only capability and operation manifests; do not activate every capability preemptively.',
4
6
  'Run a manifested deferred operation with search_call({ operation, arguments }). In progressive mode, a newly disclosed capability is callable on the next model step; in all mode, deferred operations are active immediately. search_call fails closed while an operation is inactive.',
5
7
  'Activate planning only for explicit deep research, multi-source verification, or complex comparison. Activate diagnostics only when the user asks about Provider configuration or connectivity.',
@@ -3,10 +3,14 @@ import type { CanonicalSource } from '../contracts/index.js';
3
3
  import type { CachedContext7Library } from '../documentation/cache-domain.js';
4
4
  import { type Context7DocsRemoteResult, type Context7ResolveRemoteResult } from '../documentation/context7-cache.js';
5
5
  import { type ProviderHttpDependencies } from './http.js';
6
- import type { BoundedSourceProvider, DocumentationSnippet, SourceProviderSearchInput, SourceProviderSearchOutcome } from './types.js';
6
+ import type { DocumentationSnippet, SourceProviderSearchInput } from './types.js';
7
7
  export interface Context7ProviderDependencies extends ProviderHttpDependencies {
8
8
  readonly credentials: Pick<CredentialProvider, 'describe' | 'resolve'>;
9
9
  }
10
+ /** Explicit Context7 library identity and independent documentation task. */
11
+ export interface Context7ResolveRemoteInput extends SourceProviderSearchInput {
12
+ readonly libraryName: string;
13
+ }
10
14
  export type Context7Library = CachedContext7Library;
11
15
  export interface ParsedContext7Libraries {
12
16
  readonly libraries: readonly Context7Library[];
@@ -22,7 +26,7 @@ export interface ParsedContext7Snippets {
22
26
  export declare function parseContext7LibraryResponse(body: string, maximumItems: number, maximumTextCharacters: number): Readonly<ParsedContext7Libraries>;
23
27
  /** Compatibility parser with fixed defensive ceilings. */
24
28
  export declare function parseContext7Libraries(body: string): readonly Context7Library[];
25
- /** Stable selection by exact name, description relevance, trust, benchmark, and snippet coverage. */
29
+ /** Stable selection led by explicit library identity, then bounded secondary signals. */
26
30
  export declare function selectContext7Library(libraries: readonly Context7Library[], libraryName: string, query?: string): Context7Library | undefined;
27
31
  /** Parse JSON or plain-text Context7 v2 documentation bodies exactly once and bound every snippet. */
28
32
  export declare function parseContext7SnippetResponse(body: string, maximumItems: number, maximumCharacters: number): Readonly<ParsedContext7Snippets>;
@@ -39,18 +43,9 @@ export declare class Context7RemoteClient {
39
43
  private readonly credentials;
40
44
  private readonly http;
41
45
  constructor(dependencies: Context7ProviderDependencies);
42
- resolve(input: SourceProviderSearchInput): Promise<Readonly<Context7ResolveRemoteResult>>;
46
+ resolve(input: Context7ResolveRemoteInput): Promise<Readonly<Context7ResolveRemoteResult>>;
43
47
  docs(input: SourceProviderSearchInput & {
44
48
  readonly libraryId: string;
45
49
  }): Promise<Readonly<Context7DocsRemoteResult>>;
46
50
  }
47
- /** Direct resolve-then-docs adapter retained for registration-free Provider use. */
48
- export declare class Context7Provider implements BoundedSourceProvider {
49
- readonly capability = "docs_search";
50
- readonly provider = "context7";
51
- private readonly remote;
52
- constructor(dependencies: Context7ProviderDependencies);
53
- configured(): Promise<boolean>;
54
- search(input: SourceProviderSearchInput): Promise<SourceProviderSearchOutcome>;
55
- }
56
51
  //# sourceMappingURL=context7.d.ts.map
@@ -1,6 +1,5 @@
1
1
  import { isContext7LibraryId, normalizeContext7LibraryId, } from '../documentation/context7-cache.js';
2
2
  import { ProviderError, truncateCharacters } from '../provider-runtime/index.js';
3
- import { boundSourceProviderResult } from './bounded-result.js';
4
3
  import { canonicalHttpUrl, firstString, isRecord, nonEmptyQuery, positiveLimit, providerEndpoint, resolveOptionalCredential, } from './helpers.js';
5
4
  import { ProviderHttpClient } from './http.js';
6
5
  const PROVIDER = 'context7';
@@ -127,12 +126,6 @@ function normalizedTerms(value) {
127
126
  .map(normalizedMatchText)
128
127
  .filter(term => term.length > 0);
129
128
  }
130
- function queryMentionsName(queryTerms, value) {
131
- const nameTerms = normalizedTerms(value ?? '');
132
- if (nameTerms.length === 0)
133
- return false;
134
- return queryTerms.some((_term, index) => nameTerms.every((nameTerm, offset) => queryTerms[index + offset] === nameTerm));
135
- }
136
129
  function context7LibraryScore(item, libraryName, queryTerms) {
137
130
  const wanted = normalizedMatchText(libraryName);
138
131
  const rawText = `${item.id ?? ''} ${item.title ?? ''} ${item.description ?? ''}`.toLowerCase();
@@ -140,46 +133,42 @@ function context7LibraryScore(item, libraryName, queryTerms) {
140
133
  const description = normalizedMatchText(item.description);
141
134
  const idSegments = (item.id ?? '').split('/').filter(Boolean).map(normalizedMatchText);
142
135
  let score = 0;
143
- if (queryMentionsName(queryTerms, item.title))
144
- score += 240;
145
- for (const segment of (item.id ?? '').split('/').filter(Boolean)) {
146
- if (queryMentionsName(queryTerms, segment))
147
- score += 220;
148
- }
149
- if (/official.*documentation|documentation.*official|official docs/.test(rawText))
150
- score += 260;
151
136
  if (wanted.length > 0) {
152
- if (title === wanted)
153
- score += 240;
154
- if (idSegments.some(segment => segment === wanted))
155
- score += 220;
156
- if (idSegments.at(-1) === wanted)
157
- score += 40;
158
- if (title.startsWith(wanted))
159
- score += 50;
160
- if (title.includes(wanted))
161
- score += 20;
137
+ const titleIdentity = title === wanted
138
+ ? 1000
139
+ : title.startsWith(wanted)
140
+ ? 200
141
+ : title.includes(wanted)
142
+ ? 100
143
+ : 0;
144
+ const idIdentity = idSegments.some(segment => segment === wanted)
145
+ ? 900 + (idSegments.at(-1) === wanted ? 100 : 0)
146
+ : 0;
147
+ score += Math.max(titleIdentity, idIdentity);
162
148
  if (description.includes(wanted))
163
- score += 8;
149
+ score += 15;
164
150
  }
165
- for (const normalizedTerm of queryTerms) {
166
- if (normalizedTerm.length <= 1)
151
+ let queryRelevance = 0;
152
+ for (const term of queryTerms) {
153
+ if (term.length <= 1)
167
154
  continue;
168
- if (title === normalizedTerm)
169
- score += 24;
170
- else if (title.includes(normalizedTerm))
171
- score += 2;
172
- if (idSegments.some(segment => segment === normalizedTerm))
173
- score += 20;
174
- if (description.includes(normalizedTerm))
175
- score += 1;
155
+ if (title === term)
156
+ queryRelevance += 3;
157
+ else if (title.includes(term))
158
+ queryRelevance += 1;
159
+ if (idSegments.some(segment => segment === term))
160
+ queryRelevance += 2;
161
+ if (description.includes(term))
162
+ queryRelevance += 1;
176
163
  }
164
+ score += Math.min(30, queryRelevance);
177
165
  if (/official|documentation|docs/.test(rawText))
178
166
  score += 10;
179
167
  if (item.trustScore !== undefined)
180
- score += item.trustScore * 2;
181
- if (item.benchmarkScore !== undefined)
182
- score += item.benchmarkScore / 2;
168
+ score += Math.min(20, Math.max(0, item.trustScore) * 2);
169
+ if (item.benchmarkScore !== undefined) {
170
+ score += Math.min(50, Math.max(0, item.benchmarkScore) / 2);
171
+ }
183
172
  if (item.totalSnippets !== undefined) {
184
173
  score += Math.min(30, Math.log10(Math.max(1, item.totalSnippets)) * 8);
185
174
  }
@@ -187,7 +176,7 @@ function context7LibraryScore(item, libraryName, queryTerms) {
187
176
  score += Math.min(20, Math.log10(Math.max(1, item.stars)) * 4);
188
177
  return score;
189
178
  }
190
- /** Stable selection by exact name, description relevance, trust, benchmark, and snippet coverage. */
179
+ /** Stable selection led by explicit library identity, then bounded secondary signals. */
191
180
  export function selectContext7Library(libraries, libraryName, query = libraryName) {
192
181
  const queryTerms = normalizedTerms(query);
193
182
  return libraries
@@ -304,12 +293,14 @@ export class Context7RemoteClient {
304
293
  this.http = new ProviderHttpClient(dependencies);
305
294
  }
306
295
  async resolve(input) {
296
+ const libraryName = nonEmptyQuery(input.libraryName, PROVIDER, CAPABILITY);
307
297
  const query = nonEmptyQuery(input.query, PROVIDER, CAPABILITY);
308
298
  const limit = positiveLimit(input.limit, PROVIDER, CAPABILITY);
309
299
  const providerConfig = input.config.providers.context7;
310
300
  const credential = await resolveOptionalCredential(this.credentials, providerConfig.credentialRef, input.signal, PROVIDER, CAPABILITY);
311
- const resolveUrl = new URL(providerEndpoint(providerConfig.baseUrl, '/api/v2/search'));
301
+ const resolveUrl = new URL(providerEndpoint(providerConfig.baseUrl, '/api/v2/libs/search'));
312
302
  resolveUrl.searchParams.set('query', query);
303
+ resolveUrl.searchParams.set('libraryName', libraryName);
313
304
  const response = await this.http.requestText({
314
305
  capability: CAPABILITY,
315
306
  endpoint: resolveUrl.href,
@@ -362,60 +353,4 @@ export class Context7RemoteClient {
362
353
  });
363
354
  }
364
355
  }
365
- /** Direct resolve-then-docs adapter retained for registration-free Provider use. */
366
- export class Context7Provider {
367
- capability = CAPABILITY;
368
- provider = PROVIDER;
369
- remote;
370
- constructor(dependencies) {
371
- this.remote = new Context7RemoteClient(dependencies);
372
- }
373
- async configured() {
374
- return true;
375
- }
376
- async search(input) {
377
- const query = nonEmptyQuery(input.query, PROVIDER, CAPABILITY);
378
- const limit = positiveLimit(input.limit, PROVIDER, CAPABILITY);
379
- const resolved = await this.remote.resolve({ ...input, query, limit });
380
- const selected = selectContext7Library(resolved.libraries, query, query);
381
- if (selected?.id === undefined || !isContext7LibraryId(selected.id)) {
382
- return Object.freeze({
383
- attempts: resolved.attempts,
384
- result: boundSourceProviderResult({
385
- capability: CAPABILITY,
386
- config: input.config,
387
- inputTruncated: resolved.truncated,
388
- provider: PROVIDER,
389
- requestedSources: limit,
390
- responseBytes: resolved.responseBytes,
391
- sources: [],
392
- }),
393
- state: 'complete',
394
- totalDelayMs: resolved.totalDelayMs,
395
- });
396
- }
397
- const libraryId = normalizeContext7LibraryId(selected.id);
398
- const docs = await this.remote.docs({ ...input, libraryId, query, limit });
399
- const snippets = docs.snippets.map(snippet => Object.freeze({
400
- ...snippet,
401
- libraryId,
402
- }));
403
- const source = context7LibrarySource(selected, input.config.providers.context7.baseUrl, input.config.webExtract.maxUrlCharacters, snippets[0]?.content ?? selected.description);
404
- return Object.freeze({
405
- attempts: resolved.attempts + docs.attempts,
406
- result: boundSourceProviderResult({
407
- capability: CAPABILITY,
408
- config: input.config,
409
- inputTruncated: resolved.truncated || docs.truncated,
410
- provider: PROVIDER,
411
- requestedSources: limit,
412
- responseBytes: resolved.responseBytes + docs.responseBytes,
413
- snippets,
414
- sources: source === undefined ? [] : [source],
415
- }),
416
- state: 'complete',
417
- totalDelayMs: resolved.totalDelayMs + docs.totalDelayMs,
418
- });
419
- }
420
- }
421
356
  //# sourceMappingURL=context7.js.map
@@ -1,5 +1,5 @@
1
1
  export { boundSourceProviderResult, type BoundSourceProviderResultInput, } from './bounded-result.js';
2
- export { Context7Provider, Context7RemoteClient, context7LibrarySource, context7LibraryUrl, parseContext7Libraries, parseContext7LibraryResponse, parseContext7Snippets, parseContext7SnippetResponse, selectContext7Library, type Context7Library, type Context7ProviderDependencies, type ParsedContext7Libraries, type ParsedContext7Snippets, } from './context7.js';
2
+ export { Context7RemoteClient, context7LibrarySource, context7LibraryUrl, parseContext7Libraries, parseContext7LibraryResponse, parseContext7Snippets, parseContext7SnippetResponse, selectContext7Library, type Context7Library, type Context7ProviderDependencies, type Context7ResolveRemoteInput, type ParsedContext7Libraries, type ParsedContext7Snippets, } from './context7.js';
3
3
  export { SmartDirectAdapter, SmartDirectProvider, type SmartDirectProviderDependencies, } from './smart-direct.js';
4
4
  export { SMART_DIRECT_BROWSER_PROFILES, SMART_DIRECT_OPERATING_SYSTEMS, type SmartDirectBrowserProfile, type SmartDirectOperatingSystem, } from './smart-direct-profiles.js';
5
5
  export { DirectFetchAdapter, DirectFetchProvider, type DirectFetchProviderDependencies, } from './direct-fetch.js';
@@ -1,5 +1,5 @@
1
1
  export { boundSourceProviderResult, } from './bounded-result.js';
2
- export { Context7Provider, Context7RemoteClient, context7LibrarySource, context7LibraryUrl, parseContext7Libraries, parseContext7LibraryResponse, parseContext7Snippets, parseContext7SnippetResponse, selectContext7Library, } from './context7.js';
2
+ export { Context7RemoteClient, context7LibrarySource, context7LibraryUrl, parseContext7Libraries, parseContext7LibraryResponse, parseContext7Snippets, parseContext7SnippetResponse, selectContext7Library, } from './context7.js';
3
3
  export { SmartDirectAdapter, SmartDirectProvider, } from './smart-direct.js';
4
4
  export { SMART_DIRECT_BROWSER_PROFILES, SMART_DIRECT_OPERATING_SYSTEMS, } from './smart-direct-profiles.js';
5
5
  export { DirectFetchAdapter, DirectFetchProvider, } from './direct-fetch.js';
@@ -31,7 +31,7 @@ export interface ParsedSearchApiResponse {
31
31
  }
32
32
  /** Extract URL-validated inline Markdown citations in answer order for internal orchestration. */
33
33
  export declare function extractMarkdownCitationUrls(text: string, maximumUrlCharacters?: number): readonly string[];
34
- /** Convert Search API prose/source conventions immediately into provider-neutral records. */
34
+ /** Convert Search API prose/source conventions with one bounded link scan and exact-URL enrichment. */
35
35
  export declare function parseSearchAnswerText(text: string, limitOverrides?: Partial<SearchResponseParseLimits>): ParsedSearchApiResponse;
36
36
  /**
37
37
  * Parse a complete, already-bounded Search API body (SSE or non-streaming JSON).
@@ -28,8 +28,9 @@ export const DEFAULT_SEARCH_RESPONSE_PARSE_LIMITS = Object.freeze({
28
28
  maxTitleCharacters: 1000,
29
29
  maxUrlCharacters: 8192,
30
30
  });
31
- const URL_PATTERN = /https?:\/\/[^\s<>"'`,。、;:!?》)】)]+/g;
32
- const MARKDOWN_LINK_PATTERN = /(?<!\[)\[([^\[\]]+|\[\d+\])]\((https?:\/\/[^)]+)\)/g;
31
+ const BARE_URL_PATTERN = /https?:\/\/[^\s<>"'`,。、;:!?》)】]+/g;
32
+ const MARKDOWN_URL_BOUNDARIES = new Set(['<', '>', '"', "'", '`', ',', '。', '、', ';', ':', '!', '?', '》', ')', '】']);
33
+ const URL_TRAILING_PUNCTUATION = new Set(['.', ',', ';', ':', '!', '?']);
33
34
  const SOURCES_HEADING_PATTERN = /(?:^|\n)(?:#{1,6}\s*)?(?:\*\*|__)?\s*(?:sources?|references?|citations?|信源|参考资料|参考|引用|来源列表|来源)(?:\s*[((][^)\n]*[))])?\s*(?:(?:\*\*|__)\s*)?[::]?\s*(?:(?:\*\*|__)\s*)?$/gim;
34
35
  const SOURCES_FUNCTION_PATTERN = /(^|\n)\s*(sources|source|citations|citation|references|reference|citation_card|source_cards|source_card)\s*\(/gim;
35
36
  function isRecord(value) {
@@ -77,8 +78,172 @@ function firstBoundedString(record, keys, maximum) {
77
78
  }
78
79
  return undefined;
79
80
  }
81
+ function isAsciiDigit(character) {
82
+ return character !== undefined && character >= '0' && character <= '9';
83
+ }
84
+ function isCitationOrdinalLabel(label) {
85
+ const trimmed = label.trim();
86
+ if (trimmed.length === 0)
87
+ return false;
88
+ for (let index = 0; index < trimmed.length; index += 1) {
89
+ if (!isAsciiDigit(trimmed[index]))
90
+ return false;
91
+ }
92
+ return true;
93
+ }
94
+ function isMarkdownUrlBoundary(character) {
95
+ return /\s/u.test(character) || MARKDOWN_URL_BOUNDARIES.has(character);
96
+ }
97
+ /** Scan the supported Markdown HTTP(S) link subset once without recursive parsing. */
98
+ function scanMarkdownHttpLinks(text) {
99
+ const links = [];
100
+ const intervals = [];
101
+ const scanDestination = (urlStart) => {
102
+ if (!text.startsWith('http://', urlStart) && !text.startsWith('https://', urlStart)) {
103
+ return { resume: urlStart };
104
+ }
105
+ let depth = 0;
106
+ for (let index = urlStart; index < text.length; index += 1) {
107
+ const character = text[index];
108
+ if (character === undefined)
109
+ break;
110
+ if (isMarkdownUrlBoundary(character))
111
+ return { resume: index + 1 };
112
+ if (character === '(') {
113
+ depth += 1;
114
+ }
115
+ else if (character === ')') {
116
+ if (depth === 0) {
117
+ return {
118
+ end: index + 1,
119
+ resume: index + 1,
120
+ url: text.slice(urlStart, index),
121
+ };
122
+ }
123
+ depth -= 1;
124
+ }
125
+ }
126
+ return { resume: text.length };
127
+ };
128
+ let cursor = 0;
129
+ while (cursor < text.length) {
130
+ const start = text.indexOf('[', cursor);
131
+ if (start === -1)
132
+ break;
133
+ if (start > 0 && text[start - 1] === '[') {
134
+ cursor = start + 1;
135
+ continue;
136
+ }
137
+ let label;
138
+ let citationOrdinal;
139
+ let destinationOpen;
140
+ if (text[start + 1] === '[') {
141
+ const digitsStart = start + 2;
142
+ let labelEnd = digitsStart;
143
+ while (isAsciiDigit(text[labelEnd]))
144
+ labelEnd += 1;
145
+ if (labelEnd === digitsStart
146
+ || text[labelEnd] !== ']'
147
+ || text[labelEnd + 1] !== ']'
148
+ || text[labelEnd + 2] !== '(') {
149
+ labelEnd = text[digitsStart] === '[' ? digitsStart + 1 : digitsStart;
150
+ while (labelEnd < text.length
151
+ && text[labelEnd] !== ']'
152
+ && text[labelEnd] !== '['
153
+ && !isMarkdownUrlBoundary(text[labelEnd] ?? ''))
154
+ labelEnd += 1;
155
+ if (text[labelEnd] === ']'
156
+ && text[labelEnd + 1] === ']'
157
+ && text[labelEnd + 2] === '(') {
158
+ const rejected = scanDestination(labelEnd + 3);
159
+ if (rejected.end !== undefined) {
160
+ intervals.push(Object.freeze({ end: rejected.end, start }));
161
+ }
162
+ cursor = rejected.resume;
163
+ }
164
+ else {
165
+ cursor = text[labelEnd] === '[' ? labelEnd : labelEnd + 1;
166
+ }
167
+ continue;
168
+ }
169
+ label = text.slice(digitsStart, labelEnd);
170
+ citationOrdinal = true;
171
+ destinationOpen = labelEnd + 2;
172
+ }
173
+ else {
174
+ const labelStart = start + 1;
175
+ let labelEnd = labelStart;
176
+ while (labelEnd < text.length
177
+ && text[labelEnd] !== ']'
178
+ && text[labelEnd] !== '[')
179
+ labelEnd += 1;
180
+ if (labelEnd === labelStart
181
+ || text[labelEnd] !== ']'
182
+ || text[labelEnd + 1] !== '(') {
183
+ cursor = text[labelEnd] === '[' ? labelEnd : labelEnd + 1;
184
+ continue;
185
+ }
186
+ label = text.slice(labelStart, labelEnd);
187
+ citationOrdinal = isCitationOrdinalLabel(label);
188
+ destinationOpen = labelEnd + 1;
189
+ }
190
+ const destination = scanDestination(destinationOpen + 1);
191
+ if (destination.end !== undefined && destination.url !== undefined) {
192
+ const link = Object.freeze({
193
+ citationOrdinal,
194
+ end: destination.end,
195
+ label,
196
+ start,
197
+ url: destination.url,
198
+ });
199
+ links.push(link);
200
+ intervals.push(link);
201
+ }
202
+ cursor = destination.resume;
203
+ }
204
+ return Object.freeze({
205
+ intervals: Object.freeze(intervals),
206
+ links: Object.freeze(links),
207
+ });
208
+ }
209
+ /** Remove only sentence punctuation and unmatched ASCII closing parens at the URL tail. */
210
+ function trimUrlTail(raw) {
211
+ const value = raw.trim();
212
+ const unmatchedClosings = [];
213
+ let depth = 0;
214
+ for (let index = 0; index < value.length; index += 1) {
215
+ const character = value[index];
216
+ if (character === '(')
217
+ depth += 1;
218
+ else if (character === ')') {
219
+ if (depth === 0)
220
+ unmatchedClosings.push(index);
221
+ else
222
+ depth -= 1;
223
+ }
224
+ }
225
+ let end = value.length;
226
+ let unmatchedIndex = unmatchedClosings.length - 1;
227
+ let changed = true;
228
+ while (changed) {
229
+ changed = false;
230
+ while (end > 0 && URL_TRAILING_PUNCTUATION.has(value[end - 1] ?? '')) {
231
+ end -= 1;
232
+ changed = true;
233
+ }
234
+ while (unmatchedIndex >= 0 && (unmatchedClosings[unmatchedIndex] ?? -1) >= end) {
235
+ unmatchedIndex -= 1;
236
+ }
237
+ if (unmatchedClosings[unmatchedIndex] === end - 1) {
238
+ end -= 1;
239
+ unmatchedIndex -= 1;
240
+ changed = true;
241
+ }
242
+ }
243
+ return value.slice(0, end);
244
+ }
80
245
  function validatedHttpUrl(raw, limits) {
81
- const url = raw.trim().replace(/[.,;:!?]+$/, '');
246
+ const url = trimUrlTail(raw);
82
247
  const actual = characterLength(url);
83
248
  if (actual > limits.maxUrlCharacters) {
84
249
  throw new SearchResponseParseError('limit', { actual, maximum: limits.maxUrlCharacters });
@@ -95,13 +260,31 @@ function validatedHttpUrl(raw, limits) {
95
260
  }
96
261
  function addSource(state, candidate, limits) {
97
262
  const url = validatedHttpUrl(candidate.url, limits);
98
- if (url === undefined || state.seen.has(url))
263
+ if (url === undefined)
99
264
  return;
100
- state.seen.add(url);
265
+ const existingIndex = state.sourceIndexByUrl.get(url);
266
+ if (existingIndex !== undefined) {
267
+ const existing = state.sources[existingIndex];
268
+ const title = existing.title ?? candidate.title;
269
+ const snippet = existing.snippet ?? candidate.snippet;
270
+ const publishedAt = existing.publishedAt ?? candidate.publishedAt;
271
+ if (title !== existing.title
272
+ || snippet !== existing.snippet
273
+ || publishedAt !== existing.publishedAt) {
274
+ state.sources[existingIndex] = Object.freeze({
275
+ ...existing,
276
+ ...(title === undefined ? {} : { title }),
277
+ ...(snippet === undefined ? {} : { snippet }),
278
+ ...(publishedAt === undefined ? {} : { publishedAt }),
279
+ });
280
+ }
281
+ return;
282
+ }
101
283
  if (state.sources.length >= limits.maxSources) {
102
284
  state.truncated = true;
103
285
  return;
104
286
  }
287
+ const index = state.sources.length;
105
288
  state.sources.push(Object.freeze({
106
289
  provider: 'search-api',
107
290
  url,
@@ -109,40 +292,50 @@ function addSource(state, candidate, limits) {
109
292
  ...(candidate.snippet === undefined ? {} : { snippet: candidate.snippet }),
110
293
  ...(candidate.publishedAt === undefined ? {} : { publishedAt: candidate.publishedAt }),
111
294
  }));
295
+ state.sourceIndexByUrl.set(url, index);
112
296
  }
113
297
  function extractUrls(text) {
114
298
  const urls = [];
115
- const seen = new Set();
116
- for (const match of text.matchAll(URL_PATTERN)) {
299
+ for (const match of text.matchAll(BARE_URL_PATTERN)) {
117
300
  const raw = match[0];
118
301
  if (raw === undefined)
119
302
  continue;
120
- const url = raw.replace(/[.,;:!?]+$/, '');
121
- if (seen.has(url))
122
- continue;
123
- seen.add(url);
124
- urls.push(url);
303
+ urls.push(raw);
125
304
  }
126
305
  return urls;
127
306
  }
128
- function addMarkdownSources(text, state, limits) {
129
- for (const match of text.matchAll(MARKDOWN_LINK_PATTERN)) {
130
- const rawTitle = match[1];
131
- const rawUrl = match[2];
132
- if (rawTitle === undefined || rawUrl === undefined)
133
- continue;
134
- const title = boundedOptionalString(rawTitle, limits.maxTitleCharacters);
307
+ function addMarkdownSources(links, state, limits) {
308
+ for (const link of links) {
309
+ const title = link.citationOrdinal
310
+ ? undefined
311
+ : boundedOptionalString(link.label, limits.maxTitleCharacters);
135
312
  addSource(state, {
136
- url: rawUrl,
313
+ url: link.url,
137
314
  ...(title === undefined ? {} : { title }),
138
315
  }, limits);
139
316
  }
140
317
  }
318
+ function maskMarkdownLinkIntervals(text, intervals) {
319
+ if (intervals.length === 0)
320
+ return text;
321
+ const parts = [];
322
+ let cursor = 0;
323
+ for (const link of intervals) {
324
+ parts.push(text.slice(cursor, link.start), ' ');
325
+ cursor = link.end;
326
+ }
327
+ parts.push(text.slice(cursor));
328
+ return parts.join('');
329
+ }
141
330
  /** Extract URL-validated inline Markdown citations in answer order for internal orchestration. */
142
331
  export function extractMarkdownCitationUrls(text, maximumUrlCharacters = DEFAULT_SEARCH_RESPONSE_PARSE_LIMITS.maxUrlCharacters) {
143
332
  assertSafeInteger(maximumUrlCharacters, 'maximumUrlCharacters', false);
144
- const state = { seen: new Set(), sources: [], truncated: false };
145
- addMarkdownSources(text, state, {
333
+ const state = {
334
+ sourceIndexByUrl: new Map(),
335
+ sources: [],
336
+ truncated: false,
337
+ };
338
+ addMarkdownSources(scanMarkdownHttpLinks(text).links, state, {
146
339
  ...DEFAULT_SEARCH_RESPONSE_PARSE_LIMITS,
147
340
  maxSources: Number.MAX_SAFE_INTEGER,
148
341
  maxUrlCharacters: maximumUrlCharacters,
@@ -150,8 +343,9 @@ export function extractMarkdownCitationUrls(text, maximumUrlCharacters = DEFAULT
150
343
  return Object.freeze(state.sources.map(source => source.url));
151
344
  }
152
345
  function addTextSources(text, state, limits) {
153
- addMarkdownSources(text, state, limits);
154
- const bareText = text.replace(MARKDOWN_LINK_PATTERN, ' ');
346
+ const markdown = scanMarkdownHttpLinks(text);
347
+ addMarkdownSources(markdown.links, state, limits);
348
+ const bareText = maskMarkdownLinkIntervals(text, markdown.intervals);
155
349
  for (const url of extractUrls(bareText))
156
350
  addSource(state, { url }, limits);
157
351
  }
@@ -322,7 +516,11 @@ function splitDetailsBlockSources(text, state, limits) {
322
516
  const openIndex = lower.lastIndexOf('<details', closeIndex);
323
517
  if (openIndex === -1)
324
518
  return undefined;
325
- const temporary = { seen: new Set(), sources: [], truncated: false };
519
+ const temporary = {
520
+ sourceIndexByUrl: new Map(),
521
+ sources: [],
522
+ truncated: false,
523
+ };
326
524
  addTextSources(text.slice(openIndex, closeIndex + '</details>'.length), temporary, limits);
327
525
  if (temporary.sources.length < 2 && !temporary.truncated)
328
526
  return undefined;
@@ -336,7 +534,7 @@ function isLinkOnlyLine(line) {
336
534
  const stripped = line.replace(/^\s*(?:[-*]|\d+\.)\s*/, '').trim();
337
535
  return stripped.length > 0 && (stripped.startsWith('http://')
338
536
  || stripped.startsWith('https://')
339
- || stripped.search(MARKDOWN_LINK_PATTERN) !== -1);
537
+ || scanMarkdownHttpLinks(stripped).links.length > 0);
340
538
  }
341
539
  function splitTailLinkBlock(text, state, limits) {
342
540
  const lines = text.split(/\r?\n/);
@@ -367,7 +565,7 @@ function splitTailLinkBlock(text, state, limits) {
367
565
  return undefined;
368
566
  return lines.slice(0, tailStart).join('\n').trimEnd();
369
567
  }
370
- /** Convert Search API prose/source conventions immediately into provider-neutral records. */
568
+ /** Convert Search API prose/source conventions with one bounded link scan and exact-URL enrichment. */
371
569
  export function parseSearchAnswerText(text, limitOverrides = {}) {
372
570
  const limits = { ...DEFAULT_SEARCH_RESPONSE_PARSE_LIMITS, ...limitOverrides };
373
571
  validateLimits(limits);
@@ -375,14 +573,22 @@ export function parseSearchAnswerText(text, limitOverrides = {}) {
375
573
  if (trimmed.length === 0) {
376
574
  return Object.freeze({ answer: '', sources: Object.freeze([]), sourcesTruncated: false });
377
575
  }
378
- const trailingState = { seen: new Set(), sources: [], truncated: false };
576
+ const trailingState = {
577
+ sourceIndexByUrl: new Map(),
578
+ sources: [],
579
+ truncated: false,
580
+ };
379
581
  const answer = splitFunctionCallSources(trimmed, trailingState, limits)
380
582
  ?? splitHeadingSources(trimmed, trailingState, limits)
381
583
  ?? splitDetailsBlockSources(trimmed, trailingState, limits)
382
584
  ?? splitTailLinkBlock(trimmed, trailingState, limits)
383
585
  ?? trimmed;
384
- const state = { seen: new Set(), sources: [], truncated: false };
385
- addMarkdownSources(answer, state, limits);
586
+ const state = {
587
+ sourceIndexByUrl: new Map(),
588
+ sources: [],
589
+ truncated: false,
590
+ };
591
+ addMarkdownSources(scanMarkdownHttpLinks(answer).links, state, limits);
386
592
  for (const source of trailingState.sources)
387
593
  addSource(state, source, limits);
388
594
  state.truncated ||= trailingState.truncated;
@@ -179,6 +179,7 @@ async function executeDocsSearch(args, exec, dependencies, signal) {
179
179
  const result = await dependencies.documentation.search({
180
180
  query: args.query,
181
181
  ...(args.provider === undefined ? {} : { provider: args.provider }),
182
+ ...(args.library_name === undefined ? {} : { libraryName: args.library_name }),
182
183
  ...(args.library_id === undefined ? {} : { libraryId: args.library_id }),
183
184
  maxResults,
184
185
  ...(args.force_refresh === undefined ? {} : { forceRefresh: args.force_refresh }),
@@ -197,7 +198,7 @@ async function executeDocsSearch(args, exec, dependencies, signal) {
197
198
  export function createDocsSearchTool(dependencies) {
198
199
  return defineTool({
199
200
  name: 'docs_search',
200
- description: 'High-level SDK/API/framework/README/release documentation search. Routes Context7 first and optionally Exa, returns discovery snippets and durable source_ref pagination; common documentation tasks do not need granular Context7 tools.',
201
+ description: 'High-level SDK/API/framework/README/release documentation search. Pass library_id for an exact Context7 id or library_name for a package/product; auto without either uses Exa discovery, while context7/all require one. Returns bounded discovery snippets and durable source_ref pagination.',
201
202
  parameters: DOCS_SEARCH_PARAMETERS,
202
203
  output: {
203
204
  schema: DOCS_SEARCH_OUTPUT_SCHEMA,
@@ -153,11 +153,15 @@ export declare const DOCS_SEARCH_PARAMETERS: {
153
153
  readonly provider: {
154
154
  readonly type: "string";
155
155
  readonly enum: readonly ["auto", "context7", "exa", "all"];
156
- readonly description: "auto (default), Context7, Exa, or both documentation discovery routes.";
156
+ readonly description: "auto (default), Context7, Exa, or both. Without a Context7 library identity, auto is Exa-only.";
157
+ };
158
+ readonly library_name: {
159
+ readonly type: "string";
160
+ readonly description: "Optional Context7 package or product name, such as React or FastAPI. Required for Context7 resolve when library_id is absent; never sent to Exa.";
157
161
  };
158
162
  readonly library_id: {
159
163
  readonly type: "string";
160
- readonly description: "Optional exact Context7 /org/project or /org/project/version id; valid ids skip resolve.";
164
+ readonly description: "Optional exact Context7 /org/project or /org/project/version id; takes priority over library_name and strictly skips resolve.";
161
165
  };
162
166
  readonly max_results: {
163
167
  readonly type: "integer";
@@ -144,11 +144,15 @@ export const DOCS_SEARCH_PARAMETERS = {
144
144
  provider: {
145
145
  type: 'string',
146
146
  enum: DOCUMENTATION_SEARCH_PROVIDERS,
147
- description: 'auto (default), Context7, Exa, or both documentation discovery routes.',
147
+ description: 'auto (default), Context7, Exa, or both. Without a Context7 library identity, auto is Exa-only.',
148
+ },
149
+ library_name: {
150
+ type: 'string',
151
+ description: 'Optional Context7 package or product name, such as React or FastAPI. Required for Context7 resolve when library_id is absent; never sent to Exa.',
148
152
  },
149
153
  library_id: {
150
154
  type: 'string',
151
- description: 'Optional exact Context7 /org/project or /org/project/version id; valid ids skip resolve.',
155
+ description: 'Optional exact Context7 /org/project or /org/project/version id; takes priority over library_name and strictly skips resolve.',
152
156
  },
153
157
  max_results: {
154
158
  type: 'integer',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-search-enhance",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Bounded search, documentation, extraction, progressively disclosed site mapping/research planning, and read-only diagnostics for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",