dbtips-curate 0.2.0__py3-none-any.whl
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.
- dbtips_curate/__init__.py +1 -0
- dbtips_curate/cli/__init__.py +0 -0
- dbtips_curate/cli/_common.py +67 -0
- dbtips_curate/cli/_output.py +51 -0
- dbtips_curate/cli/_widget.py +207 -0
- dbtips_curate/cli/dev.py +358 -0
- dbtips_curate/cli/entity.py +723 -0
- dbtips_curate/cli/history.py +213 -0
- dbtips_curate/cli/main.py +70 -0
- dbtips_curate/cli/op.py +399 -0
- dbtips_curate/core/__init__.py +0 -0
- dbtips_curate/core/config.py +181 -0
- dbtips_curate/core/fs.py +154 -0
- dbtips_curate/core/git.py +80 -0
- dbtips_curate/core/git_history.py +183 -0
- dbtips_curate/core/merge.py +314 -0
- dbtips_curate/core/ops.py +78 -0
- dbtips_curate/core/schema.py +161 -0
- dbtips_curate/core/worklist.py +166 -0
- dbtips_curate/mcp_server.py +114 -0
- dbtips_curate/skill/_deferred/targetability-design-note.md +170 -0
- dbtips_curate/skill/dbtips-curate/SKILL.md +400 -0
- dbtips_curate/skill/dbtips-curate-antibodies/SKILL.md +219 -0
- dbtips_curate/skill/dbtips-curate-assayability/SKILL.md +175 -0
- dbtips_curate/skill/dbtips-curate-gene-essentiality/SKILL.md +107 -0
- dbtips_curate/skill/dbtips-curate-ontology/SKILL.md +129 -0
- dbtips_curate/skill/dbtips-curate-orthologs/SKILL.md +123 -0
- dbtips_curate/skill/dbtips-curate-supplementary-materials/SKILL.md +147 -0
- dbtips_curate/skill/dbtips-curate-target-literature/SKILL.md +286 -0
- dbtips_curate/skill/dbtips-curate-target-pathway/SKILL.md +166 -0
- dbtips_curate/skill/dbtips-curate-target-pipeline/SKILL.md +291 -0
- dbtips_curate-0.2.0.dist-info/METADATA +278 -0
- dbtips_curate-0.2.0.dist-info/RECORD +35 -0
- dbtips_curate-0.2.0.dist-info/WHEEL +4 -0
- dbtips_curate-0.2.0.dist-info/entry_points.txt +3 -0
dbtips_curate/cli/dev.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""`dbtips-curate dev ...` group — install + maintenance commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from dbtips_curate.cli._output import emit_error, emit_ok
|
|
11
|
+
from dbtips_curate.core.config import write_default_config
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="Developer + setup commands.", no_args_is_help=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Harness registry — where each supported agent CLI reads skills from.
|
|
18
|
+
#
|
|
19
|
+
# Both agentskills.io reference (agentskills.io) and each vendor's own
|
|
20
|
+
# docs were checked; every entry here reads plain SKILL.md with YAML
|
|
21
|
+
# frontmatter (the same file we ship). Zero conversion — just different
|
|
22
|
+
# install paths.
|
|
23
|
+
#
|
|
24
|
+
# - Claude Code: docs.claude.com/en/docs/claude-code/skills
|
|
25
|
+
# - OpenAI Codex: developers.openai.com/codex/skills
|
|
26
|
+
# - Google Antigravity: antigravity.google/docs/skills
|
|
27
|
+
#
|
|
28
|
+
# Codex and Antigravity SHARE .agents/skills/ at workspace scope, so a
|
|
29
|
+
# single project install serves both.
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class Harness:
|
|
35
|
+
key: str # short id for --targets
|
|
36
|
+
label: str # human-readable label for prints / prompts
|
|
37
|
+
user_dir: Path # global path — always installs into <user_dir>/<skill>/SKILL.md
|
|
38
|
+
project_subdir: str # project-scope path relative to project_root
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
HARNESSES: dict[str, Harness] = {
|
|
42
|
+
"claude": Harness(
|
|
43
|
+
key="claude",
|
|
44
|
+
label="Claude Code",
|
|
45
|
+
user_dir=Path.home() / ".claude" / "skills",
|
|
46
|
+
project_subdir=".claude/skills",
|
|
47
|
+
),
|
|
48
|
+
"codex": Harness(
|
|
49
|
+
key="codex",
|
|
50
|
+
label="OpenAI Codex",
|
|
51
|
+
user_dir=Path.home() / ".agents" / "skills",
|
|
52
|
+
project_subdir=".agents/skills",
|
|
53
|
+
),
|
|
54
|
+
"antigravity": Harness(
|
|
55
|
+
key="antigravity",
|
|
56
|
+
label="Google Antigravity",
|
|
57
|
+
user_dir=Path.home() / ".gemini" / "config" / "skills",
|
|
58
|
+
# antigravity + codex share .agents/skills/ at workspace scope
|
|
59
|
+
project_subdir=".agents/skills",
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Preset bundles for the bootstrap menu.
|
|
64
|
+
PRESETS: dict[str, list[str]] = {
|
|
65
|
+
"claude": ["claude"],
|
|
66
|
+
"codex": ["codex"],
|
|
67
|
+
"antigravity": ["antigravity"],
|
|
68
|
+
"claude+codex": ["claude", "codex"],
|
|
69
|
+
"all": ["claude", "codex", "antigravity"],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _skill_root() -> Path:
|
|
74
|
+
"""Absolute path to the packaged skill/ tree (shipped inside the wheel)."""
|
|
75
|
+
return Path(__file__).resolve().parent.parent / "skill"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _discover_skills(only: str | None = None) -> list[Path]:
|
|
79
|
+
root = _skill_root()
|
|
80
|
+
candidates = sorted(
|
|
81
|
+
p for p in root.iterdir()
|
|
82
|
+
if p.is_dir() and not p.name.startswith("_") and (p / "SKILL.md").exists()
|
|
83
|
+
)
|
|
84
|
+
if only:
|
|
85
|
+
candidates = [p for p in candidates if p.name == only]
|
|
86
|
+
return candidates
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _copy_skills(skills: list[Path], dest_root: Path) -> list[dict]:
|
|
90
|
+
"""Copy every SKILL.md into dest_root/<skill-name>/SKILL.md. Idempotent."""
|
|
91
|
+
dest_root.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
written: list[dict] = []
|
|
93
|
+
for src_dir in skills:
|
|
94
|
+
src = src_dir / "SKILL.md"
|
|
95
|
+
dst_dir = dest_root / src_dir.name
|
|
96
|
+
dst_dir.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
dst = dst_dir / "SKILL.md"
|
|
98
|
+
dst.write_text(src.read_text())
|
|
99
|
+
written.append({
|
|
100
|
+
"skill": src_dir.name,
|
|
101
|
+
"path": str(dst),
|
|
102
|
+
"bytes": dst.stat().st_size,
|
|
103
|
+
})
|
|
104
|
+
return written
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _parse_targets(raw: str) -> list[str]:
|
|
108
|
+
"""Turn a --targets value into a list of harness keys.
|
|
109
|
+
|
|
110
|
+
Accepts presets ('all', 'claude+codex'), commas ('claude,codex'),
|
|
111
|
+
and single keys. Errors on unknown keys.
|
|
112
|
+
"""
|
|
113
|
+
raw = (raw or "").strip().lower()
|
|
114
|
+
if not raw:
|
|
115
|
+
return []
|
|
116
|
+
if raw in PRESETS:
|
|
117
|
+
return PRESETS[raw]
|
|
118
|
+
parts = [p.strip() for p in raw.split(",") if p.strip()]
|
|
119
|
+
unknown = [p for p in parts if p not in HARNESSES]
|
|
120
|
+
if unknown:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"unknown target(s): {unknown}. "
|
|
123
|
+
f"Valid: {sorted(HARNESSES)} or presets: {sorted(PRESETS)}"
|
|
124
|
+
)
|
|
125
|
+
return parts
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _interactive_pick_targets() -> list[str]:
|
|
129
|
+
"""Show a numbered menu; return the chosen preset's harness list."""
|
|
130
|
+
from rich.console import Console
|
|
131
|
+
from rich.prompt import Prompt
|
|
132
|
+
|
|
133
|
+
console = Console()
|
|
134
|
+
console.print()
|
|
135
|
+
console.print("[bold]Choose which AI coding harnesses to install skills into[/bold]")
|
|
136
|
+
console.print()
|
|
137
|
+
console.print(" [cyan]1[/cyan]) claude only — Claude Code")
|
|
138
|
+
console.print(" [cyan]2[/cyan]) claude + codex — Anthropic + OpenAI")
|
|
139
|
+
console.print(" [cyan]3[/cyan]) all — Claude + Codex + Antigravity")
|
|
140
|
+
console.print(" [cyan]4[/cyan]) claude + antigravity — Anthropic + Google")
|
|
141
|
+
console.print(" [cyan]5[/cyan]) codex only")
|
|
142
|
+
console.print(" [cyan]6[/cyan]) antigravity only")
|
|
143
|
+
console.print()
|
|
144
|
+
choice = Prompt.ask("Enter choice", choices=["1", "2", "3", "4", "5", "6"], default="1")
|
|
145
|
+
mapping = {
|
|
146
|
+
"1": ["claude"],
|
|
147
|
+
"2": ["claude", "codex"],
|
|
148
|
+
"3": ["claude", "codex", "antigravity"],
|
|
149
|
+
"4": ["claude", "antigravity"],
|
|
150
|
+
"5": ["codex"],
|
|
151
|
+
"6": ["antigravity"],
|
|
152
|
+
}
|
|
153
|
+
return mapping[choice]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Commands
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@app.command("init")
|
|
162
|
+
def init(
|
|
163
|
+
ctx: typer.Context,
|
|
164
|
+
path: Path = typer.Option(Path.cwd() / "curation", "--path",
|
|
165
|
+
help="Where to create the curation/ folder"),
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Initialize a curation/ folder with a default .dbtips-curate.toml.
|
|
168
|
+
|
|
169
|
+
\b
|
|
170
|
+
Examples:
|
|
171
|
+
dbtips-curate dev init
|
|
172
|
+
dbtips-curate dev init --path ./backend/.../scripts/curation
|
|
173
|
+
"""
|
|
174
|
+
json_mode = ctx.obj.get("json", False)
|
|
175
|
+
cfg_path = write_default_config(path)
|
|
176
|
+
emit_ok({"created": str(cfg_path), "existed": cfg_path.read_text() != ""},
|
|
177
|
+
json_mode=json_mode,
|
|
178
|
+
human=f"✓ Wrote default config at {cfg_path}\n"
|
|
179
|
+
f" Edit it to register additional sections, then run 'dbtips-curate entity track <id>'")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@app.command("install-skill")
|
|
183
|
+
def install_skill(
|
|
184
|
+
ctx: typer.Context,
|
|
185
|
+
home: Path = typer.Option(
|
|
186
|
+
None, "--home",
|
|
187
|
+
help="Override the skills directory. Default: the selected target's global user path.",
|
|
188
|
+
),
|
|
189
|
+
target: str = typer.Option(
|
|
190
|
+
"claude", "--target",
|
|
191
|
+
help=f"Which harness to install for. One of: {sorted(HARNESSES)}. Default: claude.",
|
|
192
|
+
),
|
|
193
|
+
only: str = typer.Option(
|
|
194
|
+
None, "--only",
|
|
195
|
+
help="Install only this skill (e.g. 'dbtips-curate-antibodies').",
|
|
196
|
+
),
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Install every SKILL.md into one harness's skills directory.
|
|
199
|
+
|
|
200
|
+
This is the single-target primitive. For a multi-target install with a
|
|
201
|
+
menu, use `dbtips-curate dev bootstrap` instead.
|
|
202
|
+
|
|
203
|
+
\b
|
|
204
|
+
Examples:
|
|
205
|
+
dbtips-curate dev install-skill # claude, global
|
|
206
|
+
dbtips-curate dev install-skill --target codex # codex, global
|
|
207
|
+
dbtips-curate dev install-skill --target antigravity # antigravity, global
|
|
208
|
+
dbtips-curate dev install-skill --home ./.claude/skills # override path
|
|
209
|
+
dbtips-curate dev install-skill --only dbtips-curate-antibodies
|
|
210
|
+
"""
|
|
211
|
+
json_mode = ctx.obj.get("json", False)
|
|
212
|
+
if target not in HARNESSES:
|
|
213
|
+
emit_error(
|
|
214
|
+
"UNKNOWN_TARGET",
|
|
215
|
+
f"--target must be one of {sorted(HARNESSES)}, got {target!r}",
|
|
216
|
+
json_mode=json_mode,
|
|
217
|
+
)
|
|
218
|
+
raise typer.Exit(code=2)
|
|
219
|
+
harness = HARNESSES[target]
|
|
220
|
+
dest = home or harness.user_dir
|
|
221
|
+
|
|
222
|
+
skills = _discover_skills(only=only)
|
|
223
|
+
if only and not skills:
|
|
224
|
+
emit_ok(
|
|
225
|
+
{"installed": [], "skipped_reason": f"no skill named {only!r}"},
|
|
226
|
+
json_mode=json_mode,
|
|
227
|
+
human=f"✗ No skill matches --only {only!r}. "
|
|
228
|
+
f"Available: {[p.name for p in _discover_skills()]}",
|
|
229
|
+
)
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
written = _copy_skills(skills, dest)
|
|
233
|
+
|
|
234
|
+
human = [f"✓ Installed {len(written)} skill(s) to {harness.label} at {dest}:"]
|
|
235
|
+
for entry in written:
|
|
236
|
+
human.append(f" · {entry['skill']:38s} {entry['bytes']:>6d} bytes")
|
|
237
|
+
human.append("")
|
|
238
|
+
human.append(f" Open {harness.label} and say 'QC ACVR2A target for antibodies' to activate.")
|
|
239
|
+
emit_ok(
|
|
240
|
+
{"installed": written, "target": target, "home": str(dest)},
|
|
241
|
+
json_mode=json_mode,
|
|
242
|
+
human="\n".join(human),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@app.command("bootstrap")
|
|
247
|
+
def bootstrap(
|
|
248
|
+
ctx: typer.Context,
|
|
249
|
+
targets: str = typer.Option(
|
|
250
|
+
None, "--targets",
|
|
251
|
+
help=(
|
|
252
|
+
"Which harnesses to install for. Accepts presets ('all', 'claude+codex') "
|
|
253
|
+
"or a comma-separated list ('claude,codex,antigravity'). "
|
|
254
|
+
"Interactive menu if omitted."
|
|
255
|
+
),
|
|
256
|
+
),
|
|
257
|
+
project: Path = typer.Option(
|
|
258
|
+
None, "--project",
|
|
259
|
+
help=(
|
|
260
|
+
"Also install into <project>/<harness-project-subdir>/ (e.g. .claude/skills, "
|
|
261
|
+
".agents/skills). Skipped if not given."
|
|
262
|
+
),
|
|
263
|
+
),
|
|
264
|
+
only: str = typer.Option(
|
|
265
|
+
None, "--only",
|
|
266
|
+
help="Install only this skill by name (e.g. 'dbtips-curate-antibodies').",
|
|
267
|
+
),
|
|
268
|
+
) -> None:
|
|
269
|
+
"""One-shot install of all skills across selected AI coding harnesses.
|
|
270
|
+
|
|
271
|
+
Discovers every SKILL.md packaged with dbtips-curate and copies it into the
|
|
272
|
+
right skills directory for each chosen target. All targets read the same
|
|
273
|
+
agentskills.io SKILL.md format — no conversion.
|
|
274
|
+
|
|
275
|
+
Path matrix:
|
|
276
|
+
|
|
277
|
+
\b
|
|
278
|
+
claude ~/.claude/skills/<name>/ and <project>/.claude/skills/<name>/
|
|
279
|
+
codex ~/.agents/skills/<name>/ and <project>/.agents/skills/<name>/
|
|
280
|
+
antigravity ~/.gemini/config/skills/<name>/ and <project>/.agents/skills/<name>/
|
|
281
|
+
(codex + antigravity share .agents/ at workspace scope)
|
|
282
|
+
|
|
283
|
+
\b
|
|
284
|
+
Examples:
|
|
285
|
+
dbtips-curate dev bootstrap # interactive menu
|
|
286
|
+
dbtips-curate dev bootstrap --targets all # claude + codex + antigravity
|
|
287
|
+
dbtips-curate dev bootstrap --targets claude,codex --project .
|
|
288
|
+
uvx dbtips-curate dev bootstrap # one-shot, no persistent install
|
|
289
|
+
"""
|
|
290
|
+
json_mode = ctx.obj.get("json", False)
|
|
291
|
+
|
|
292
|
+
# Resolve targets — non-interactive if --targets given, else prompt.
|
|
293
|
+
try:
|
|
294
|
+
target_list = _parse_targets(targets) if targets else _interactive_pick_targets()
|
|
295
|
+
except ValueError as e:
|
|
296
|
+
emit_error("BAD_TARGETS", str(e), json_mode=json_mode)
|
|
297
|
+
raise typer.Exit(code=2)
|
|
298
|
+
if not target_list:
|
|
299
|
+
emit_error(
|
|
300
|
+
"NO_TARGETS",
|
|
301
|
+
"no targets selected; pass --targets or use the interactive prompt",
|
|
302
|
+
json_mode=json_mode,
|
|
303
|
+
)
|
|
304
|
+
raise typer.Exit(code=2)
|
|
305
|
+
|
|
306
|
+
skills = _discover_skills(only=only)
|
|
307
|
+
if only and not skills:
|
|
308
|
+
emit_error(
|
|
309
|
+
"SKILL_NOT_FOUND",
|
|
310
|
+
f"no skill named {only!r} — available: {[p.name for p in _discover_skills()]}",
|
|
311
|
+
json_mode=json_mode,
|
|
312
|
+
)
|
|
313
|
+
raise typer.Exit(code=2)
|
|
314
|
+
|
|
315
|
+
results: list[dict] = []
|
|
316
|
+
for tkey in target_list:
|
|
317
|
+
harness = HARNESSES[tkey]
|
|
318
|
+
# Global scope — always writes.
|
|
319
|
+
user_written = _copy_skills(skills, harness.user_dir)
|
|
320
|
+
results.append({
|
|
321
|
+
"target": tkey,
|
|
322
|
+
"scope": "global",
|
|
323
|
+
"path": str(harness.user_dir),
|
|
324
|
+
"count": len(user_written),
|
|
325
|
+
})
|
|
326
|
+
# Project scope — only when --project given.
|
|
327
|
+
if project is not None:
|
|
328
|
+
proj_dest = project.resolve() / harness.project_subdir
|
|
329
|
+
proj_written = _copy_skills(skills, proj_dest)
|
|
330
|
+
results.append({
|
|
331
|
+
"target": tkey,
|
|
332
|
+
"scope": "project",
|
|
333
|
+
"path": str(proj_dest),
|
|
334
|
+
"count": len(proj_written),
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
# Human-readable summary.
|
|
338
|
+
human = [f"✓ Installed {len(skills)} skill(s) into {len(target_list)} target(s):"]
|
|
339
|
+
for r in results:
|
|
340
|
+
label = HARNESSES[r["target"]].label
|
|
341
|
+
scope = r["scope"]
|
|
342
|
+
human.append(f" · {label:22s} ({scope:7s}) {r['path']}")
|
|
343
|
+
human.append("")
|
|
344
|
+
human.append(
|
|
345
|
+
" Open your harness (Claude Code / Codex / Antigravity) and try:"
|
|
346
|
+
)
|
|
347
|
+
human.append(' "QC ACVR2A target for antibodies"')
|
|
348
|
+
if project is None:
|
|
349
|
+
human.append("")
|
|
350
|
+
human.append(
|
|
351
|
+
" Tip: pass --project . to also install into the current repo's "
|
|
352
|
+
"workspace-scope skills folder."
|
|
353
|
+
)
|
|
354
|
+
emit_ok(
|
|
355
|
+
{"targets": target_list, "results": results, "skill_count": len(skills)},
|
|
356
|
+
json_mode=json_mode,
|
|
357
|
+
human="\n".join(human),
|
|
358
|
+
)
|