@blxzer/cursor-trellis 0.1.3 → 0.2.1

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 (74) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +45 -23
  3. package/dist/configurators/workflow.d.ts +1 -1
  4. package/dist/configurators/workflow.d.ts.map +1 -1
  5. package/dist/configurators/workflow.js +20 -9
  6. package/dist/configurators/workflow.js.map +1 -1
  7. package/dist/migrations/manifests/0.1.3.json +9 -0
  8. package/dist/migrations/manifests/0.1.4.json +9 -0
  9. package/dist/templates/markdown/agents.md +1 -1
  10. package/dist/templates/markdown/index.d.ts +1 -0
  11. package/dist/templates/markdown/index.d.ts.map +1 -1
  12. package/dist/templates/markdown/index.js +1 -0
  13. package/dist/templates/markdown/index.js.map +1 -1
  14. package/dist/templates/markdown/prompts/run-semantic-slice-12.md.txt +13 -29
  15. package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +0 -49
  16. package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +0 -85
  17. package/dist/templates/markdown/spec/guides/cursor-semantic-compliance.md.txt +7 -21
  18. package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +6 -10
  19. package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +7 -13
  20. package/dist/templates/shared-hooks/inject-retrieval-plan.py +42 -23
  21. package/dist/templates/shared-hooks/inject-subagent-context.py +6 -2
  22. package/dist/templates/shared-hooks/session-start.py +17 -9
  23. package/dist/templates/trellis/index.d.ts +7 -4
  24. package/dist/templates/trellis/index.d.ts.map +1 -1
  25. package/dist/templates/trellis/index.js +11 -7
  26. package/dist/templates/trellis/index.js.map +1 -1
  27. package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +2 -2
  28. package/dist/templates/trellis/scripts/common/config.py +1 -1
  29. package/dist/templates/trellis/scripts/common/parent_orchestration.py +27 -15
  30. package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +1 -1
  31. package/dist/templates/trellis/scripts/common/smart_search_resolve.py +28 -3
  32. package/dist/templates/trellis/scripts/common/task_gates.py +372 -73
  33. package/dist/templates/trellis/scripts/common/task_map.py +29 -1
  34. package/dist/templates/trellis/workflow.md +9 -53
  35. package/package.json +5 -44
  36. package/bin/smart-search.js +0 -79
  37. package/dist/templates/trellis/scripts/aggregate_retrieval_telemetry.py +0 -342
  38. package/dist/templates/trellis/scripts/batch_plan_envelope.py +0 -105
  39. package/vendor/smart-search/LICENSE +0 -21
  40. package/vendor/smart-search/README.md +0 -412
  41. package/vendor/smart-search/README.zh-CN.md +0 -430
  42. package/vendor/smart-search/npm/bin/smart-search.js +0 -63
  43. package/vendor/smart-search/npm/scripts/postinstall.js +0 -87
  44. package/vendor/smart-search/npm/scripts/resolve-prerelease-version.js +0 -108
  45. package/vendor/smart-search/npm/scripts/set-package-version.js +0 -35
  46. package/vendor/smart-search/npm/scripts/sync-python-version.js +0 -22
  47. package/vendor/smart-search/npm/scripts/test-wrapper-repair.js +0 -99
  48. package/vendor/smart-search/npm/scripts/test.js +0 -85
  49. package/vendor/smart-search/package.json +0 -42
  50. package/vendor/smart-search/pyproject.toml +0 -37
  51. package/vendor/smart-search/skills/smart-search-cli/SKILL.md +0 -387
  52. package/vendor/smart-search/skills/smart-search-cli/agents/openai.yaml +0 -3
  53. package/vendor/smart-search/skills/smart-search-cli/examples/batch-search.md +0 -98
  54. package/vendor/smart-search/skills/smart-search-cli/examples/evidence-gathering.md +0 -89
  55. package/vendor/smart-search/skills/smart-search-cli/references/cli-contract.md +0 -337
  56. package/vendor/smart-search/src/smart_search/__init__.py +0 -1
  57. package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/SKILL.md +0 -387
  58. package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/agents/openai.yaml +0 -3
  59. package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/examples/batch-search.md +0 -98
  60. package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/examples/evidence-gathering.md +0 -89
  61. package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/references/cli-contract.md +0 -337
  62. package/vendor/smart-search/src/smart_search/cli.py +0 -2003
  63. package/vendor/smart-search/src/smart_search/config.py +0 -580
  64. package/vendor/smart-search/src/smart_search/logger.py +0 -43
  65. package/vendor/smart-search/src/smart_search/providers/__init__.py +0 -16
  66. package/vendor/smart-search/src/smart_search/providers/base.py +0 -41
  67. package/vendor/smart-search/src/smart_search/providers/context7.py +0 -141
  68. package/vendor/smart-search/src/smart_search/providers/exa.py +0 -206
  69. package/vendor/smart-search/src/smart_search/providers/jina.py +0 -136
  70. package/vendor/smart-search/src/smart_search/providers/openai_compatible.py +0 -370
  71. package/vendor/smart-search/src/smart_search/providers/zhipu.py +0 -143
  72. package/vendor/smart-search/src/smart_search/service.py +0 -2865
  73. package/vendor/smart-search/src/smart_search/sources.py +0 -429
  74. package/vendor/smart-search/src/smart_search/utils.py +0 -220
