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/SKILL.md +145 -0
- craft_kit/__init__.py +3 -0
- craft_kit/__main__.py +5 -0
- craft_kit/agent.py +165 -0
- craft_kit/banner.py +86 -0
- craft_kit/brief.py +101 -0
- craft_kit/browser.py +198 -0
- craft_kit/checks.py +219 -0
- craft_kit/cli.py +394 -0
- craft_kit/home.py +370 -0
- craft_kit/png.py +135 -0
- craft_kit/proc.py +161 -0
- craft_kit/study.py +170 -0
- craft_kit/tables.py +154 -0
- craft_kit/ui.py +284 -0
- craft_kit/workspace.py +320 -0
- craftkit-0.3.0.dist-info/METADATA +174 -0
- craftkit-0.3.0.dist-info/RECORD +23 -0
- craftkit-0.3.0.dist-info/WHEEL +5 -0
- craftkit-0.3.0.dist-info/entry_points.txt +5 -0
- craftkit-0.3.0.dist-info/licenses/LICENSE-APACHE +177 -0
- craftkit-0.3.0.dist-info/licenses/LICENSE-MIT +21 -0
- craftkit-0.3.0.dist-info/top_level.txt +1 -0
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,,
|