@optima-chat/optima-agent 0.8.95 → 0.8.96

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 (41) hide show
  1. package/.claude/skills/gen/SKILL.md +1 -1
  2. package/.claude/skills/video-gen/SKILL.md +443 -0
  3. package/.claude/skills/video-gen/templates/lifestyle-scene.md +18 -0
  4. package/.claude/skills/video-gen/templates/pdp-360-showcase.md +17 -0
  5. package/.claude/skills/video-gen/templates/pdp-feature-highlight.md +18 -0
  6. package/.claude/skills/video-gen/templates/tiktok-before-after.md +17 -0
  7. package/.claude/skills/video-gen/templates/tiktok-product-reveal.md +17 -0
  8. package/.claude/skills/video-gen/templates/tiktok-unboxing.md +18 -0
  9. package/package.json +1 -1
  10. package/.claude/skills/video-clone/SKILL.md +0 -199
  11. package/.claude/skills/video-clone/assets/phase-state-template.json +0 -11
  12. package/.claude/skills/video-clone/references/ffmpeg-commands.md +0 -42
  13. package/.claude/skills/video-clone/references/gate-enforcement.md +0 -144
  14. package/.claude/skills/video-clone/references/kling-api.md +0 -85
  15. package/.claude/skills/video-clone/references/prompt-template.md +0 -71
  16. package/.claude/skills/video-clone/references/url-parsing.md +0 -32
  17. package/.claude/skills/video-clone/references/workflow-system.md +0 -92
  18. package/.claude/skills/video-clone/scripts/_confirm.py +0 -96
  19. package/.claude/skills/video-clone/scripts/_confirm_test.py +0 -125
  20. package/.claude/skills/video-clone/scripts/_gate.py +0 -162
  21. package/.claude/skills/video-clone/scripts/_gate_e2e_test.py +0 -226
  22. package/.claude/skills/video-clone/scripts/_gate_test.py +0 -148
  23. package/.claude/skills/video-clone/scripts/_project.py +0 -56
  24. package/.claude/skills/video-clone/scripts/analyze_source.py +0 -113
  25. package/.claude/skills/video-clone/scripts/analyze_source_test.py +0 -52
  26. package/.claude/skills/video-clone/scripts/assemble.py +0 -106
  27. package/.claude/skills/video-clone/scripts/confirm.py +0 -12
  28. package/.claude/skills/video-clone/scripts/edit_first_frame.py +0 -66
  29. package/.claude/skills/video-clone/scripts/extract_frames.py +0 -108
  30. package/.claude/skills/video-clone/scripts/gen_video.py +0 -59
  31. package/.claude/skills/video-clone/scripts/init_project.py +0 -103
  32. package/.claude/skills/video-clone/scripts/init_project_test.py +0 -106
  33. package/.claude/skills/video-clone/scripts/kling_generate.py +0 -262
  34. package/.claude/skills/video-clone/scripts/kling_generate_test.py +0 -191
  35. package/.claude/skills/video-clone/scripts/preflight.py +0 -102
  36. package/.claude/skills/video-clone/scripts/preview.py +0 -208
  37. package/.claude/skills/video-clone/scripts/preview_test.py +0 -169
  38. package/.claude/skills/video-clone/scripts/save_workflow.py +0 -129
  39. package/.claude/skills/video-clone/scripts/save_workflow_test.py +0 -106
  40. package/.claude/skills/video-clone/scripts/status.py +0 -202
  41. package/.claude/skills/video-clone/scripts/status_test.py +0 -174
