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.
Files changed (35) hide show
  1. dbtips_curate/__init__.py +1 -0
  2. dbtips_curate/cli/__init__.py +0 -0
  3. dbtips_curate/cli/_common.py +67 -0
  4. dbtips_curate/cli/_output.py +51 -0
  5. dbtips_curate/cli/_widget.py +207 -0
  6. dbtips_curate/cli/dev.py +358 -0
  7. dbtips_curate/cli/entity.py +723 -0
  8. dbtips_curate/cli/history.py +213 -0
  9. dbtips_curate/cli/main.py +70 -0
  10. dbtips_curate/cli/op.py +399 -0
  11. dbtips_curate/core/__init__.py +0 -0
  12. dbtips_curate/core/config.py +181 -0
  13. dbtips_curate/core/fs.py +154 -0
  14. dbtips_curate/core/git.py +80 -0
  15. dbtips_curate/core/git_history.py +183 -0
  16. dbtips_curate/core/merge.py +314 -0
  17. dbtips_curate/core/ops.py +78 -0
  18. dbtips_curate/core/schema.py +161 -0
  19. dbtips_curate/core/worklist.py +166 -0
  20. dbtips_curate/mcp_server.py +114 -0
  21. dbtips_curate/skill/_deferred/targetability-design-note.md +170 -0
  22. dbtips_curate/skill/dbtips-curate/SKILL.md +400 -0
  23. dbtips_curate/skill/dbtips-curate-antibodies/SKILL.md +219 -0
  24. dbtips_curate/skill/dbtips-curate-assayability/SKILL.md +175 -0
  25. dbtips_curate/skill/dbtips-curate-gene-essentiality/SKILL.md +107 -0
  26. dbtips_curate/skill/dbtips-curate-ontology/SKILL.md +129 -0
  27. dbtips_curate/skill/dbtips-curate-orthologs/SKILL.md +123 -0
  28. dbtips_curate/skill/dbtips-curate-supplementary-materials/SKILL.md +147 -0
  29. dbtips_curate/skill/dbtips-curate-target-literature/SKILL.md +286 -0
  30. dbtips_curate/skill/dbtips-curate-target-pathway/SKILL.md +166 -0
  31. dbtips_curate/skill/dbtips-curate-target-pipeline/SKILL.md +291 -0
  32. dbtips_curate-0.2.0.dist-info/METADATA +278 -0
  33. dbtips_curate-0.2.0.dist-info/RECORD +35 -0
  34. dbtips_curate-0.2.0.dist-info/WHEEL +4 -0
  35. dbtips_curate-0.2.0.dist-info/entry_points.txt +3 -0
@@ -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
+ )