-
-
Notifications
You must be signed in to change notification settings - Fork 244
Expand file tree
/
Copy pathsetup.py
More file actions
377 lines (316 loc) · 13.5 KB
/
setup.py
File metadata and controls
377 lines (316 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
#!/usr/bin/env python3
"""
Flow Kit — AI Tool Setup
Generates AI tool configs from skills/fk:*.md (single source of truth).
Usage:
python setup.py # Interactive: pick your AI tool(s)
python setup.py --tool claude # Generate for Claude
python setup.py --tool gemini # Generate for Gemini
python setup.py --tool codex # Generate for Codex
python setup.py --tool all # Generate for all tools
python setup.py sync # Re-sync all previously selected tools
python setup.py clean # Remove all generated tool configs
"""
import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).parent
SKILLS_DIR = ROOT / "skills"
CLAUDE_COMMANDS_DIR = ROOT / ".claude" / "commands"
GEMINI_COMMANDS_DIR = ROOT / ".gemini" / "commands" / "fk"
AGENTS_MD = ROOT / "AGENTS.md"
GEMINI_MD = ROOT / "GEMINI.md"
STATE_FILE = ROOT / ".fk-setup.json"
def discover_skills():
"""Scan skills/fk:*.md and extract name + first-line description."""
if not SKILLS_DIR.exists():
print(f"ERROR: skills/ directory not found at {SKILLS_DIR}")
return []
skills = []
for path in sorted(SKILLS_DIR.glob("fk:*.md")):
name = path.stem[len("fk:"):] # strip "fk:" prefix
description = ""
try:
first_line = path.read_text(encoding="utf-8").splitlines()[0].strip()
# Strip leading markdown heading markers
description = first_line.lstrip("#").strip()
except (IndexError, OSError):
description = f"{name} skill"
skills.append({"name": name, "description": description, "path": str(path)})
return skills
def generate_claude(skills):
"""Create .claude/commands/fk:<name>.md stubs."""
CLAUDE_COMMANDS_DIR.mkdir(parents=True, exist_ok=True)
count = 0
for skill in skills:
name = skill["name"]
desc = skill["description"]
dest = CLAUDE_COMMANDS_DIR / f"fk:{name}.md"
content = (
f"{desc}\n"
f"<!-- AUTO-GENERATED by setup.py — do not edit. Source: skills/ -->\n"
f"Read and follow the instructions in skills/fk:{name}.md\n\n"
f"Arguments: $ARGUMENTS\n"
)
dest.write_text(content, encoding="utf-8")
count += 1
print(f"Generated {count} Claude commands in .claude/commands/")
return count
def generate_gemini(skills):
"""Create .gemini/commands/fk/<name>.toml for each skill."""
GEMINI_COMMANDS_DIR.mkdir(parents=True, exist_ok=True)
count = 0
for skill in skills:
name = skill["name"]
desc = skill["description"].replace('"', '\\"')
dest = GEMINI_COMMANDS_DIR / f"{name}.toml"
content = (
f'# AUTO-GENERATED by setup.py — do not edit. Source: skills/\n'
f'description = "{desc}"\n'
f'prompt = "Read the skill file at skills/fk:{name}.md and follow the instructions inside. Arguments: $ARGUMENTS"\n'
)
dest.write_text(content, encoding="utf-8")
count += 1
print(f"Generated {count} Gemini commands in .gemini/commands/fk/")
return count
## ── Inline content for GEMINI.md / AGENTS.md ──
# Keep in sync with CLAUDE.md rules. These are inlined so non-Claude AI tools
# see the critical rules directly without needing to follow a "read CLAUDE.md" pointer.
_CRITICAL_RULES = """\
## Critical Rules (MUST follow)
1. **Media ID is always UUID** — format `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`. Never use `CAMS...` / base64 strings.
2. **Scene prompts = ACTION only** — never describe character appearance. Reference images handle visual consistency via `imageInputs`.
3. **All reference images must exist before scene images** — verify every entity has `media_id` before generating scene images.
4. **No throwaway scripts** — NEVER write Python, shell, or any script file to loop over API requests. Use `POST /api/requests/batch` to submit all requests at once, then poll `GET /api/requests/batch-status`. The server throttles automatically.
5. **Locations use landscape, characters use portrait** — reference image orientation depends on entity type.
6. **UUID extraction** — if a response gives `CAMS...` instead of UUID, extract UUID from the `fifeUrl` in the response URL: `/image/{UUID}?...`.
7. **Cascade on regen** — regenerating an image auto-clears downstream video + upscale.
8. **REGENERATE vs GENERATE** — `GENERATE_*` skips if already COMPLETED. `REGENERATE_*` always runs (clears + regenerates).
9. **Image Material required** — every project needs a `material` field (e.g. `realistic`, `3d_pixar`, `anime`). List available: `GET /api/materials`.
10. **Server handles throttling** — worker enforces max 5 concurrent requests + 10s cooldown. Submit ALL requests via `/batch`; do NOT manually stagger or loop.
11. **Video prompts use sub-clip timing** — structure 8s video as time segments: `0-3s: [action]. 3-6s: [action]. 6-8s: [action].`
12. **Character dialogue in sub-clips** — embed speech in quotes: `Luna says "Goodnight."` Max 10-15 words per character per 2-3s segment.
13. **Scenes are mutable** — use `PATCH /api/scenes/{sid}` to update `prompt`, `video_prompt`, `narrator_text`, `character_names` after creation. Don't delete and recreate — patch instead.
"""
_PIPELINE_OVERVIEW = """\
## Pipeline Order
```
1. Health check GET /health → extension_connected: true
2. Create project POST /api/projects (with entities + material)
3. Create video POST /api/videos
4. Create scenes POST /api/scenes (with character_names, chain_type)
5. Gen ref images POST /api/requests/batch → poll /batch-status?project_id=<PID>
Wait for done=true, verify all entities have media_id
6. Gen scene images POST /api/requests/batch → poll /batch-status?video_id=<VID>
Wait for done=true, verify image_media_id = UUID
7. Gen videos POST /api/requests/batch → poll /batch-status?video_id=<VID>
Wait for done=true (videos take 2-5 min each)
8. (Optional) 4K POST /api/requests/batch (TIER_TWO only)
9. (Optional) TTS Create voice template → POST /api/videos/{vid}/narrate
10. Concat ffmpeg normalize + concat
```
"""
_BATCH_API = """\
## Batch API
Submit N requests at once (server throttles automatically — max 5 concurrent, 10s cooldown):
```bash
curl -X POST http://127.0.0.1:8100/api/requests/batch \\
-H "Content-Type: application/json" \\
-d '{"requests": [{"type": "...", "scene_id": "...", "project_id": "...", "video_id": "...", "orientation": "VERTICAL"}, ...]}'
```
Poll aggregate status:
```bash
curl -s "http://127.0.0.1:8100/api/requests/batch-status?video_id=<VID>&type=GENERATE_IMAGE"
# Returns: {"total": 40, "pending": 30, "processing": 5, "completed": 5, "failed": 0, "done": false}
# When "done": true → all requests have left the queue (completed or failed)
# When "all_succeeded": true → every request completed successfully
```
For full API reference, workflow recipes, and video prompt guidelines, see `CLAUDE.md`.
"""
def _agents_md_body(skills, title):
"""Build the shared AGENTS.md / GEMINI.md body with critical rules inlined."""
rows = "\n".join(
f"| `/fk:{s['name']}` | {s['description']} |" for s in skills
)
return (
f"<!-- AUTO-GENERATED by setup.py — do not edit. Source: skills/ -->\n"
f"# Flow Kit — {title}\n\n"
f"Base URL: `http://127.0.0.1:8100`\n\n"
f"## Pre-flight\n\n"
f"Before ANY workflow:\n"
f"```bash\n"
f"curl -s http://127.0.0.1:8100/health\n"
f"# Must return: {{\"extension_connected\": true}}\n"
f"```\n\n"
f"{_CRITICAL_RULES}\n"
f"{_PIPELINE_OVERVIEW}\n"
f"{_BATCH_API}\n"
f"## Skills\n\n"
f"This project has reusable skills in `skills/`. When the user says `/fk:<name>`, "
f"read `skills/fk:<name>.md` and follow the instructions inside.\n\n"
f"| Skill | Purpose |\n"
f"|-------|---------|\n"
f"{rows}\n"
)
def generate_codex(skills):
"""Generate AGENTS.md for Codex CLI."""
content = _agents_md_body(skills, "Codex CLI Instructions")
AGENTS_MD.write_text(content, encoding="utf-8")
print(f"Generated AGENTS.md ({len(skills)} skills)")
return len(skills)
def generate_gemini_md(skills):
"""Generate GEMINI.md for Gemini CLI."""
content = _agents_md_body(skills, "Gemini CLI Instructions")
GEMINI_MD.write_text(content, encoding="utf-8")
print(f"Generated GEMINI.md ({len(skills)} skills)")
return len(skills)
def save_state(tools, skill_count):
state = {
"tools": tools,
"last_sync": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"skill_count": skill_count,
}
STATE_FILE.write_text(json.dumps(state, indent=2), encoding="utf-8")
def load_state():
if not STATE_FILE.exists():
return None
try:
return json.loads(STATE_FILE.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return None
def run_generators(tools, skills):
"""Run the appropriate generators for the given tool list."""
generators = {
"claude": generate_claude,
"gemini": generate_gemini,
"codex": generate_codex,
"gemini_md": generate_gemini_md,
}
for tool in tools:
if tool == "gemini":
generators["gemini"](skills)
generators["gemini_md"](skills)
elif tool in generators:
generators[tool](skills)
def do_interactive(skills):
"""Interactive mode: prompt user to pick tools."""
print("\nFlow Kit — AI Tool Setup\n")
print("Which AI tools do you use with this project?")
print(" [1] Claude Code (.claude/commands/)")
print(" [2] Gemini CLI (.gemini/commands/fk/ + GEMINI.md)")
print(" [3] Codex CLI (AGENTS.md)")
print(" [4] All of the above")
print()
raw = input("Select (comma-separated, e.g. 1,2): ").strip()
if not raw:
print("No selection made. Exiting.")
return
mapping = {"1": "claude", "2": "gemini", "3": "codex", "4": "all"}
choices = [c.strip() for c in raw.split(",")]
tools = []
for c in choices:
if c not in mapping:
print(f"Invalid choice: {c}")
continue
val = mapping[c]
if val == "all":
tools = ["claude", "gemini", "codex"]
break
if val not in tools:
tools.append(val)
if not tools:
print("No valid tools selected. Exiting.")
return
run_generators(tools, skills)
save_state(tools, len(skills))
print(f"\nSetup complete. State saved to .fk-setup.json")
def do_tool(tool, skills):
"""Non-interactive: generate for a specific tool or all."""
if tool == "all":
tools = ["claude", "gemini", "codex"]
else:
tools = [tool]
run_generators(tools, skills)
save_state(tools, len(skills))
print(f"\nSetup complete. State saved to .fk-setup.json")
def do_sync(skills):
"""Re-sync all previously selected tools."""
state = load_state()
if not state:
print("No previous setup found. Run 'python setup.py' first.")
sys.exit(1)
tools = state.get("tools", [])
if not tools:
print("No tools recorded in .fk-setup.json.")
sys.exit(1)
print(f"Re-syncing for tools: {', '.join(tools)}")
run_generators(tools, skills)
save_state(tools, len(skills))
print(f"\nSync complete. {len(skills)} skills processed.")
def do_clean():
"""Remove all generated tool configs."""
removed = []
# Claude commands
if CLAUDE_COMMANDS_DIR.exists():
for f in CLAUDE_COMMANDS_DIR.glob("fk:*.md"):
f.unlink()
removed.append(str(f))
# Gemini commands dir
if GEMINI_COMMANDS_DIR.exists():
for f in GEMINI_COMMANDS_DIR.glob("*.toml"):
f.unlink()
removed.append(str(f))
try:
GEMINI_COMMANDS_DIR.rmdir()
except OSError:
pass # not empty, leave it
# AGENTS.md and GEMINI.md
for md in [AGENTS_MD, GEMINI_MD]:
if md.exists():
first_line = md.read_text(encoding="utf-8").splitlines()[0]
if "AUTO-GENERATED by setup.py" in first_line:
md.unlink()
removed.append(str(md))
# State file
if STATE_FILE.exists():
STATE_FILE.unlink()
removed.append(str(STATE_FILE))
if removed:
print(f"Removed {len(removed)} generated files.")
else:
print("Nothing to clean.")
def main():
parser = argparse.ArgumentParser(
description="Flow Kit — AI Tool Setup",
add_help=True,
)
parser.add_argument(
"subcommand",
nargs="?",
choices=["sync", "clean"],
help="sync: re-sync previously selected tools | clean: remove all generated configs",
)
parser.add_argument(
"--tool",
choices=["claude", "gemini", "codex", "all"],
help="Generate configs for a specific tool (non-interactive)",
)
args = parser.parse_args()
if args.subcommand == "clean":
do_clean()
return
# All other subcommands need skill discovery
skills = discover_skills()
if not skills and args.subcommand != "clean":
print("No skills found in skills/fk:*.md — nothing to generate.")
sys.exit(1)
if args.subcommand == "sync":
do_sync(skills)
elif args.tool:
do_tool(args.tool, skills)
else:
do_interactive(skills)
if __name__ == "__main__":
main()