@@ -1,580 +0,0 @@
1
- import json
2
- import os
3
- import sys
4
- from pathlib import Path
5
-
6
- class Config:
7
- _instance = None
8
- _SETUP_COMMAND = (
9
- "Run `smart-search setup`, or configure OPENAI_COMPATIBLE_API_URL plus "
10
- "OPENAI_COMPATIBLE_API_KEY, then run `smart-search doctor --format json`."
11
- )
12
- _DEFAULT_MODEL = "grok-4.20-multi-agent-xhigh"
13
- _DEFAULT_VALIDATION_LEVEL = "balanced"
14
- _DEFAULT_FALLBACK_MODE = "auto"
15
- _DEFAULT_MINIMUM_PROFILE = "standard"
16
- _ALLOWED_VALIDATION_LEVELS = {"fast", "balanced", "strict"}
17
- _ALLOWED_FALLBACK_MODES = {"auto", "off"}
18
- _ALLOWED_MINIMUM_PROFILES = {"standard", "off"}
19
- _CONFIG_KEYS = {
20
- "OPENAI_COMPATIBLE_API_URL",
21
- "OPENAI_COMPATIBLE_API_KEY",
22
- "OPENAI_COMPATIBLE_MODEL",
23
- "OPENAI_COMPATIBLE_STREAM",
24
- "SMART_SEARCH_VALIDATION_LEVEL",
25
- "SMART_SEARCH_FALLBACK_MODE",
26
- "SMART_SEARCH_MINIMUM_PROFILE",
27
- "SMART_SEARCH_RESEARCH_PREFERRED_PROVIDERS",
28
- "SMART_SEARCH_RESEARCH_DISABLED_PROVIDERS",
29
- "EXA_API_KEY",
30
- "EXA_BASE_URL",
31
- "EXA_TIMEOUT_SECONDS",
32
- "CONTEXT7_API_KEY",
33
- "CONTEXT7_BASE_URL",
34
- "CONTEXT7_TIMEOUT_SECONDS",
35
- "ZHIPU_API_KEY",
36
- "ZHIPU_API_URL",
37
- "ZHIPU_SEARCH_ENGINE",
38
- "ZHIPU_TIMEOUT_SECONDS",
39
- "JINA_API_KEY",
40
- "JINA_READER_API_URL",
41
- "JINA_RESPOND_WITH",
42
- "JINA_TIMEOUT_SECONDS",
43
- "TAVILY_API_KEY",
44
- "TAVILY_API_URL",
45
- "TAVILY_ENABLED",
46
- "TAVILY_TIMEOUT_SECONDS",
47
- "FIRECRAWL_API_KEY",
48
- "FIRECRAWL_API_URL",
49
- "SMART_SEARCH_DEBUG",
50
- "SMART_SEARCH_LOG_LEVEL",
51
- "SMART_SEARCH_LOG_DIR",
52
- "SMART_SEARCH_EVIDENCE_DIR",
53
- "SMART_SEARCH_RETRY_MAX_ATTEMPTS",
54
- "SMART_SEARCH_RETRY_MULTIPLIER",
55
- "SMART_SEARCH_RETRY_MAX_WAIT",
56
- "SMART_SEARCH_OUTPUT_CLEANUP",
57
- "SMART_SEARCH_LOG_TO_FILE",
58
- "SSL_VERIFY",
59
- }
60
- _LEGACY_CONFIG_KEYS: dict[str, str] = {}
61
-
62
- def __new__(cls):
63
- if cls._instance is None:
64
- cls._instance = super().__new__(cls)
65
- cls._instance._config_file = None
66
- cls._instance._config_dir_source = None
67
- cls._instance._cached_model = None
68
- return cls._instance
69
-
70
- @staticmethod
71
- def _default_config_dir() -> Path:
72
- if sys.platform.startswith("win"):
73
- local_appdata = os.getenv("LOCALAPPDATA")
74
- if local_appdata:
75
- return Path(local_appdata).expanduser() / "smart-search"
76
- return Path.home() / ".config" / "smart-search"
77
-
78
- @staticmethod
79
- def _legacy_windows_config_dir() -> Path:
80
- return Path.home() / ".config" / "smart-search"
81
-
82
- @staticmethod
83
- def _config_dir_override_value() -> str:
84
- return os.getenv("SMART_SEARCH_CONFIG_DIR") or ""
85
-
86
- @staticmethod
87
- def _same_config_dir(left: Path, right: Path) -> bool:
88
- left_text = os.path.abspath(os.path.expanduser(str(left)))
89
- right_text = os.path.abspath(os.path.expanduser(str(right)))
90
- if sys.platform.startswith("win"):
91
- left_text = left_text.replace("/", "\\").rstrip("\\").lower()
92
- right_text = right_text.replace("/", "\\").rstrip("\\").lower()
93
- else:
94
- left_text = left_text.rstrip("/")
95
- right_text = right_text.rstrip("/")
96
- return left_text == right_text
97
-
98
- @classmethod
99
- def _config_dir_override_matches_default(cls) -> bool:
100
- env_dir = cls._config_dir_override_value()
101
- if not env_dir:
102
- return False
103
- return cls._same_config_dir(Path(env_dir).expanduser(), cls._default_config_dir())
104
-
105
- @staticmethod
106
- def _resolve_config_dir() -> tuple[Path, str]:
107
- env_dir = os.getenv("SMART_SEARCH_CONFIG_DIR")
108
- if env_dir:
109
- return Path(env_dir).expanduser(), "environment"
110
- default_dir = Config._default_config_dir()
111
- if sys.platform.startswith("win"):
112
- legacy_dir = Config._legacy_windows_config_dir()
113
- if legacy_dir != default_dir and not (default_dir / "config.json").exists() and (legacy_dir / "config.json").exists():
114
- return legacy_dir, "legacy_windows_home"
115
- return default_dir, "default"
116
-
117
- @staticmethod
118
- def _safe_mkdir(p: Path) -> bool:
119
- try:
120
- p.mkdir(parents=True, exist_ok=True)
121
- return True
122
- except (PermissionError, OSError):
123
- return False
124
-
125
- @property
126
- def config_file(self) -> Path:
127
- if self._config_file is None:
128
- config_dir, config_dir_source = self._resolve_config_dir()
129
- ok = self._safe_mkdir(config_dir)
130
- if config_dir_source == "default" and not ok:
131
- cwd_dir = Path.cwd() / ".smart-search"
132
- if self._safe_mkdir(cwd_dir):
133
- config_dir = cwd_dir
134
- config_dir_source = "cwd_fallback"
135
- self._config_file = config_dir / "config.json"
136
- self._config_dir_source = config_dir_source
137
- return self._config_file
138
-
139
- @property
140
- def config_dir_source(self) -> str:
141
- if self._config_file is None:
142
- _ = self.config_file
143
- return self._config_dir_source or "override"
144
-
145
- def _load_config_file(self) -> dict:
146
- try:
147
- with open(self.config_file, 'r', encoding='utf-8') as f:
148
- data = json.load(f)
149
- return data if isinstance(data, dict) else {}
150
- except (FileNotFoundError, PermissionError, OSError, json.JSONDecodeError):
151
- return {}
152
-
153
- def _save_config_file(self, config_data: dict) -> None:
154
- try:
155
- with open(self.config_file, 'w', encoding='utf-8') as f:
156
- json.dump(config_data, f, ensure_ascii=False, indent=2)
157
- except (IOError, PermissionError, OSError) as e:
158
- hint = " (sandbox/CI 下可设 SMART_SEARCH_CONFIG_DIR 指向可写目录)" if isinstance(e, PermissionError) else ""
159
- raise ValueError(f"无法保存配置文件: {str(e)}{hint}")
160
-
161
- def _get_config_value(self, key: str, default: str | None = None) -> str | None:
162
- env_value = os.getenv(key)
163
- if env_value is not None:
164
- return env_value
165
-
166
- data = self._load_config_file()
167
- value = data.get(key)
168
- if value is None:
169
- legacy_key = next((old for old, new in self._LEGACY_CONFIG_KEYS.items() if new == key), None)
170
- if legacy_key:
171
- value = data.get(legacy_key)
172
- if value is None:
173
- return default
174
- return str(value)
175
-
176
- def get_saved_config(self, masked: bool = True) -> dict:
177
- data = self._load_config_file()
178
- normalized: dict[str, str] = {}
179
- for old_key, new_key in self._LEGACY_CONFIG_KEYS.items():
180
- if old_key in data and new_key not in data:
181
- normalized[new_key] = str(data[old_key])
182
- for key, value in data.items():
183
- if key in self._CONFIG_KEYS and value is not None:
184
- normalized[key] = str(value)
185
- if not masked:
186
- return normalized
187
- return {key: self._mask_if_secret(key, value) for key, value in normalized.items()}
188
-
189
- def get_config_source(self, key: str) -> str:
190
- if os.getenv(key) is not None:
191
- return "environment"
192
- data = self._load_config_file()
193
- if key in data:
194
- return "config_file"
195
- legacy_key = next((old for old, new in self._LEGACY_CONFIG_KEYS.items() if new == key), None)
196
- if legacy_key and legacy_key in data:
197
- return "config_file"
198
- return "default"
199
-
200
- def get_config_sources(self) -> dict[str, str]:
201
- return {key: self.get_config_source(key) for key in sorted(self._CONFIG_KEYS)}
202
-
203
- def set_config_value(self, key: str, value: str) -> None:
204
- key = key.strip().upper()
205
- if key not in self._CONFIG_KEYS:
206
- raise ValueError(f"Unsupported config key: {key}")
207
- config_data = self._load_config_file()
208
- config_data[key] = value
209
- self._save_config_file(config_data)
210
- if key in {
211
- "OPENAI_COMPATIBLE_API_URL",
212
- "OPENAI_COMPATIBLE_API_KEY",
213
- "OPENAI_COMPATIBLE_MODEL",
214
- "OPENAI_COMPATIBLE_STREAM",
215
- "SMART_SEARCH_VALIDATION_LEVEL",
216
- "SMART_SEARCH_FALLBACK_MODE",
217
- "SMART_SEARCH_MINIMUM_PROFILE",
218
- }:
219
- self._cached_model = None
220
-
221
- def unset_config_value(self, key: str) -> None:
222
- key = key.strip().upper()
223
- if key not in self._CONFIG_KEYS:
224
- raise ValueError(f"Unsupported config key: {key}")
225
- config_data = self._load_config_file()
226
- config_data.pop(key, None)
227
- for old_key, new_key in self._LEGACY_CONFIG_KEYS.items():
228
- if new_key == key:
229
- config_data.pop(old_key, None)
230
- self._save_config_file(config_data)
231
- if key in {
232
- "OPENAI_COMPATIBLE_API_URL",
233
- "OPENAI_COMPATIBLE_API_KEY",
234
- "OPENAI_COMPATIBLE_MODEL",
235
- "OPENAI_COMPATIBLE_STREAM",
236
- "SMART_SEARCH_VALIDATION_LEVEL",
237
- "SMART_SEARCH_FALLBACK_MODE",
238
- "SMART_SEARCH_MINIMUM_PROFILE",
239
- }:
240
- self._cached_model = None
241
-
242
- def config_path_info(self) -> dict:
243
- return {
244
- "ok": True,
245
- "config_file": str(self.config_file),
246
- "config_dir": str(self.config_file.parent),
247
- "config_dir_source": self.config_dir_source,
248
- "default_config_file": str(self._default_config_dir() / "config.json"),
249
- "legacy_windows_config_file": str(self._legacy_windows_config_dir() / "config.json") if sys.platform.startswith("win") else "",
250
- "legacy_windows_config_exists": (self._legacy_windows_config_dir() / "config.json").exists() if sys.platform.startswith("win") else False,
251
- "config_dir_override_value": self._config_dir_override_value(),
252
- "config_dir_override_matches_default": self._config_dir_override_matches_default(),
253
- "evidence_dir_config_value": self.evidence_dir_config_value,
254
- "resolved_evidence_dir": str(self.evidence_dir),
255
- "exists": self.config_file.exists(),
256
- }
257
-
258
- @property
259
- def debug_enabled(self) -> bool:
260
- return (self._get_config_value("SMART_SEARCH_DEBUG", "false") or "false").lower() in ("true", "1", "yes")
261
-
262
- @property
263
- def retry_max_attempts(self) -> int:
264
- return int(self._get_config_value("SMART_SEARCH_RETRY_MAX_ATTEMPTS", "3") or "3")
265
-
266
- @property
267
- def retry_multiplier(self) -> float:
268
- return float(self._get_config_value("SMART_SEARCH_RETRY_MULTIPLIER", "1") or "1")
269
-
270
- @property
271
- def retry_max_wait(self) -> int:
272
- return int(self._get_config_value("SMART_SEARCH_RETRY_MAX_WAIT", "10") or "10")
273
-
274
- @property
275
- def openai_compatible_api_url(self) -> str | None:
276
- return self._get_config_value("OPENAI_COMPATIBLE_API_URL")
277
-
278
- @property
279
- def openai_compatible_api_key(self) -> str | None:
280
- return self._get_config_value("OPENAI_COMPATIBLE_API_KEY")
281
-
282
- @property
283
- def openai_compatible_model(self) -> str:
284
- model = self._get_config_value("OPENAI_COMPATIBLE_MODEL") or self._base_model_value()
285
- return self.apply_model_suffix_for_url(model, self.openai_compatible_api_url or "")
286
-
287
- @property
288
- def openai_compatible_stream(self) -> bool:
289
- return (self._get_config_value("OPENAI_COMPATIBLE_STREAM", "true") or "true").lower() in ("true", "1", "yes")
290
-
291
- def _validated_enum(self, key: str, default: str, allowed: set[str]) -> str:
292
- value = (self._get_config_value(key, default) or default).strip().lower()
293
- if value not in allowed:
294
- allowed_text = ", ".join(sorted(allowed))
295
- raise ValueError(f"Invalid {key}: {value}. Supported values: {allowed_text}")
296
- return value
297
-
298
- def _enum_info(self, key: str, default: str, allowed: set[str]) -> tuple[str, str]:
299
- value = (self._get_config_value(key, default) or default).strip().lower()
300
- if value not in allowed:
301
- allowed_text = ", ".join(sorted(allowed))
302
- return value, f"Invalid {key}: {value}. Supported values: {allowed_text}"
303
- return value, ""
304
-
305
- @property
306
- def validation_level(self) -> str:
307
- return self._validated_enum(
308
- "SMART_SEARCH_VALIDATION_LEVEL",
309
- self._DEFAULT_VALIDATION_LEVEL,
310
- self._ALLOWED_VALIDATION_LEVELS,
311
- )
312
-
313
- @property
314
- def fallback_mode(self) -> str:
315
- return self._validated_enum(
316
- "SMART_SEARCH_FALLBACK_MODE",
317
- self._DEFAULT_FALLBACK_MODE,
318
- self._ALLOWED_FALLBACK_MODES,
319
- )
320
-
321
- @property
322
- def minimum_profile(self) -> str:
323
- return self._validated_enum(
324
- "SMART_SEARCH_MINIMUM_PROFILE",
325
- self._DEFAULT_MINIMUM_PROFILE,
326
- self._ALLOWED_MINIMUM_PROFILES,
327
- )
328
-
329
- def _csv_values(self, key: str) -> list[str]:
330
- raw = self._get_config_value(key, "") or ""
331
- values: list[str] = []
332
- seen: set[str] = set()
333
- for item in raw.split(","):
334
- value = item.strip().lower()
335
- if value and value not in seen:
336
- seen.add(value)
337
- values.append(value)
338
- return values
339
-
340
- @property
341
- def research_preferred_providers(self) -> list[str]:
342
- return self._csv_values("SMART_SEARCH_RESEARCH_PREFERRED_PROVIDERS")
343
-
344
- @property
345
- def research_disabled_providers(self) -> list[str]:
346
- return self._csv_values("SMART_SEARCH_RESEARCH_DISABLED_PROVIDERS")
347
-
348
- @property
349
- def tavily_enabled(self) -> bool:
350
- return (self._get_config_value("TAVILY_ENABLED", "true") or "true").lower() in ("true", "1", "yes")
351
-
352
- @property
353
- def tavily_api_url(self) -> str:
354
- return self._get_config_value("TAVILY_API_URL", "https://api.tavily.com") or "https://api.tavily.com"
355
-
356
- @property
357
- def tavily_api_key(self) -> str | None:
358
- return self._get_config_value("TAVILY_API_KEY")
359
-
360
- @property
361
- def tavily_timeout(self) -> float:
362
- return float(self._get_config_value("TAVILY_TIMEOUT_SECONDS", "60") or "60")
363
-
364
- @property
365
- def firecrawl_api_url(self) -> str:
366
- return self._get_config_value("FIRECRAWL_API_URL", "https://api.firecrawl.dev/v2") or "https://api.firecrawl.dev/v2"
367
-
368
- @property
369
- def firecrawl_api_key(self) -> str | None:
370
- return self._get_config_value("FIRECRAWL_API_KEY")
371
-
372
- @property
373
- def log_level(self) -> str:
374
- return (self._get_config_value("SMART_SEARCH_LOG_LEVEL", "INFO") or "INFO").upper()
375
-
376
- @property
377
- def log_dir(self) -> Path:
378
- log_dir_str = self.log_dir_config_value
379
- log_dir = Path(log_dir_str)
380
- if log_dir.is_absolute():
381
- return log_dir
382
-
383
- return self.config_file.parent / log_dir
384
-
385
- @property
386
- def log_dir_config_value(self) -> str:
387
- return self._get_config_value("SMART_SEARCH_LOG_DIR", "logs") or "logs"
388
-
389
- @property
390
- def evidence_dir(self) -> Path:
391
- evidence_dir_str = self.evidence_dir_config_value
392
- evidence_dir = Path(evidence_dir_str)
393
- if evidence_dir.is_absolute():
394
- return evidence_dir
395
-
396
- return self.config_file.parent / evidence_dir
397
-
398
- @property
399
- def evidence_dir_config_value(self) -> str:
400
- return self._get_config_value("SMART_SEARCH_EVIDENCE_DIR", "evidence") or "evidence"
401
-
402
- @staticmethod
403
- def apply_model_suffix_for_url(model: str, api_url: str) -> str:
404
- if "openrouter" in api_url and ":online" not in model:
405
- return f"{model}:online"
406
- return model
407
-
408
- def _base_model_value(self) -> str:
409
- return self._DEFAULT_MODEL
410
-
411
- @staticmethod
412
- def _mask_api_key(key: str) -> str:
413
- if not key or len(key) <= 8:
414
- return "***"
415
- return f"{key[:4]}{'*' * (len(key) - 8)}{key[-4:]}"
416
-
417
- @classmethod
418
- def _mask_if_secret(cls, key: str, value: str) -> str:
419
- if "KEY" in key or "TOKEN" in key or "SECRET" in key:
420
- return cls._mask_api_key(value)
421
- return value
422
-
423
- @property
424
- def output_cleanup_enabled(self) -> bool:
425
- return (self._get_config_value("SMART_SEARCH_OUTPUT_CLEANUP", "true") or "true").lower() in ("true", "1", "yes")
426
-
427
- @property
428
- def log_to_file_enabled(self) -> bool:
429
- return (self._get_config_value("SMART_SEARCH_LOG_TO_FILE", "false") or "false").lower() in ("true", "1", "yes")
430
-
431
- @property
432
- def ssl_verify_enabled(self) -> bool:
433
- return (self._get_config_value("SSL_VERIFY", "true") or "true").lower() not in ("false", "0", "no")
434
-
435
- @property
436
- def exa_api_key(self) -> str | None:
437
- return self._get_config_value("EXA_API_KEY")
438
-
439
- @property
440
- def exa_base_url(self) -> str:
441
- return self._get_config_value("EXA_BASE_URL", "https://api.exa.ai") or "https://api.exa.ai"
442
-
443
- @property
444
- def exa_timeout(self) -> float:
445
- return float(self._get_config_value("EXA_TIMEOUT_SECONDS", "60") or "60")
446
-
447
- @property
448
- def context7_api_key(self) -> str | None:
449
- return self._get_config_value("CONTEXT7_API_KEY")
450
-
451
- @property
452
- def context7_base_url(self) -> str:
453
- return self._get_config_value("CONTEXT7_BASE_URL", "https://context7.com") or "https://context7.com"
454
-
455
- @property
456
- def context7_timeout(self) -> float:
457
- return float(self._get_config_value("CONTEXT7_TIMEOUT_SECONDS", "60") or "60")
458
-
459
- @property
460
- def zhipu_api_key(self) -> str | None:
461
- return self._get_config_value("ZHIPU_API_KEY")
462
-
463
- @property
464
- def zhipu_api_url(self) -> str:
465
- return self._get_config_value("ZHIPU_API_URL", "https://open.bigmodel.cn/api") or "https://open.bigmodel.cn/api"
466
-
467
- @property
468
- def zhipu_search_engine(self) -> str:
469
- return self._get_config_value("ZHIPU_SEARCH_ENGINE", "search_std") or "search_std"
470
-
471
- @property
472
- def zhipu_timeout(self) -> float:
473
- return float(self._get_config_value("ZHIPU_TIMEOUT_SECONDS", "60") or "60")
474
-
475
- @property
476
- def jina_api_key(self) -> str | None:
477
- return self._get_config_value("JINA_API_KEY")
478
-
479
- @property
480
- def jina_reader_api_url(self) -> str:
481
- return self._get_config_value("JINA_READER_API_URL", "https://r.jina.ai") or "https://r.jina.ai"
482
-
483
- @property
484
- def jina_respond_with(self) -> str:
485
- return self._get_config_value("JINA_RESPOND_WITH", "") or ""
486
-
487
- @property
488
- def jina_timeout(self) -> float:
489
- return float(self._get_config_value("JINA_TIMEOUT_SECONDS", "60") or "60")
490
-
491
- def get_config_info(self) -> dict:
492
- config_parameter_errors: list[str] = []
493
- explicit_main_configured = bool(
494
- self.openai_compatible_api_url and self.openai_compatible_api_key
495
- )
496
- if explicit_main_configured:
497
- config_status = "ok: 配置完整"
498
- else:
499
- config_status = f"config_error: {self._SETUP_COMMAND}"
500
-
501
- validation_level, validation_error = self._enum_info(
502
- "SMART_SEARCH_VALIDATION_LEVEL",
503
- self._DEFAULT_VALIDATION_LEVEL,
504
- self._ALLOWED_VALIDATION_LEVELS,
505
- )
506
- fallback_mode, fallback_error = self._enum_info(
507
- "SMART_SEARCH_FALLBACK_MODE",
508
- self._DEFAULT_FALLBACK_MODE,
509
- self._ALLOWED_FALLBACK_MODES,
510
- )
511
- minimum_profile, minimum_error = self._enum_info(
512
- "SMART_SEARCH_MINIMUM_PROFILE",
513
- self._DEFAULT_MINIMUM_PROFILE,
514
- self._ALLOWED_MINIMUM_PROFILES,
515
- )
516
- config_parameter_errors.extend(error for error in (validation_error, fallback_error, minimum_error) if error)
517
- if config_parameter_errors and config_status.startswith("ok:"):
518
- config_status = f"config_error: {'; '.join(config_parameter_errors)}"
519
-
520
- return {
521
- "OPENAI_COMPATIBLE_API_URL": self.openai_compatible_api_url or "未配置",
522
- "OPENAI_COMPATIBLE_API_KEY": self._mask_api_key(self.openai_compatible_api_key) if self.openai_compatible_api_key else "未配置",
523
- "OPENAI_COMPATIBLE_MODEL": self.openai_compatible_model,
524
- "OPENAI_COMPATIBLE_STREAM": self.openai_compatible_stream,
525
- "SMART_SEARCH_VALIDATION_LEVEL": validation_level,
526
- "SMART_SEARCH_FALLBACK_MODE": fallback_mode,
527
- "SMART_SEARCH_MINIMUM_PROFILE": minimum_profile,
528
- "SMART_SEARCH_RESEARCH_PREFERRED_PROVIDERS": ",".join(self.research_preferred_providers),
529
- "SMART_SEARCH_RESEARCH_DISABLED_PROVIDERS": ",".join(self.research_disabled_providers),
530
- "SMART_SEARCH_DEBUG": self.debug_enabled,
531
- "SMART_SEARCH_LOG_LEVEL": self.log_level,
532
- "SMART_SEARCH_LOG_DIR": self.log_dir_config_value,
533
- "SMART_SEARCH_EVIDENCE_DIR": self.evidence_dir_config_value,
534
- "SMART_SEARCH_RETRY_MAX_ATTEMPTS": self.retry_max_attempts,
535
- "SMART_SEARCH_RETRY_MULTIPLIER": self.retry_multiplier,
536
- "SMART_SEARCH_RETRY_MAX_WAIT": self.retry_max_wait,
537
- "TAVILY_API_URL": self.tavily_api_url,
538
- "TAVILY_ENABLED": self.tavily_enabled,
539
- "TAVILY_API_KEY": self._mask_api_key(self.tavily_api_key) if self.tavily_api_key else "未配置",
540
- "TAVILY_TIMEOUT_SECONDS": self.tavily_timeout,
541
- "FIRECRAWL_API_URL": self.firecrawl_api_url,
542
- "FIRECRAWL_API_KEY": self._mask_api_key(self.firecrawl_api_key) if self.firecrawl_api_key else "未配置",
543
- "SMART_SEARCH_OUTPUT_CLEANUP": self.output_cleanup_enabled,
544
- "SMART_SEARCH_LOG_TO_FILE": self.log_to_file_enabled,
545
- "SSL_VERIFY": self.ssl_verify_enabled,
546
- "EXA_API_KEY": self._mask_api_key(self.exa_api_key) if self.exa_api_key else "未配置",
547
- "EXA_BASE_URL": self.exa_base_url,
548
- "EXA_TIMEOUT_SECONDS": self.exa_timeout,
549
- "CONTEXT7_API_KEY": self._mask_api_key(self.context7_api_key) if self.context7_api_key else "未配置",
550
- "CONTEXT7_BASE_URL": self.context7_base_url,
551
- "CONTEXT7_TIMEOUT_SECONDS": self.context7_timeout,
552
- "ZHIPU_API_KEY": self._mask_api_key(self.zhipu_api_key) if self.zhipu_api_key else "未配置",
553
- "ZHIPU_API_URL": self.zhipu_api_url,
554
- "ZHIPU_SEARCH_ENGINE": self.zhipu_search_engine,
555
- "ZHIPU_TIMEOUT_SECONDS": self.zhipu_timeout,
556
- "JINA_API_KEY": self._mask_api_key(self.jina_api_key) if self.jina_api_key else "未配置",
557
- "JINA_READER_API_URL": self.jina_reader_api_url,
558
- "JINA_RESPOND_WITH": self.jina_respond_with,
559
- "JINA_TIMEOUT_SECONDS": self.jina_timeout,
560
- "primary_api_mode": "chat-completions" if (self.openai_compatible_api_url and self.openai_compatible_api_key) else "未配置",
561
- "primary_api_mode_source": "config_file" if explicit_main_configured else "default",
562
- "config_file": str(self.config_file),
563
- "config_dir": str(self.config_file.parent),
564
- "config_dir_source": self.config_dir_source,
565
- "default_config_file": str(self._default_config_dir() / "config.json"),
566
- "legacy_windows_config_file": str(self._legacy_windows_config_dir() / "config.json") if sys.platform.startswith("win") else "",
567
- "legacy_windows_config_exists": (self._legacy_windows_config_dir() / "config.json").exists() if sys.platform.startswith("win") else False,
568
- "config_dir_override_value": self._config_dir_override_value(),
569
- "config_dir_override_matches_default": self._config_dir_override_matches_default(),
570
- "log_dir_config_value": self.log_dir_config_value,
571
- "resolved_log_dir": str(self.log_dir),
572
- "evidence_dir_config_value": self.evidence_dir_config_value,
573
- "resolved_evidence_dir": str(self.evidence_dir),
574
- "file_logging_enabled": self.debug_enabled or self.log_to_file_enabled,
575
- "config_sources": self.get_config_sources(),
576
- "config_parameter_errors": config_parameter_errors,
577
- "config_status": config_status
578
- }
579
-
580
- config = Config()
@@ -1,43 +0,0 @@
1
- import logging
2
- import os
3
- from datetime import datetime
4
- from .config import config
5
-
6
- logger = logging.getLogger("smart_search")
7
- logger.setLevel(getattr(logging, config.log_level))
8
- logger.addHandler(logging.NullHandler())
9
-
10
-
11
- def _file_logging_enabled() -> bool:
12
- return config.debug_enabled or config.log_to_file_enabled
13
-
14
-
15
- def _configure_file_logging() -> None:
16
- if not _file_logging_enabled():
17
- return
18
- if any(isinstance(handler, logging.FileHandler) for handler in logger.handlers):
19
- return
20
-
21
- log_dir = config.log_dir
22
- log_dir.mkdir(parents=True, exist_ok=True)
23
- log_file = log_dir / f"smart_search_{datetime.now().strftime('%Y%m%d')}.log"
24
-
25
- file_handler = logging.FileHandler(log_file, encoding="utf-8")
26
- file_handler.setLevel(getattr(logging, config.log_level))
27
-
28
- formatter = logging.Formatter(
29
- "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
30
- datefmt="%Y-%m-%d %H:%M:%S",
31
- )
32
- file_handler.setFormatter(formatter)
33
- logger.addHandler(file_handler)
34
-
35
-
36
- _configure_file_logging()
37
-
38
- async def log_info(ctx, message: str, is_debug: bool = False):
39
- if is_debug:
40
- logger.info(message)
41
-
42
- if ctx:
43
- await ctx.info(message)
@@ -1,16 +0,0 @@
1
- from .base import BaseSearchProvider, SearchResult
2
- from .context7 import Context7Provider
3
- from .openai_compatible import OpenAICompatibleSearchProvider
4
- from .exa import ExaSearchProvider
5
- from .jina import JinaReaderProvider
6
- from .zhipu import ZhipuWebSearchProvider
7
-
8
- __all__ = [
9
- "BaseSearchProvider",
10
- "SearchResult",
11
- "Context7Provider",
12
- "OpenAICompatibleSearchProvider",
13
- "ExaSearchProvider",
14
- "JinaReaderProvider",
15
- "ZhipuWebSearchProvider",
16
- ]
@@ -1,41 +0,0 @@
1
- from abc import ABC, abstractmethod
2
- from typing import Dict, List
3
-
4
-
5
- class SearchResult:
6
- def __init__(
7
- self,
8
- title: str,
9
- url: str,
10
- snippet: str,
11
- source: str = "",
12
- published_date: str = "",
13
- ):
14
- self.title = title
15
- self.url = url
16
- self.snippet = snippet
17
- self.source = source
18
- self.published_date = published_date
19
-
20
- def to_dict(self) -> Dict[str, str]:
21
- return {
22
- "title": self.title,
23
- "url": self.url,
24
- "snippet": self.snippet,
25
- "source": self.source,
26
- "published_date": self.published_date,
27
- }
28
-
29
-
30
- class BaseSearchProvider(ABC):
31
- def __init__(self, api_url: str, api_key: str):
32
- self.api_url = api_url
33
- self.api_key = api_key
34
-
35
- @abstractmethod
36
- async def search(self, query: str, max_results: int = 5) -> List[SearchResult]:
37
- pass
38
-
39
- @abstractmethod
40
- def get_provider_name(self) -> str:
41
- pass