@shirlytaylor73/smart-search 0.2.0-beta.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.
- package/LICENSE +21 -0
- package/README.md +188 -0
- package/README.zh-CN.md +180 -0
- package/npm/bin/smart-search.js +68 -0
- package/npm/scripts/postinstall.js +87 -0
- package/npm/scripts/resolve-prerelease-version.js +108 -0
- package/npm/scripts/set-package-version.js +35 -0
- package/npm/scripts/sync-python-version.js +22 -0
- package/npm/scripts/test-wrapper-repair.js +137 -0
- package/npm/scripts/test.js +76 -0
- package/package.json +42 -0
- package/pyproject.toml +36 -0
- package/skills/smart-search-cli/SKILL.md +41 -0
- package/skills/smart-search-cli/agents/openai.yaml +3 -0
- package/skills/smart-search-cli/references/cli-contract.md +7 -0
- package/skills/smart-search-cli/references/cli-core.md +5 -0
- package/skills/smart-search-cli/references/command-patterns.md +12 -0
- package/skills/smart-search-cli/references/provider-routing.md +3 -0
- package/skills/smart-search-cli/references/regression-release.md +28 -0
- package/skills/smart-search-cli/references/setup-config.md +3 -0
- package/src/smart_search/__init__.py +1 -0
- package/src/smart_search/assets/skills/smart-search-cli/SKILL.md +41 -0
- package/src/smart_search/assets/skills/smart-search-cli/agents/openai.yaml +3 -0
- package/src/smart_search/assets/skills/smart-search-cli/references/cli-contract.md +7 -0
- package/src/smart_search/assets/skills/smart-search-cli/references/cli-core.md +5 -0
- package/src/smart_search/assets/skills/smart-search-cli/references/command-patterns.md +12 -0
- package/src/smart_search/assets/skills/smart-search-cli/references/provider-routing.md +3 -0
- package/src/smart_search/assets/skills/smart-search-cli/references/regression-release.md +28 -0
- package/src/smart_search/assets/skills/smart-search-cli/references/setup-config.md +3 -0
- package/src/smart_search/cli.py +3118 -0
- package/src/smart_search/config.py +809 -0
- package/src/smart_search/embedding_presets.py +40 -0
- package/src/smart_search/intent_router.py +757 -0
- package/src/smart_search/logger.py +43 -0
- package/src/smart_search/providers/__init__.py +20 -0
- package/src/smart_search/providers/base.py +41 -0
- package/src/smart_search/providers/context7.py +141 -0
- package/src/smart_search/providers/exa.py +206 -0
- package/src/smart_search/providers/jina.py +136 -0
- package/src/smart_search/providers/openai_compatible.py +541 -0
- package/src/smart_search/providers/xai_responses.py +117 -0
- package/src/smart_search/providers/zhipu.py +143 -0
- package/src/smart_search/providers/zhipu_mcp.py +230 -0
- package/src/smart_search/service.py +3445 -0
- package/src/smart_search/skill_installer.py +342 -0
- package/src/smart_search/sources.py +429 -0
- package/src/smart_search/utils.py +220 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from hashlib import sha256
|
|
6
|
+
from importlib import resources
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
SKILL_NAME = "smart-search-cli"
|
|
12
|
+
PACKAGE_ROOT_ENV = "SMART_SEARCH_PACKAGE_ROOT"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class SkillTarget:
|
|
17
|
+
target_id: str
|
|
18
|
+
label: str
|
|
19
|
+
relative_root: str
|
|
20
|
+
default: bool = False
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def skill_relative_path(self) -> str:
|
|
24
|
+
return f"{self.relative_root}/{SKILL_NAME}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
SKILL_TARGETS: tuple[SkillTarget, ...] = (
|
|
28
|
+
SkillTarget("codex", "Codex", ".codex/skills", True),
|
|
29
|
+
SkillTarget("claude", "Claude Code", ".claude/skills", True),
|
|
30
|
+
SkillTarget("cursor", "Cursor", ".cursor/skills", True),
|
|
31
|
+
SkillTarget("opencode", "OpenCode", ".opencode/skills"),
|
|
32
|
+
SkillTarget("copilot", "GitHub Copilot", ".copilot/skills"),
|
|
33
|
+
SkillTarget("gemini", "Gemini CLI", ".gemini/skills"),
|
|
34
|
+
SkillTarget("kiro", "Kiro", ".kiro/skills"),
|
|
35
|
+
SkillTarget("qoder", "Qoder", ".qoder/skills"),
|
|
36
|
+
SkillTarget("codebuddy", "CodeBuddy", ".codebuddy/skills"),
|
|
37
|
+
SkillTarget("droid", "Factory Droid", ".factory/skills"),
|
|
38
|
+
SkillTarget("pi", "Pi Agent", ".pi/agent/skills"),
|
|
39
|
+
SkillTarget("kilo", "Kilo CLI", ".kilocode/skills"),
|
|
40
|
+
SkillTarget("antigravity", "Antigravity", ".agent/skills"),
|
|
41
|
+
SkillTarget("windsurf", "Windsurf", ".windsurf/skills"),
|
|
42
|
+
SkillTarget("hermes", "Hermes Agent", ".hermes/skills"),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
SKILL_TARGET_BY_ID = {target.target_id: target for target in SKILL_TARGETS}
|
|
46
|
+
DEFAULT_SKILL_TARGET_IDS = [target.target_id for target in SKILL_TARGETS if target.default]
|
|
47
|
+
|
|
48
|
+
_TARGET_ALIASES = {
|
|
49
|
+
"agents": "codex",
|
|
50
|
+
"agentskills": "codex",
|
|
51
|
+
"agent-skills": "codex",
|
|
52
|
+
"claude-code": "claude",
|
|
53
|
+
"github-copilot": "copilot",
|
|
54
|
+
"gh-copilot": "copilot",
|
|
55
|
+
"factory": "droid",
|
|
56
|
+
"factory-droid": "droid",
|
|
57
|
+
"pi-agent": "pi",
|
|
58
|
+
"kilo-cli": "kilo",
|
|
59
|
+
"hermes-agent": "hermes",
|
|
60
|
+
"nous-hermes": "hermes",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SkillInstallError(ValueError):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_skill_targets(raw: str) -> list[str]:
|
|
69
|
+
if not raw.strip():
|
|
70
|
+
return []
|
|
71
|
+
tokens = [part.strip().lower() for part in raw.replace(";", ",").replace("+", ",").split(",")]
|
|
72
|
+
if len(tokens) == 1 and " " in tokens[0]:
|
|
73
|
+
tokens = [part.strip().lower() for part in tokens[0].split()]
|
|
74
|
+
|
|
75
|
+
selected: list[str] = []
|
|
76
|
+
invalid: list[str] = []
|
|
77
|
+
for token in tokens:
|
|
78
|
+
if not token:
|
|
79
|
+
continue
|
|
80
|
+
if token in {"skip", "none", "no", "n", "跳过", "无", "否"}:
|
|
81
|
+
return []
|
|
82
|
+
if token in {"all", "全部"}:
|
|
83
|
+
return [target.target_id for target in SKILL_TARGETS]
|
|
84
|
+
target_id = _TARGET_ALIASES.get(token, token)
|
|
85
|
+
if target_id not in SKILL_TARGET_BY_ID:
|
|
86
|
+
invalid.append(token)
|
|
87
|
+
continue
|
|
88
|
+
if target_id not in selected:
|
|
89
|
+
selected.append(target_id)
|
|
90
|
+
|
|
91
|
+
if invalid:
|
|
92
|
+
valid = ", ".join(target.target_id for target in SKILL_TARGETS)
|
|
93
|
+
raise SkillInstallError(f"Unknown skill target(s): {', '.join(invalid)}. Valid targets: {valid}")
|
|
94
|
+
return selected
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _resource_skill_root() -> Any:
|
|
98
|
+
try:
|
|
99
|
+
root = resources.files("smart_search").joinpath("assets", "skills", SKILL_NAME)
|
|
100
|
+
if root.is_dir():
|
|
101
|
+
return root
|
|
102
|
+
except (FileNotFoundError, ModuleNotFoundError, AttributeError):
|
|
103
|
+
pass
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _filesystem_skill_root() -> Path | None:
|
|
108
|
+
candidates: list[Path] = []
|
|
109
|
+
package_root = os.getenv(PACKAGE_ROOT_ENV, "").strip()
|
|
110
|
+
if package_root:
|
|
111
|
+
base = Path(package_root)
|
|
112
|
+
candidates.extend([
|
|
113
|
+
base / "src" / "smart_search" / "assets" / "skills" / SKILL_NAME,
|
|
114
|
+
base / "skills" / SKILL_NAME,
|
|
115
|
+
])
|
|
116
|
+
|
|
117
|
+
repo_root = Path(__file__).resolve().parents[2]
|
|
118
|
+
candidates.extend([
|
|
119
|
+
repo_root / "src" / "smart_search" / "assets" / "skills" / SKILL_NAME,
|
|
120
|
+
repo_root / "skills" / SKILL_NAME,
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
for candidate in candidates:
|
|
124
|
+
if candidate.is_dir():
|
|
125
|
+
return candidate
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _iter_resource_files(root: Any) -> list[tuple[str, bytes]]:
|
|
130
|
+
files: list[tuple[str, bytes]] = []
|
|
131
|
+
|
|
132
|
+
def visit(node: Any, prefix: str = "") -> None:
|
|
133
|
+
for child in node.iterdir():
|
|
134
|
+
rel = f"{prefix}/{child.name}" if prefix else child.name
|
|
135
|
+
if child.is_dir():
|
|
136
|
+
visit(child, rel)
|
|
137
|
+
elif child.is_file():
|
|
138
|
+
files.append((rel, child.read_bytes()))
|
|
139
|
+
|
|
140
|
+
visit(root)
|
|
141
|
+
return files
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _iter_filesystem_files(root: Path) -> list[tuple[str, bytes]]:
|
|
145
|
+
return [
|
|
146
|
+
(str(path.relative_to(root)).replace("\\", "/"), path.read_bytes())
|
|
147
|
+
for path in root.rglob("*")
|
|
148
|
+
if path.is_file()
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _load_skill_files(source_root: Path | None = None) -> list[tuple[str, bytes]]:
|
|
153
|
+
if source_root is not None:
|
|
154
|
+
if not source_root.is_dir():
|
|
155
|
+
raise SkillInstallError(f"Skill source directory not found: {source_root}")
|
|
156
|
+
return _iter_filesystem_files(source_root)
|
|
157
|
+
|
|
158
|
+
resource_root = _resource_skill_root()
|
|
159
|
+
if resource_root is not None:
|
|
160
|
+
files = _iter_resource_files(resource_root)
|
|
161
|
+
if files:
|
|
162
|
+
return files
|
|
163
|
+
|
|
164
|
+
filesystem_root = _filesystem_skill_root()
|
|
165
|
+
if filesystem_root is not None:
|
|
166
|
+
files = _iter_filesystem_files(filesystem_root)
|
|
167
|
+
if files:
|
|
168
|
+
return files
|
|
169
|
+
|
|
170
|
+
raise SkillInstallError("Bundled smart-search-cli skill files were not found.")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _skill_digest(files: list[tuple[str, bytes]]) -> str:
|
|
174
|
+
digest = sha256()
|
|
175
|
+
for rel_path, content in sorted(files, key=lambda item: item[0]):
|
|
176
|
+
digest.update(rel_path.encode("utf-8"))
|
|
177
|
+
digest.update(b"\0")
|
|
178
|
+
digest.update(content)
|
|
179
|
+
digest.update(b"\0")
|
|
180
|
+
return digest.hexdigest()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _target_installed_files(path: Path) -> list[tuple[str, bytes]]:
|
|
184
|
+
if not path.is_dir():
|
|
185
|
+
return []
|
|
186
|
+
return _iter_filesystem_files(path)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def status_skill_targets(
|
|
190
|
+
target_ids: list[str],
|
|
191
|
+
*,
|
|
192
|
+
project_root: str | Path | None = None,
|
|
193
|
+
source_root: str | Path | None = None,
|
|
194
|
+
) -> dict[str, Any]:
|
|
195
|
+
root = Path(project_root).expanduser().resolve() if project_root else Path.home().expanduser().resolve()
|
|
196
|
+
selected = [SKILL_TARGET_BY_ID[target_id] for target_id in target_ids]
|
|
197
|
+
source = Path(source_root).expanduser().resolve() if source_root is not None else None
|
|
198
|
+
source_files = _load_skill_files(source)
|
|
199
|
+
source_by_path = {rel_path: content for rel_path, content in source_files}
|
|
200
|
+
bundled_digest = _skill_digest(source_files)
|
|
201
|
+
targets: list[dict[str, Any]] = []
|
|
202
|
+
|
|
203
|
+
for target in selected:
|
|
204
|
+
dest = root / Path(target.skill_relative_path)
|
|
205
|
+
item: dict[str, Any] = {
|
|
206
|
+
"target": target.target_id,
|
|
207
|
+
"label": target.label,
|
|
208
|
+
"path": str(dest),
|
|
209
|
+
"status": "missing",
|
|
210
|
+
"files": len(source_files),
|
|
211
|
+
"installed_files": 0,
|
|
212
|
+
"bundled_hash": bundled_digest,
|
|
213
|
+
"installed_hash": "",
|
|
214
|
+
"hash_match": False,
|
|
215
|
+
"managed_hash_match": False,
|
|
216
|
+
"extra_files": [],
|
|
217
|
+
"missing_files": sorted(source_by_path),
|
|
218
|
+
"stale_files": [],
|
|
219
|
+
}
|
|
220
|
+
try:
|
|
221
|
+
installed_files = _target_installed_files(dest)
|
|
222
|
+
installed_by_path = {rel_path: content for rel_path, content in installed_files}
|
|
223
|
+
item["installed_files"] = len(installed_files)
|
|
224
|
+
if not dest.exists():
|
|
225
|
+
targets.append(item)
|
|
226
|
+
continue
|
|
227
|
+
if not dest.is_dir():
|
|
228
|
+
item["status"] = "error"
|
|
229
|
+
item["error"] = "Installed skill path exists but is not a directory."
|
|
230
|
+
targets.append(item)
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
installed_digest = _skill_digest(installed_files)
|
|
234
|
+
extra_files = sorted(rel_path for rel_path in installed_by_path if rel_path not in source_by_path)
|
|
235
|
+
missing_files = sorted(rel_path for rel_path in source_by_path if rel_path not in installed_by_path)
|
|
236
|
+
stale_files = sorted(
|
|
237
|
+
rel_path
|
|
238
|
+
for rel_path, content in source_by_path.items()
|
|
239
|
+
if rel_path in installed_by_path and installed_by_path[rel_path] != content
|
|
240
|
+
)
|
|
241
|
+
managed_hash_match = not missing_files and not stale_files
|
|
242
|
+
hash_match = managed_hash_match and not extra_files
|
|
243
|
+
item.update(
|
|
244
|
+
{
|
|
245
|
+
"installed_hash": installed_digest if installed_files else "",
|
|
246
|
+
"hash_match": hash_match,
|
|
247
|
+
"managed_hash_match": managed_hash_match,
|
|
248
|
+
"extra_files": extra_files,
|
|
249
|
+
"missing_files": missing_files,
|
|
250
|
+
"stale_files": stale_files,
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
if missing_files or stale_files:
|
|
254
|
+
item["status"] = "stale"
|
|
255
|
+
elif extra_files:
|
|
256
|
+
item["status"] = "extra_files"
|
|
257
|
+
else:
|
|
258
|
+
item["status"] = "up_to_date"
|
|
259
|
+
except OSError as e:
|
|
260
|
+
item["status"] = "error"
|
|
261
|
+
item["error"] = str(e)
|
|
262
|
+
targets.append(item)
|
|
263
|
+
|
|
264
|
+
status_counts: dict[str, int] = {}
|
|
265
|
+
for item in targets:
|
|
266
|
+
status = str(item.get("status", "error"))
|
|
267
|
+
status_counts[status] = status_counts.get(status, 0) + 1
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
"ok": not any(item.get("status") == "error" for item in targets),
|
|
271
|
+
"root": str(root),
|
|
272
|
+
"selected": [target.target_id for target in selected],
|
|
273
|
+
"skill": SKILL_NAME,
|
|
274
|
+
"bundled_files": len(source_files),
|
|
275
|
+
"bundled_hash": bundled_digest,
|
|
276
|
+
"targets": targets,
|
|
277
|
+
"status_counts": status_counts,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def install_skill_targets(
|
|
282
|
+
target_ids: list[str],
|
|
283
|
+
*,
|
|
284
|
+
project_root: str | Path | None = None,
|
|
285
|
+
source_root: str | Path | None = None,
|
|
286
|
+
) -> dict[str, Any]:
|
|
287
|
+
root = Path(project_root).expanduser().resolve() if project_root else Path.home().expanduser().resolve()
|
|
288
|
+
selected = [SKILL_TARGET_BY_ID[target_id] for target_id in target_ids]
|
|
289
|
+
if not selected:
|
|
290
|
+
return {
|
|
291
|
+
"ok": True,
|
|
292
|
+
"root": str(root),
|
|
293
|
+
"installed": [],
|
|
294
|
+
"skipped": [],
|
|
295
|
+
"failed": [],
|
|
296
|
+
"selected": [],
|
|
297
|
+
"installed_count": 0,
|
|
298
|
+
"skipped_count": 0,
|
|
299
|
+
"failed_count": 0,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
source = Path(source_root).expanduser().resolve() if source_root is not None else None
|
|
303
|
+
files = _load_skill_files(source)
|
|
304
|
+
installed: list[dict[str, Any]] = []
|
|
305
|
+
failed: list[dict[str, str]] = []
|
|
306
|
+
|
|
307
|
+
for target in selected:
|
|
308
|
+
dest = root / Path(target.skill_relative_path)
|
|
309
|
+
try:
|
|
310
|
+
for rel_path, content in files:
|
|
311
|
+
file_path = dest / Path(rel_path)
|
|
312
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
313
|
+
file_path.write_bytes(content)
|
|
314
|
+
installed.append(
|
|
315
|
+
{
|
|
316
|
+
"target": target.target_id,
|
|
317
|
+
"label": target.label,
|
|
318
|
+
"path": str(dest),
|
|
319
|
+
"files": len(files),
|
|
320
|
+
}
|
|
321
|
+
)
|
|
322
|
+
except OSError as e:
|
|
323
|
+
failed.append(
|
|
324
|
+
{
|
|
325
|
+
"target": target.target_id,
|
|
326
|
+
"label": target.label,
|
|
327
|
+
"path": str(dest),
|
|
328
|
+
"error": str(e),
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
"ok": not failed,
|
|
334
|
+
"root": str(root),
|
|
335
|
+
"installed": installed,
|
|
336
|
+
"skipped": [],
|
|
337
|
+
"failed": failed,
|
|
338
|
+
"selected": [target.target_id for target in selected],
|
|
339
|
+
"installed_count": len(installed),
|
|
340
|
+
"skipped_count": 0,
|
|
341
|
+
"failed_count": len(failed),
|
|
342
|
+
}
|