@@ -1,59 +0,0 @@
1
- """Phase 3 (no-audio path): generate video via `gen video` (Wan 2.6 I2V).
2
-
3
- Usage:
4
- python scripts/gen_video.py --project <name> --frame <path> \
5
- [--duration 10]
6
-
7
- Use this instead of kling_generate.py when the video does NOT need audio /
8
- lip sync (cost ~$0.02 vs ~$1). Prompt is read from <project>/prompt.md.
9
-
10
- Gate: requires preview_confirmed.
11
- """
12
- from __future__ import annotations
13
-
14
- import argparse
15
- import subprocess
16
- import sys
17
- from pathlib import Path
18
-
19
- from _gate import require_gate
20
- from _project import resolve_project, next_version, append_log
21
-
22
-
23
- def main() -> int:
24
- ap = argparse.ArgumentParser()
25
- ap.add_argument("--project", required=True)
26
- ap.add_argument("--frame", required=True)
27
- ap.add_argument("--duration", type=int, default=10, choices=[5, 10])
28
- args = ap.parse_args()
29
-
30
- project_dir = resolve_project(args.project)
31
- require_gate(project_dir, "preview_confirmed")
32
-
33
- frame = Path(args.frame)
34
- if not frame.is_file():
35
- print(f"ERROR: frame not found at {frame}", file=sys.stderr)
36
- return 1
37
-
38
- prompt_path = project_dir / "prompt.md"
39
- if not prompt_path.is_file():
40
- print(f"ERROR: {prompt_path} missing. Write prompt.md first.", file=sys.stderr)
41
- return 1
42
- prompt = prompt_path.read_text(encoding="utf-8").strip()
43
-
44
- out = next_version(project_dir / "videos", f"video_{args.duration}s_", ".mp4")
45
- cmd = ["gen", "video", "-i", str(frame), "-o", str(out),
46
- "-p", prompt, "--duration", str(args.duration)]
47
- print(f"Running: {' '.join(cmd[:4])} … (prompt trimmed)")
48
- r = subprocess.run(cmd)
49
- if r.returncode != 0:
50
- print(f"ERROR: gen video failed with exit {r.returncode}", file=sys.stderr)
51
- return r.returncode
52
-
53
- print(f"\nDone: {out}")
54
- append_log(project_dir, f"video_generated_silent → {out.name}")
55
- return 0
56
-
57
-
58
- if __name__ == "__main__":
59
- sys.exit(main())
@@ -1,103 +0,0 @@
1
- """Initialize a new video-clone project directory + phase state.
2
-
3
- Usage:
4
- python scripts/init_project.py --name <project-name> --task-type video_clone
5
- python scripts/init_project.py --name <project-name> --task-type video_gen
6
-
7
- Creates:
8
- <gen-output>/video-clone/<name>/
9
- source/
10
- frames/
11
- videos/
12
- .state/phase.json (copied from assets/phase-state-template.json)
13
-
14
- By default <gen-output> is `./gen-output`. Override with GEN_OUTPUT_ROOT env.
15
-
16
- Refuses to overwrite an existing project directory — pick a new name or
17
- bump a version suffix (e.g. handheld-phone-swap-v2).
18
- """
19
- from __future__ import annotations
20
-
21
- import argparse
22
- import json
23
- import os
24
- import sys
25
- from datetime import datetime, timezone
26
- from pathlib import Path
27
-
28
- VALID_TASK_TYPES = ("video_clone", "video_gen")
29
-
30
- SKILL_ROOT = Path(__file__).parent.parent
31
- TEMPLATE = SKILL_ROOT / "assets" / "phase-state-template.json"
32
-
33
-
34
- def _gen_output_root() -> Path:
35
- override = os.environ.get("GEN_OUTPUT_ROOT")
36
- if override:
37
- return Path(override)
38
- return Path.cwd() / "gen-output"
39
-
40
-
41
- def _now_iso() -> str:
42
- return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
43
-
44
-
45
- def main() -> int:
46
- ap = argparse.ArgumentParser(description="Initialize a video-clone project.")
47
- ap.add_argument("--name", required=True, help="Project name (slug).")
48
- ap.add_argument(
49
- "--task-type",
50
- required=True,
51
- choices=VALID_TASK_TYPES,
52
- help="video_clone or video_gen",
53
- )
54
- args = ap.parse_args()
55
-
56
- if not TEMPLATE.is_file():
57
- print(f"ERROR: state template missing at {TEMPLATE}", file=sys.stderr)
58
- return 1
59
-
60
- base = _gen_output_root() / "video-clone" / args.name
61
- if base.exists():
62
- print(
63
- f"ERROR: project already exists at {base}\n"
64
- f"Pick a new name or bump a version suffix (e.g. {args.name}-v2).",
65
- file=sys.stderr,
66
- )
67
- return 1
68
-
69
- for sub in ("source", "frames", "videos", ".state"):
70
- (base / sub).mkdir(parents=True)
71
-
72
- state = json.loads(TEMPLATE.read_text(encoding="utf-8"))
73
- state["project"] = args.name
74
- state["task_type"] = args.task_type
75
- state["created_at"] = _now_iso()
76
- state["history"].append({"event": "project_created", "at": state["created_at"]})
77
-
78
- (base / ".state" / "phase.json").write_text(
79
- json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8"
80
- )
81
-
82
- print(f"Created project: {base}")
83
- print(f" phase state: {base / '.state' / 'phase.json'}")
84
- print()
85
- print("Next steps:")
86
- print(" 1. (Phase 0 sanity check) Tell the user one sentence describing your")
87
- print(" understanding of the task and reference workflow. Wait for 'yes'.")
88
- print(" 2. Run prep autonomously: analyze_source → extract_frames → write")
89
- print(" prompt.md → edit_first_frame → write cost.json → preview.py")
90
- print(f" 3. Show preview to user, then run:")
91
- print(
92
- f" python scripts/confirm.py --project {args.name} "
93
- f"--quote \"<user's actual confirmation>\""
94
- )
95
- print(f" 4. Run kling_generate.py / gen_video.py → assemble.py")
96
- print()
97
- print(f"At any time, check status:")
98
- print(f" python scripts/status.py --project {args.name}")
99
- return 0
100
-
101
-
102
- if __name__ == "__main__":
103
- sys.exit(main())
@@ -1,106 +0,0 @@
1
- """Tests for init_project.py.
2
-
3
- Uses GEN_OUTPUT_ROOT env override to isolate the tests from the real
4
- gen-output/ directory.
5
-
6
- Run: python scripts/init_project_test.py
7
- """
8
- import json
9
- import os
10
- import shutil
11
- import subprocess
12
- import sys
13
- import tempfile
14
- from pathlib import Path
15
-
16
- SCRIPTS = Path(__file__).parent
17
- INIT = SCRIPTS / "init_project.py"
18
-
19
-
20
- def _run(root: Path, *args: str) -> subprocess.CompletedProcess:
21
- env = os.environ.copy()
22
- env["GEN_OUTPUT_ROOT"] = str(root)
23
- return subprocess.run(
24
- [sys.executable, str(INIT), *args],
25
- capture_output=True, text=True, env=env,
26
- )
27
-
28
-
29
- def test_creates_full_structure():
30
- with tempfile.TemporaryDirectory() as tmp:
31
- root = Path(tmp)
32
- r = _run(root, "--name", "myproj", "--task-type", "video_clone")
33
- assert r.returncode == 0, f"init failed: {r.stderr}"
34
- base = root / "video-clone" / "myproj"
35
- assert (base / "source").is_dir()
36
- assert (base / "frames").is_dir()
37
- assert (base / "videos").is_dir()
38
- state_file = base / ".state" / "phase.json"
39
- assert state_file.is_file()
40
- state = json.loads(state_file.read_text(encoding="utf-8"))
41
- assert state["project"] == "myproj"
42
- assert state["task_type"] == "video_clone"
43
- assert state["created_at"] is not None
44
- # Single-gate model: only preview_confirmed, no plan/prompt/frame gates
45
- assert "preview_confirmed" in state["gates"], (
46
- f"Expected single-gate schema with 'preview_confirmed', got: {list(state['gates'])}"
47
- )
48
- assert state["gates"]["preview_confirmed"]["status"] is False
49
- # Old 3-gate keys must NOT exist
50
- for old_gate in ("plan_confirmed", "prompt_confirmed", "frame_confirmed"):
51
- assert old_gate not in state["gates"], (
52
- f"Old gate '{old_gate}' still present in template"
53
- )
54
-
55
-
56
- def test_creates_video_gen_project():
57
- with tempfile.TemporaryDirectory() as tmp:
58
- root = Path(tmp)
59
- r = _run(root, "--name", "genproj", "--task-type", "video_gen")
60
- assert r.returncode == 0, f"init failed: {r.stderr}"
61
- state_file = root / "video-clone" / "genproj" / ".state" / "phase.json"
62
- state = json.loads(state_file.read_text(encoding="utf-8"))
63
- assert state["task_type"] == "video_gen"
64
- assert "preview_confirmed" in state["gates"]
65
-
66
-
67
- def test_rejects_duplicate():
68
- with tempfile.TemporaryDirectory() as tmp:
69
- root = Path(tmp)
70
- _run(root, "--name", "dup", "--task-type", "video_clone")
71
- r = _run(root, "--name", "dup", "--task-type", "video_clone")
72
- assert r.returncode == 1, (
73
- f"expected exit 1 on duplicate, got {r.returncode}\n"
74
- f"stdout: {r.stdout}\nstderr: {r.stderr}"
75
- )
76
-
77
-
78
- def test_rejects_unknown_task_type():
79
- with tempfile.TemporaryDirectory() as tmp:
80
- r = _run(Path(tmp), "--name", "x", "--task-type", "nonsense")
81
- assert r.returncode != 0
82
-
83
-
84
- TESTS = [
85
- ("creates full structure", test_creates_full_structure),
86
- ("creates video_gen project", test_creates_video_gen_project),
87
- ("rejects duplicate", test_rejects_duplicate),
88
- ("rejects unknown task type", test_rejects_unknown_task_type),
89
- ]
90
-
91
-
92
- def main() -> int:
93
- failed = 0
94
- for name, fn in TESTS:
95
- try:
96
- fn()
97
- print(f"PASS {name}")
98
- except Exception as e:
99
- failed += 1
100
- print(f"FAIL {name}: {type(e).__name__}: {e}")
101
- print(f"\n{len(TESTS) - failed}/{len(TESTS)} passed")
102
- return 0 if failed == 0 else 1
103
-
104
-
105
- if __name__ == "__main__":
106
- sys.exit(main())
@@ -1,262 +0,0 @@
1
- """Phase 3: generate video with audio via the Optima generation backend.
2
-
3
- The generation backend routes to Kling 3.0 (PiAPI) when provider="piapi".
4
- Billing is automatic — the server's billing middleware deducts credits from
5
- the user's Optima wallet at submit time and refunds on task failure.
6
-
7
- Usage:
8
- python scripts/kling_generate.py --project <name> --frame <path> \
9
- [--duration 10] [--aspect-ratio 9:16] [--mode std] \
10
- [--cfg-scale 0.5] [--no-audio]
11
-
12
- Auth & API URL discovery (matches @optima-chat/gen-cli convention):
13
- Token:
14
- 1. OPTIMA_TOKEN env var
15
- 2. ~/.optima/token.json ("access_token" field)
16
- Generation API URL:
17
- 1. GENERATION_API_URL env var
18
- 2. ~/.optima/token.json ("env" field) → ci/stage/prod mapping
19
- 3. default: prod (https://gen-api.optima.onl)
20
-
21
- A logged-in user (`optima login`) has both automatically; no server-side
22
- env injection is required.
23
-
24
- Pipeline:
25
- 1. Gate: requires preview_confirmed.
26
- 2. POST <GEN_URL>/api/video/generate with provider="piapi" + base64 frame.
27
- Server-side billing middleware pre-deducts credits here.
28
- 3. Poll GET <GEN_URL>/api/task/{id} every 10s until completed/failed.
29
- 4. Download result_url to <project>/videos/ (3-attempt retry).
30
-
31
- Reads prompt from <project>/prompt.md. Writes output with a versioned filename.
32
- """
33
- from __future__ import annotations
34
-
35
- import argparse
36
- import base64
37
- import json
38
- import os
39
- import sys
40
- import time
41
- from pathlib import Path
42
-
43
- from _gate import require_gate
44
- from _project import resolve_project, next_version, append_log
45
-
46
- try:
47
- import requests # type: ignore
48
- except ImportError: # pragma: no cover
49
- requests = None
50
-
51
- POLL_INTERVAL_S = 10
52
- POLL_TIMEOUT_S = 15 * 60 # 15 min (server side uses 10 min, leave 5 min headroom)
53
- DOWNLOAD_RETRIES = 3
54
- DOWNLOAD_BACKOFF_S = 5
55
-
56
- TOKEN_FILE = Path.home() / ".optima" / "token.json"
57
- API_URLS = {
58
- "prod": "https://gen-api.optima.onl",
59
- "stage": "https://gen-api.stage.optima.onl",
60
- }
61
-
62
-
63
- def _load_token_file() -> dict | None:
64
- """Read ~/.optima/token.json if present. Returns parsed dict or None."""
65
- if not TOKEN_FILE.is_file():
66
- return None
67
- try:
68
- data = json.loads(TOKEN_FILE.read_text(encoding="utf-8"))
69
- except (OSError, json.JSONDecodeError):
70
- return None
71
- if not data.get("access_token"):
72
- return None
73
- return data
74
-
75
-
76
- def _get_token() -> str:
77
- """Resolve the Optima auth token.
78
-
79
- Order matches @optima-chat/gen-cli: env var > token.json. Fail fast with
80
- a login hint if neither is available.
81
- """
82
- env_token = os.environ.get("OPTIMA_TOKEN", "").strip()
83
- if env_token:
84
- return env_token
85
- data = _load_token_file()
86
- if data:
87
- return data["access_token"]
88
- print(
89
- "ERROR: Optima token not found. Set OPTIMA_TOKEN or run `optima login`.",
90
- file=sys.stderr,
91
- )
92
- sys.exit(1)
93
-
94
-
95
- def _get_gen_api_url() -> str:
96
- """Resolve the generation service base URL.
97
-
98
- Order matches @optima-chat/gen-cli: GENERATION_API_URL > token.json env
99
- field > prod default.
100
- """
101
- override = os.environ.get("GENERATION_API_URL", "").strip()
102
- if override:
103
- return override.rstrip("/")
104
- data = _load_token_file()
105
- env = (data or {}).get("env") or "prod"
106
- return API_URLS.get(env, API_URLS["prod"])
107
-
108
-
109
- def _auth_header(token: str) -> str:
110
- """Accept either 'Bearer xxx' or a raw token; normalize to Bearer form."""
111
- return token if token.lower().startswith("bearer ") else f"Bearer {token}"
112
-
113
-
114
- def _submit(gen_url: str, auth: str, prompt: str, frame_b64: str,
115
- duration: int, aspect: str, mode: str,
116
- cfg: float, audio: bool) -> str:
117
- """POST /api/video/generate with provider='piapi'. Returns backend task_id.
118
-
119
- On billing failure the server returns 402 (INSUFFICIENT_CREDITS) or
120
- 403 (PLAN_RESTRICTED); both are surfaced as actionable errors.
121
- """
122
- payload = {
123
- "provider": "piapi",
124
- "image": frame_b64,
125
- "prompt": prompt,
126
- "duration": duration,
127
- "aspect_ratio": aspect,
128
- "mode": mode,
129
- "cfg_scale": float(cfg),
130
- "enable_audio": audio,
131
- }
132
- r = requests.post(
133
- f"{gen_url}/api/video/generate",
134
- headers={"Authorization": auth, "Content-Type": "application/json"},
135
- json=payload, timeout=60,
136
- )
137
- if r.status_code == 402:
138
- raise RuntimeError(f"INSUFFICIENT_CREDITS: {r.json().get('error', {}).get('message', 'not enough credits')}")
139
- if r.status_code == 403:
140
- raise RuntimeError(f"PLAN_RESTRICTED: {r.json().get('error', {}).get('message', 'plan does not include video generation')}")
141
- r.raise_for_status()
142
- return r.json()["task_id"]
143
-
144
-
145
- def _poll(gen_url: str, auth: str, task_id: str) -> tuple[str, dict]:
146
- """Poll /api/task/{id} until completed or failed. Returns (result_url, result_meta).
147
-
148
- On client-side timeout the task may still complete server-side (credits
149
- already deducted). The task_id is included in the timeout message so the
150
- caller can re-check later via GET /api/task/<id>.
151
- """
152
- start = time.monotonic()
153
- while True:
154
- if time.monotonic() - start > POLL_TIMEOUT_S:
155
- raise RuntimeError(
156
- f"polling timed out after {POLL_TIMEOUT_S}s (task_id={task_id}). "
157
- f"Task may still complete server-side — query "
158
- f"GET /api/task/{task_id} to check status and retrieve the result."
159
- )
160
- r = requests.get(
161
- f"{gen_url}/api/task/{task_id}",
162
- headers={"Authorization": auth}, timeout=60,
163
- )
164
- r.raise_for_status()
165
- d = r.json()
166
- status = d.get("status", "")
167
- if status == "completed":
168
- url = d.get("result_url")
169
- if not url:
170
- raise RuntimeError(
171
- f"task {task_id} completed but result_url missing from response"
172
- )
173
- return url, d.get("result_meta") or {}
174
- if status == "failed":
175
- raise RuntimeError(d.get("error_message") or f"task {task_id} failed without error message")
176
- print(f" status: {status} — polling again in {POLL_INTERVAL_S}s")
177
- time.sleep(POLL_INTERVAL_S)
178
-
179
-
180
- def _download(url: str, out: Path) -> None:
181
- last_err = None
182
- for attempt in range(DOWNLOAD_RETRIES):
183
- try:
184
- r = requests.get(url, timeout=300)
185
- r.raise_for_status()
186
- out.write_bytes(r.content)
187
- return
188
- except Exception as e:
189
- last_err = e
190
- print(f" download attempt {attempt + 1}/{DOWNLOAD_RETRIES} failed: {e}")
191
- time.sleep(DOWNLOAD_BACKOFF_S)
192
- raise RuntimeError(f"download failed after {DOWNLOAD_RETRIES} attempts: {last_err}")
193
-
194
-
195
- def main() -> int:
196
- ap = argparse.ArgumentParser()
197
- ap.add_argument("--project", required=True)
198
- ap.add_argument("--frame", required=True, help="confirmed first frame")
199
- ap.add_argument("--duration", type=int, default=10, choices=[5, 10])
200
- ap.add_argument("--aspect-ratio", default="9:16")
201
- ap.add_argument("--mode", default="std", choices=["std", "pro"])
202
- ap.add_argument("--cfg-scale", type=float, default=0.5)
203
- ap.add_argument("--no-audio", action="store_true")
204
- args = ap.parse_args()
205
-
206
- project_dir = resolve_project(args.project)
207
- require_gate(project_dir, "preview_confirmed")
208
-
209
- if requests is None:
210
- print("ERROR: the 'requests' package is required. Install: pip install requests",
211
- file=sys.stderr)
212
- return 1
213
-
214
- gen_url = _get_gen_api_url()
215
- auth = _auth_header(_get_token())
216
-
217
- frame = Path(args.frame)
218
- if not frame.is_file():
219
- print(f"ERROR: frame not found at {frame}", file=sys.stderr)
220
- return 1
221
-
222
- prompt_path = project_dir / "prompt.md"
223
- if not prompt_path.is_file():
224
- print(f"ERROR: {prompt_path} missing. Write prompt.md first.", file=sys.stderr)
225
- return 1
226
- prompt = prompt_path.read_text(encoding="utf-8").strip()
227
-
228
- frame_b64 = base64.b64encode(frame.read_bytes()).decode()
229
-
230
- print("Submitting video generation task …")
231
- try:
232
- task_id = _submit(
233
- gen_url, auth, prompt, frame_b64,
234
- duration=args.duration, aspect=args.aspect_ratio, mode=args.mode,
235
- cfg=args.cfg_scale, audio=not args.no_audio,
236
- )
237
- except RuntimeError as e:
238
- print(f"ERROR: {e}", file=sys.stderr)
239
- return 1
240
- print(f" task_id: {task_id}")
241
-
242
- print("Polling for completion …")
243
- try:
244
- result_url, result_meta = _poll(gen_url, auth, task_id)
245
- except RuntimeError as e:
246
- print(f"ERROR: {e}", file=sys.stderr)
247
- return 1
248
- print(f" result_url: {result_url}")
249
- if result_meta:
250
- print(f" result_meta: {result_meta}")
251
-
252
- out = next_version(project_dir / "videos", f"video_{args.duration}s_", ".mp4")
253
- print(f"Downloading → {out}")
254
- _download(result_url, out)
255
-
256
- print(f"\nDone: {out}")
257
- append_log(project_dir, f"video_generated_with_audio → {out.name} (task {task_id})")
258
- return 0
259
-
260
-
261
- if __name__ == "__main__":
262
- sys.exit(main())