craftkit 0.3.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.
craft_kit/workspace.py ADDED
@@ -0,0 +1,320 @@
1
+ """The project folder and its one state file, craft.json (versioned, written atomically).
2
+
3
+ <project>/
4
+ craft.json request, target, kind, stack, mode, rounds used, step states
5
+ BRIEF.md what the builder is told (regenerated before each first turn)
6
+ capture/ Study's evidence, README.md first (read-only for the builder)
7
+ reference/ pictures or files the person adds (read-only)
8
+ app/ the only folder the builder changes
9
+ checks/round-N/ each check: result.json, screenshots, logs
10
+ checks/turn-N/ each builder turn: the message sent, its log and reply
11
+ unknowns.md open questions about the original, kept by the builder
12
+ questions.json questions the builder asks the person, with their answers
13
+ feed.log everything that happened, in order
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import hashlib
19
+ import json
20
+ import os
21
+ import re
22
+ import tempfile
23
+ import time
24
+ from pathlib import Path
25
+ from typing import Optional
26
+
27
+ from . import proc, tables
28
+
29
+ STATE = "craft.json"
30
+ VERSION = 1
31
+ STEPS = ["study", "build", "check", "ready"]
32
+ IGNORED = {"node_modules", "target", "dist", "out", "build", ".git", "__pycache__", ".venv", "venv", ".next", "shots", ".pytest_cache"}
33
+
34
+
35
+ class CraftError(Exception):
36
+ """A failure with a plain-English remediation."""
37
+
38
+ def __init__(self, message: str, fix: str = ""):
39
+ super().__init__(message)
40
+ self.message = message
41
+ self.fix = fix
42
+
43
+
44
+ def write_atomic(path: Path, data: str) -> None:
45
+ path.parent.mkdir(parents=True, exist_ok=True)
46
+ fd, tmp = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent))
47
+ try:
48
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as f:
49
+ f.write(data)
50
+ os.chmod(tmp, 0o644)
51
+ for attempt in range(5): # Windows: a scanner may hold the file for a moment
52
+ try:
53
+ os.replace(tmp, path)
54
+ return
55
+ except PermissionError:
56
+ if attempt == 4:
57
+ raise
58
+ time.sleep(0.2)
59
+ finally:
60
+ if os.path.exists(tmp):
61
+ os.unlink(tmp)
62
+
63
+
64
+ def read_json(path: Path, default=None):
65
+ try:
66
+ return json.loads(path.read_text("utf-8"))
67
+ except (OSError, ValueError):
68
+ return default
69
+
70
+
71
+ def parse_target(raw: str) -> dict:
72
+ raw = (raw or "").strip()
73
+ if not raw:
74
+ return {"type": "none", "value": ""}
75
+ if re.match(r"^https?://", raw, re.I):
76
+ return {"type": "website", "value": raw}
77
+ p = Path(raw).expanduser()
78
+ if p.exists():
79
+ return {"type": "local", "value": str(p.resolve())}
80
+ found = proc.which(raw)
81
+ if found and os.sep not in raw:
82
+ return {"type": "local", "value": found}
83
+ if re.match(r"^[\w-]+(\.[\w-]+)+(/.*)?$", raw):
84
+ return {"type": "website", "value": f"https://{raw}"}
85
+ raise CraftError(f"Can't find the target “{raw}”.", "Give a web address (https://…), a file or folder path, a command on your PATH, or nothing to build from the request alone.")
86
+
87
+
88
+ def slug(text: str) -> str:
89
+ s = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
90
+ return (s[:40].rstrip("-") or "project")
91
+
92
+
93
+ # The projects the person has made or opened, for the home screen (config_dir/projects.json).
94
+ REGISTRY_LIMIT = 300
95
+
96
+
97
+ def projects_dir() -> Path:
98
+ """Where the home screen creates new projects."""
99
+ return Path(os.environ.get("CRAFT_PROJECTS_DIR") or Path.home() / "craft-projects").expanduser()
100
+
101
+
102
+ def _registry_path() -> Path:
103
+ return tables.config_dir() / "projects.json"
104
+
105
+
106
+ def remembered() -> list[dict]:
107
+ data = read_json(_registry_path(), {})
108
+ items = data.get("projects") if isinstance(data, dict) else None
109
+ if not isinstance(items, list):
110
+ return []
111
+ good = [i for i in items if isinstance(i, dict) and isinstance(i.get("path"), str)]
112
+ return sorted(good, key=lambda i: str(i.get("opened", "")), reverse=True)
113
+
114
+
115
+ def _save_registry(items: list[dict]) -> None:
116
+ try:
117
+ write_atomic(_registry_path(), json.dumps({"version": 1, "projects": items[:REGISTRY_LIMIT]}, indent=1, ensure_ascii=False) + "\n")
118
+ except OSError:
119
+ pass # the list is a convenience: never fail a command over it
120
+
121
+
122
+ def remember(root: Path) -> None:
123
+ path = str(Path(root).resolve())
124
+ items = [i for i in remembered() if i["path"] != path]
125
+ items.insert(0, {"path": path, "opened": time.strftime("%Y-%m-%dT%H:%M:%S")})
126
+ _save_registry(items)
127
+
128
+
129
+ def forget(path: str) -> None:
130
+ _save_registry([i for i in remembered() if i["path"] != path])
131
+
132
+
133
+ def find_root(start: Path) -> Path:
134
+ here = start.resolve()
135
+ for d in [here, *here.parents]:
136
+ if (d / STATE).is_file():
137
+ return d
138
+ raise CraftError("This folder isn't a craft project.", "Run `craft new` to start one, or `craft -C <project> …`.")
139
+
140
+
141
+ class Project:
142
+ def __init__(self, root: Path, state: dict):
143
+ self.root = root
144
+ self.state = state
145
+
146
+ # paths -------------------------------------------------------------------
147
+ @property
148
+ def app(self) -> Path:
149
+ return self.root / "app"
150
+
151
+ @property
152
+ def capture(self) -> Path:
153
+ return self.root / "capture"
154
+
155
+ @property
156
+ def reference(self) -> Path:
157
+ return self.root / "reference"
158
+
159
+ def round_dir(self, n: int) -> Path:
160
+ return self.root / "checks" / f"round-{n}"
161
+
162
+ # state -------------------------------------------------------------------
163
+ @classmethod
164
+ def create(cls, folder: Path, target: dict, request: str, kind: str, stack: str, deep: bool, allow_run: bool) -> "Project":
165
+ if (folder / STATE).exists():
166
+ raise CraftError(f"{folder} is already a craft project.", "Use `craft run` there, or pick another folder with --dir.")
167
+ state = {
168
+ "version": VERSION,
169
+ "name": folder.name,
170
+ "request": request,
171
+ "target": target,
172
+ "kind": kind,
173
+ "stack": stack,
174
+ "deep": deep,
175
+ "allow_run": allow_run,
176
+ "max_rounds": tables.MAX_ROUNDS,
177
+ "good_enough": tables.GOOD_ENOUGH,
178
+ "rounds_used": 0,
179
+ "round": 0,
180
+ "steps": {s: "todo" for s in STEPS},
181
+ "agent": None,
182
+ "session": None,
183
+ "unchanged": 0,
184
+ "fingerprint": None,
185
+ "created": time.strftime("%Y-%m-%dT%H:%M:%S"),
186
+ }
187
+ for d in ("app", "capture", "reference", "checks"):
188
+ (folder / d).mkdir(parents=True, exist_ok=True)
189
+ p = cls(folder, state)
190
+ unknowns = folder / "unknowns.md"
191
+ if not unknowns.exists():
192
+ write_atomic(unknowns, "# Unknowns\n\nQuestions the evidence doesn't answer yet.\n`- [ ] question — what would settle it — impact: high|medium|low`; tick with the answer and its evidence.\n\n")
193
+ p.save()
194
+ remember(folder)
195
+ p.log(f"new project: {request!r} target={target['value'] or '-'} kind={kind} stack={stack} mode={'deep' if deep else 'clean room'}")
196
+ return p
197
+
198
+ @classmethod
199
+ def open(cls, start: Path) -> "Project":
200
+ root = find_root(start)
201
+ state = read_json(root / STATE)
202
+ if not isinstance(state, dict):
203
+ raise CraftError(f"{root / STATE} can't be read.", "Fix or delete the file; it is JSON.")
204
+ v = state.get("version")
205
+ if not isinstance(v, int) or v > VERSION:
206
+ raise CraftError(f"{STATE} says version {v}; this craft reads version {VERSION}.", "Update craft (`pipx upgrade craftkit`, `uv tool upgrade craftkit` or `npm update -g craftkit`).")
207
+ # migrations go here as versions are added
208
+ state.setdefault("steps", {s: "todo" for s in STEPS})
209
+ remember(root)
210
+ return cls(root, state)
211
+
212
+ def save(self) -> None:
213
+ write_atomic(self.root / STATE, json.dumps(self.state, indent=2, ensure_ascii=False) + "\n")
214
+
215
+ def set_step(self, step: str, status: str) -> None:
216
+ self.state["steps"][step] = status
217
+ self.save()
218
+
219
+ def log(self, text: str) -> None:
220
+ try:
221
+ with open(self.root / "feed.log", "a", encoding="utf-8") as f:
222
+ f.write(f"{time.strftime('%H:%M:%S')} {text}\n")
223
+ except OSError:
224
+ pass
225
+
226
+ @property
227
+ def deep(self) -> bool:
228
+ return bool(self.state.get("deep"))
229
+
230
+ def mode(self) -> str:
231
+ return "deep" if self.deep else "clean room"
232
+
233
+ # what the builder made -----------------------------------------------------
234
+ def app_spec(self) -> dict:
235
+ """How the build is built, tested and run: app/craft-app.json if the builder wrote one,
236
+ else what its files show. Lists of program + arguments; `site` is the folder to serve."""
237
+ spec: dict = {}
238
+ declared = read_json(self.app / "craft-app.json", {})
239
+ if isinstance(declared, dict):
240
+ for key in ("install", "build", "test", "run"):
241
+ v = declared.get(key)
242
+ if isinstance(v, list) and v and all(isinstance(a, str) for a in v):
243
+ spec[key] = v
244
+ if isinstance(declared.get("site"), str):
245
+ spec["site"] = declared["site"]
246
+ for tc in tables.TOOLCHAINS:
247
+ if (self.app / tc["file"]).exists():
248
+ for key in ("install", "build", "test"):
249
+ if key not in spec and key in tc:
250
+ spec[key] = list(tc[key])
251
+ spec.setdefault("toolchain", tc["file"])
252
+ break
253
+ return spec
254
+
255
+ def site_dir(self) -> Optional[Path]:
256
+ spec = self.app_spec()
257
+ names = [spec["site"]] if "site" in spec else tables.SITE_DIRS
258
+ for n in names:
259
+ d = (self.app / n).resolve()
260
+ if self.app.resolve() in [d, *d.parents] and (d / "index.html").is_file():
261
+ return d
262
+ return None
263
+
264
+ def fingerprint(self) -> str:
265
+ h = hashlib.sha256()
266
+ count = 0
267
+ for dirpath, dirnames, filenames in os.walk(self.app):
268
+ dirnames[:] = sorted(d for d in dirnames if d not in IGNORED)
269
+ for name in sorted(filenames):
270
+ path = Path(dirpath) / name
271
+ try:
272
+ st = path.stat()
273
+ h.update(str(path.relative_to(self.app)).encode())
274
+ if st.st_size <= 1_000_000:
275
+ h.update(path.read_bytes())
276
+ else:
277
+ h.update(f"{st.st_size}:{st.st_mtime_ns}".encode())
278
+ except OSError:
279
+ continue
280
+ count += 1
281
+ if count > 20_000:
282
+ return h.hexdigest()
283
+ return h.hexdigest()
284
+
285
+ # questions -----------------------------------------------------------------
286
+ def questions(self) -> list[dict]:
287
+ data = read_json(self.root / "questions.json", {})
288
+ qs = data.get("questions") if isinstance(data, dict) else None
289
+ return [q for q in qs if isinstance(q, dict) and isinstance(q.get("question"), str)] if isinstance(qs, list) else []
290
+
291
+ def pending_questions(self) -> list[dict]:
292
+ return [q for q in self.questions() if not q.get("answer")]
293
+
294
+ def undelivered_answers(self) -> list[dict]:
295
+ """Answers the builder hasn't been given yet (typed here or written into questions.json)."""
296
+ return [q for q in self.questions() if q.get("answer") and not q.get("delivered")]
297
+
298
+ def mark_delivered(self, answered: list[dict]) -> None:
299
+ if not answered:
300
+ return
301
+ done = {q["question"] for q in answered}
302
+ qs = self.questions()
303
+ for q in qs:
304
+ if q["question"] in done:
305
+ q["delivered"] = True
306
+ write_atomic(self.root / "questions.json", json.dumps({"version": 1, "questions": qs}, indent=2, ensure_ascii=False) + "\n")
307
+
308
+ def save_answers(self, answered: list[dict]) -> None:
309
+ by_q = {q["question"]: q.get("answer") for q in answered}
310
+ qs = self.questions()
311
+ for q in qs:
312
+ if q["question"] in by_q and by_q[q["question"]]:
313
+ q["answer"] = by_q[q["question"]]
314
+ write_atomic(self.root / "questions.json", json.dumps({"version": 1, "questions": qs}, indent=2, ensure_ascii=False) + "\n")
315
+
316
+ def open_unknowns(self) -> int:
317
+ try:
318
+ return sum(1 for line in (self.root / "unknowns.md").read_text("utf-8").splitlines() if line.lstrip().startswith("- [ ]"))
319
+ except OSError:
320
+ return 0
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: craftkit
3
+ Version: 0.3.0
4
+ Summary: Study → build → check → refine any software, from the terminal, with your coding agent.
5
+ License-Expression: MIT OR Apache-2.0
6
+ Keywords: cli,coding-agent,claude-code,codex,gemini,rebuild,skill
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Environment :: Console
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Code Generators
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE-MIT
16
+ License-File: LICENSE-APACHE
17
+ Provides-Extra: browser
18
+ Requires-Dist: playwright>=1.40; extra == "browser"
19
+ Dynamic: license-file
20
+
21
+ # craftkit
22
+
23
+ > **In English:** `craft` rebuilds any software (website, CLI, app, game, library) with the coding
24
+ > agent you already use (Claude Code, Codex, Gemini CLI): it studies the original, briefs the agent,
25
+ > checks the result against the original and sends it back until it holds. Install with
26
+ > `npm install -g craftkit` or `pipx install craftkit`, then type `craft`.
27
+
28
+ La méthode de Craft Anything, réduite à l'essentiel : **étudier → construire → vérifier → affiner**,
29
+ pour recréer ou construire n'importe quel logiciel (site web, outil en ligne de commande, application,
30
+ jeu, librairie), depuis le terminal.
31
+
32
+ Deux façons de l'utiliser :
33
+
34
+ 1. **Le skill** (`craft_kit/SKILL.md`) : la méthode seule, en Markdown. N'importe quel agent de code
35
+ (Claude Code, Codex, Gemini CLI, Cursor…) peut la suivre, avec ou sans la CLI.
36
+ 2. **La CLI `craft`** : la même méthode, outillée. Elle tient le dossier du projet, collecte les
37
+ preuves, donne la tâche à un agent de code, mesure le résultat et relance l'agent jusqu'à ce que
38
+ ça passe. Python 3.9+, **aucune dépendance**.
39
+
40
+ ```text
41
+ craft · horloge-pomodoro cli · Rust · clean room
42
+ ▍study ./pomodoro ────────────────────────────────────────────────
43
+ ✓ runs 4 runs recorded
44
+ ▍build building · turn 1 ─────────────────────────────────────────
45
+ 0:03 ◇ read BRIEF.md
46
+ 0:05 ☰ plan 2/5 · Writing the timer
47
+ 0:41 ✚ write app/src/main.rs
48
+ 1:02 ❯ run cargo build
49
+ 1:20 ✗ failed error[E0425]: cannot find value `paused`
50
+ 1:31 ✎ edit app/src/main.rs
51
+ └ 2m14s · 14 actions: 6 edit, 4 run, 3 read, 1 plan · 1 failed
52
+ ✓ build I built the timer with pause and resume.
53
+ ▍check round 1 ───────────────────────────────────────────────────
54
+ ✗ runs 6 of 8 claims hold; output 71% alike.
55
+ │ With --help, it prints what the original prints: 52% of the words match
56
+ ▍refine refining 1/6 · turn 2 ────────────────────────────────────
57
+ ⠼ refining 1/6 · claude · 0m48s · 3 · Edit app/src/main.rs
58
+ ```
59
+
60
+ Pendant le travail, chaque action de l'agent s'affiche en direct (lecture, écriture, commande,
61
+ recherche, plan, échec), chacune avec sa couleur, et chaque étape a la sienne : étude bleu ciel,
62
+ construction bleu, affinage violet, vérification ambre, prêt vert.
63
+
64
+ ## Installation
65
+
66
+ Gratuit, au choix :
67
+
68
+ ```sh
69
+ npm install -g craftkit # avec Node ; il faut aussi Python 3.9+ sur la machine
70
+ pipx install craftkit # ou : uv tool install craftkit
71
+ ```
72
+
73
+ Pour l'essayer sans rien installer : `npx craftkit` ou `uvx craftkit`.
74
+ Ensuite, le skill pour Claude Code (tous les projets) : `craft skill --install ~/.claude/skills`.
75
+
76
+ Les deux paquets contiennent exactement le même programme Python (aucune dépendance) ; celui de npm
77
+ le lance avec le Python de la machine (`CRAFT_PYTHON=/chemin/vers/python3` pour en choisir un).
78
+ Mise à jour : `npm update -g craftkit`, `pipx upgrade craftkit` ou `uv tool upgrade craftkit`.
79
+
80
+ Depuis les sources : `pipx install .` dans ce dossier, ou `python3 -m craft_kit …` sans installer.
81
+
82
+ Optionnel, pour les sites web : un navigateur pour les captures. La CLI prend, dans l'ordre,
83
+ Playwright pour Python (`pip install playwright && playwright install chromium`, qui ajoute les
84
+ mesures visibles : polices, couleurs, titres, texte), sinon Chrome/Edge installé (en mode headless,
85
+ avec un profil temporaire), sinon Playwright via `npx`.
86
+
87
+ ## Utilisation
88
+
89
+ Tapez simplement **`craft`** : l'écran d'accueil affiche le titre CRAFT KIT, vos créations
90
+ précédentes et « + New project ».
91
+
92
+ | Touche | Action |
93
+ |--------|--------|
94
+ | ↑ ↓ (ou j k) | se déplacer |
95
+ | Entrée | ouvrir le projet : continuer, demander un changement, ouvrir le résultat, revérifier, voir le dossier |
96
+ | n | nouveau projet (quelques questions, créé dans `~/craft-projects`, ou `CRAFT_PROJECTS_DIR`) |
97
+ | x | retirer de la liste (le dossier n'est pas supprimé) |
98
+ | q / Échap | retour, ou quitter |
99
+
100
+ La liste contient tous les projets créés ou ouverts avec `craft` (mémorisés dans
101
+ `~/.config/craft/projects.json`), plus ceux du dossier courant.
102
+
103
+ Raccourcis : `craft-new …` = `craft new …`, `craft-run …` = `craft run …`.
104
+
105
+ En ligne de commande directe :
106
+
107
+ ```sh
108
+ craft new https://exemple.com "recrée ce site en React" # crée ./recree-ce-site-en-react/
109
+ cd recree-ce-site-en-react
110
+ craft run # toute la boucle
111
+ craft run "le bouton doit être bleu" # une modification (nouveau budget d'affinage)
112
+ craft open # ouvrir le résultat
113
+ ```
114
+
115
+ | Commande | Rôle |
116
+ |----------|------|
117
+ | `craft new [CIBLE] [DEMANDE…]` | crée le projet ; demande ce qui manque. CIBLE = URL, fichier, dossier, commande du PATH, ou rien |
118
+ | `craft run [MESSAGE…]` | étude (si pas faite) → construction → vérification → affinage, jusqu'à la réussite |
119
+ | `craft study` / `build` / `check` | une étape à la fois |
120
+ | `craft check --self` | la vérification que l'agent lance lui-même pendant qu'il travaille (non comptée) |
121
+ | `craft` | écran d'accueil : créations précédentes, nouveau projet |
122
+ | `craft status` | où en est le projet |
123
+ | `craft answer` | répondre aux questions de l'agent |
124
+ | `craft brief` | afficher la tâche, pour la coller dans n'importe quel agent |
125
+ | `craft skill [--install DOSSIER]` | afficher ou installer le SKILL.md |
126
+
127
+ Options de `new` : `--kind website|cli|app|game|tool`, `--stack rust|python|react|html|…`,
128
+ `--deep` (regarder à l'intérieur de l'original, usage privé), `--allow-run` (lancer le programme
129
+ d'origine pour enregistrer ce qu'il affiche), `--dir`.
130
+
131
+ ## Ce que la CLI reprend de Craft Anything
132
+
133
+ | Craft Anything | craft-kit |
134
+ |----------------|-----------|
135
+ | Clean room / Deep | même règle : par défaut, seulement ce qu'un visiteur voit ; `--deep` pour regarder dedans |
136
+ | Study → `capture/README.md` | captures à 1440/768/390 px + mesures visibles ; exécutions d'un CLI ; inventaire en Deep |
137
+ | Le builder ne touche que `app/` | idem, et `app/craft-app.json` déclare build/test/run |
138
+ | `rebuild_check` pendant la construction | `craft check --self` |
139
+ | Contrats `--screenshot` / `--self-test` | identiques (applis et jeux) |
140
+ | Comparaison des runs d'un programme console | même code de sortie + ≥ 80 % de mots communs |
141
+ | `max_rounds` 6, `good_enough` 90 %, arrêt après 2 tours sans changement | identiques ; le budget survit aux relances, seule une nouvelle demande le remet à zéro |
142
+ | Cartes question/approbation | `questions.json` → questions numérotées dans le terminal |
143
+ | Inconnues (`core_unknown_add`) | `unknowns.md` |
144
+ | Processus : stdin fermé, délais, groupe tué | idem (`proc.py`) |
145
+
146
+ Ce qu'elle ne reprend pas : les ~320 outils d'analyse (désassembleur, décompilateur, Ghidra, adb,
147
+ Frida…), l'interface graphique et le serveur MCP. En mode Deep, c'est l'agent qui inspecte avec ses
148
+ propres outils.
149
+
150
+ ## Agents
151
+
152
+ Ordre par défaut : `claude`, `codex`, `gemini` (le premier installé). Choisir : `--agent codex` ou
153
+ `CRAFT_AGENT=codex`. La tâche est envoyée sur l'entrée standard (jamais en argument : elle peut être
154
+ longue). Ajouter ou modifier un agent dans `~/.config/craft/agents.json` :
155
+
156
+ ```json
157
+ {"mon-agent": {"argv": ["mon-agent", "--non-interactive"], "stream": "text"}}
158
+ ```
159
+
160
+ Claude Code est lancé avec `--permission-mode acceptEdits` et une liste d'outils autorisés
161
+ (lecture/écriture, npm, cargo, go, python, make, craft…), pas en contournant les permissions.
162
+
163
+ Délais : `CRAFT_TURN_TIMEOUT` (3 h par tour), `CRAFT_IDLE_TIMEOUT` (20 min sans sortie).
164
+ Navigateur : `CRAFT_BROWSER=python|chrome|npx|none`, `CRAFT_CHROME=/chemin/vers/chrome`.
165
+ Affichage : `CRAFT_FEED=0` garde seulement la ligne animée (sans le fil d'actions) ; `NO_COLOR=1`
166
+ retire les couleurs. Hors terminal (pipe, CI), le fil s'écrit en texte simple, une ligne par action.
167
+
168
+ ## Limites connues
169
+
170
+ - La comparaison visuelle est une grille de couleurs (64 colonnes, tolérance 32) : elle repère les
171
+ écarts de mise en page et de couleur, pas les détails fins.
172
+ - Avec Chrome headless, la capture fait 2400 px de haut, pas la page entière ; les mesures visibles
173
+ (polices, titres, texte) demandent Playwright pour Python.
174
+ - Les adaptateurs `codex` et `gemini` n'ont pas été essayés en vrai.
@@ -0,0 +1,23 @@
1
+ craft_kit/SKILL.md,sha256=5pLGJ0gJGQ52cdhAsk08elLHzn_ZowKkeIerr3QoyoA,8406
2
+ craft_kit/__init__.py,sha256=Sark2099VyVu-REBs2AVCRvWRREmCQzIfBn9niAwHVY,110
3
+ craft_kit/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
4
+ craft_kit/agent.py,sha256=qOH2I8HZk2nIL4b7RNMDfGqKwLXWDCkobKcytpP1tR8,7704
5
+ craft_kit/banner.py,sha256=RtOoEF4iZdwwvVimtGoAsNgzJSb0Wl0A4LCnh4FJBLk,3544
6
+ craft_kit/brief.py,sha256=jAbmYeUNknRRN11sQDYp35bxJP1AxGpEmfc_JGfGH58,7862
7
+ craft_kit/browser.py,sha256=nMGprBKH352UjdsxegiS8eQlATD3aFezS2KD0cejGrA,8563
8
+ craft_kit/checks.py,sha256=6iMH2pY3jeMRZuzreRwWSlvlLwMzDTaW50KeTBNdNvk,11597
9
+ craft_kit/cli.py,sha256=5d2Tq-0OxrJ8AsN3DudfPD6EEHm-oF5WDdauzMJ8uc8,15928
10
+ craft_kit/home.py,sha256=10NL8dQOT3PUSCw_eybbhr1r7TTvSndNRuTobXrNyjA,15121
11
+ craft_kit/png.py,sha256=ouawGipJIbzj4Mvxcg_7ifRAVPf_IAGxb_h--8E2sjk,5170
12
+ craft_kit/proc.py,sha256=I7MqSYQV4-6r4f232E_Nr59gtHDEPsOKFztwY7gA8IM,5106
13
+ craft_kit/study.py,sha256=GNdtK7wiL7ktkwSUAQ396Y4zmFwqarVgvmkYotWvGQg,7963
14
+ craft_kit/tables.py,sha256=fApV6te6rUPqeob1NbDKR4Lwks2jrBgqdhO0hwK30z0,8325
15
+ craft_kit/ui.py,sha256=T9adCzuYhb1bdDcfITFpkKA3EyCnari-MskIZPK8Z1Q,10896
16
+ craft_kit/workspace.py,sha256=mu7L-BzR4t4S2bAnrZ-Pkl15SEal_aQjxrk5NSN2JX4,12497
17
+ craftkit-0.3.0.dist-info/licenses/LICENSE-APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
18
+ craftkit-0.3.0.dist-info/licenses/LICENSE-MIT,sha256=d3tfDGW9nCicsjTjKFe1lHRdXHbWEwNIcKoFvQDTJiI,1106
19
+ craftkit-0.3.0.dist-info/METADATA,sha256=FQf1Skq5E9lMiHsyLAoFe5ErLE2Uh0l4w_NeJnc2hdE,9417
20
+ craftkit-0.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
21
+ craftkit-0.3.0.dist-info/entry_points.txt,sha256=zYsBuXnXq6R7GctQayEx5gNE8T28SeCgLO6WFIjfPHU,145
22
+ craftkit-0.3.0.dist-info/top_level.txt,sha256=uU70H-OKnfKZ18GpHxj2PTjwQ66nTtprja_UZrAXnQs,10
23
+ craftkit-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ craft = craft_kit.cli:main
3
+ craft-new = craft_kit.cli:main_new
4
+ craft-run = craft_kit.cli:main_run
5
+ craftkit = craft_kit.cli:main