arkaos 4.31.0 → 4.32.0

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 (43) hide show
  1. package/README.md +1 -1
  2. package/THE-ARKAOS-GUIDE.md +1 -1
  3. package/VERSION +1 -1
  4. package/arka/SKILL.md +1 -1
  5. package/config/claude-agents/brand-director.md +1 -1
  6. package/config/claude-agents/content-strategist.md +1 -1
  7. package/config/claude-agents/frontend-dev.md +1 -1
  8. package/config/claude-agents/marketing-director.md +1 -1
  9. package/config/claude-agents/scriptwriter.md +1 -1
  10. package/config/claude-agents/trends-analyst.md +1 -1
  11. package/config/claude-agents/video-producer.md +1 -1
  12. package/config/skills-curated.yaml +2 -1
  13. package/config/skills-provenance.yaml +6 -0
  14. package/core/keys.py +17 -5
  15. package/departments/brand/agents/brand-director.yaml +1 -0
  16. package/departments/content/agents/content-strategist.yaml +1 -0
  17. package/departments/content/agents/production/trends-analyst.yaml +1 -0
  18. package/departments/content/agents/production/video-producer.yaml +1 -0
  19. package/departments/content/agents/scriptwriter.yaml +1 -0
  20. package/departments/dev/agents/frontend-dev.yaml +1 -0
  21. package/departments/dev/skills/animated-website/SKILL.md +14 -1
  22. package/departments/dev/skills/watch/SKILL.md +162 -0
  23. package/departments/dev/skills/watch/references/claude-video.LICENSE +21 -0
  24. package/departments/dev/skills/watch/scripts/config.py +145 -0
  25. package/departments/dev/skills/watch/scripts/download.py +179 -0
  26. package/departments/dev/skills/watch/scripts/frames.py +762 -0
  27. package/departments/dev/skills/watch/scripts/setup.py +373 -0
  28. package/departments/dev/skills/watch/scripts/transcribe.py +95 -0
  29. package/departments/dev/skills/watch/scripts/watch.py +531 -0
  30. package/departments/dev/skills/watch/scripts/whisper.py +466 -0
  31. package/departments/marketing/agents/marketing-director.yaml +1 -0
  32. package/departments/marketing/skills/ad-creative/SKILL.md +5 -0
  33. package/harness/codex/AGENTS.md +1 -1
  34. package/harness/copilot/copilot-instructions.md +1 -1
  35. package/harness/cursor/rules/arkaos.mdc +2 -2
  36. package/harness/gemini/GEMINI.md +1 -1
  37. package/harness/opencode/AGENTS.md +1 -1
  38. package/harness/zed/.rules +1 -1
  39. package/installer/doctor.js +38 -4
  40. package/knowledge/agents-registry-v2.json +8 -1
  41. package/knowledge/skills-manifest.json +14 -1
  42. package/package.json +1 -1
  43. package/pyproject.toml +1 -1
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env python3
2
+ """Setup / preflight for /watch.
3
+
4
+ Modes:
5
+ setup.py --check Silent preflight. Exit 0 if ready, 2/3/4 on failure.
6
+ setup.py --json Machine-readable status for Claude to parse.
7
+ setup.py Installer. Auto-installs deps, scaffolds .env, marks SETUP_COMPLETE.
8
+
9
+ Design:
10
+ - Silent on success: --check exits 0 with no output when everything's ready so
11
+ that /watch doesn't spam "setup is complete" on every turn.
12
+ - Idempotent: re-running the installer is safe — it never clobbers existing
13
+ keys and only appends missing ones.
14
+ - SETUP_COMPLETE=true in ~/.arkaos/watch.env tells us the user has been
15
+ through a successful installer run at least once.
16
+ - Never sudo. On macOS, auto-install via brew. Elsewhere, print exact commands.
17
+ - Never write an API key to disk automatically — keys belong in `/arka keys`
18
+ (~/.arkaos/keys.json); watch.env only carries placeholders as a fallback.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import contextlib
23
+ import json
24
+ import platform
25
+ import shutil
26
+ import subprocess
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ SCRIPT_DIR = Path(__file__).resolve().parent
31
+ if str(SCRIPT_DIR) not in sys.path:
32
+ sys.path.insert(0, str(SCRIPT_DIR))
33
+ from config import ( # noqa: E402
34
+ config_file,
35
+ get_config,
36
+ read_env_file,
37
+ resolve_api_key,
38
+ )
39
+
40
+ REQUIRED_BINARIES = ["ffmpeg", "ffprobe", "yt-dlp"]
41
+ ENV_TEMPLATE = """# ArkaOS /watch configuration
42
+ #
43
+ # Whisper transcription fallback — used only when yt-dlp cannot get captions
44
+ # (or when you point /watch at a local file with no subtitles).
45
+ #
46
+ # PREFERRED: store keys with `/arka keys` (~/.arkaos/keys.json) — this file
47
+ # is only a fallback. Groq runs whisper-large-v3 cheaper and faster; OpenAI
48
+ # whisper-1 is the compatible alternative.
49
+ #
50
+ # Get a Groq key: https://console.groq.com/keys
51
+ # Get an OpenAI key: https://platform.openai.com/api-keys
52
+ #
53
+ # With no key anywhere, /watch still works, but videos without native
54
+ # captions come back frames-only.
55
+
56
+ GROQ_API_KEY=
57
+ OPENAI_API_KEY=
58
+
59
+ # Default watch behavior (the /watch first-run wizard sets this for you).
60
+ # Allowed values: transcript | efficient | balanced | token-burner
61
+ # Keep the value on its own line with no trailing comment.
62
+ # WATCH_DETAIL=balanced
63
+ """
64
+
65
+
66
+ def _which(name: str) -> str | None:
67
+ return shutil.which(name)
68
+
69
+
70
+ def _binary_runs(name: str) -> bool:
71
+ """A binary on PATH can still be a corpse — e.g. a dangling homebrew
72
+ dylib makes ffmpeg abort at load (dyld error, exit 134). Probe
73
+ `-version` so a broken install is reported as missing instead of
74
+ crashing mid-run with no setup hint."""
75
+ try:
76
+ result = subprocess.run([name, "-version"], capture_output=True, timeout=10)
77
+ except (OSError, subprocess.SubprocessError):
78
+ return False
79
+ return result.returncode == 0
80
+
81
+
82
+ def _check_binaries() -> list[str]:
83
+ missing: list[str] = []
84
+ for b in REQUIRED_BINARIES:
85
+ if not _which(b):
86
+ missing.append(b)
87
+ elif b in ("ffmpeg", "ffprobe") and not _binary_runs(b):
88
+ # Only probe the native binaries: they are the dyld-breakage
89
+ # candidates and probe in ~20ms. yt-dlp is a Python zipapp —
90
+ # probing it costs interpreter startup and breaks the <100ms
91
+ # --check budget.
92
+ missing.append(b)
93
+ return missing
94
+
95
+
96
+ _PERM_WARNED: set[str] = set()
97
+
98
+
99
+ def _check_file_permissions(path: Path) -> None:
100
+ """Warn to stderr (once per path per process) if a secrets file is
101
+ world/group readable."""
102
+ key = str(path)
103
+ if key in _PERM_WARNED:
104
+ return
105
+ try:
106
+ mode = path.stat().st_mode
107
+ if mode & 0o044:
108
+ _PERM_WARNED.add(key)
109
+ sys.stderr.write(
110
+ f"[watch] WARNING: {path} is readable by other users. "
111
+ f"Run: chmod 600 {path}\n"
112
+ )
113
+ sys.stderr.flush()
114
+ except OSError:
115
+ pass
116
+
117
+
118
+ def _have_api_key() -> tuple[bool, str | None]:
119
+ cfg = config_file()
120
+ if cfg.exists():
121
+ _check_file_permissions(cfg)
122
+ if resolve_api_key("GROQ_API_KEY"):
123
+ return True, "groq"
124
+ if resolve_api_key("OPENAI_API_KEY"):
125
+ return True, "openai"
126
+ return False, None
127
+
128
+
129
+ def is_first_run() -> bool:
130
+ """True if the installer hasn't completed successfully yet."""
131
+ return read_env_file().get("SETUP_COMPLETE") != "true"
132
+
133
+
134
+ def _scaffold_env() -> bool:
135
+ """Create ~/.arkaos/watch.env with placeholders if missing."""
136
+ cfg = config_file()
137
+ if cfg.exists():
138
+ return False
139
+ cfg.parent.mkdir(parents=True, exist_ok=True)
140
+ cfg.write_text(ENV_TEMPLATE, encoding="utf-8")
141
+ with contextlib.suppress(OSError):
142
+ cfg.chmod(0o600)
143
+ return True
144
+
145
+
146
+ def _write_setup_complete() -> None:
147
+ """Idempotently append SETUP_COMPLETE=true to watch.env.
148
+
149
+ Used only after a fully successful install (deps + key). Future sessions
150
+ detect this marker to skip wizard-style UI and stay silent.
151
+ """
152
+ cfg = config_file()
153
+ cfg.parent.mkdir(parents=True, exist_ok=True)
154
+ if cfg.exists():
155
+ existing = cfg.read_text(encoding="utf-8")
156
+ for line in existing.splitlines():
157
+ if line.strip().startswith("SETUP_COMPLETE="):
158
+ return
159
+ if existing and not existing.endswith("\n"):
160
+ existing += "\n"
161
+ cfg.write_text(existing + "SETUP_COMPLETE=true\n", encoding="utf-8")
162
+ else:
163
+ cfg.write_text(ENV_TEMPLATE + "\nSETUP_COMPLETE=true\n", encoding="utf-8")
164
+ with contextlib.suppress(OSError):
165
+ cfg.chmod(0o600)
166
+
167
+
168
+ def _brew_pkg(missing: list[str]) -> list[str]:
169
+ pkgs: list[str] = []
170
+ for bin_name in missing:
171
+ if bin_name in ("ffmpeg", "ffprobe"):
172
+ if "ffmpeg" not in pkgs:
173
+ pkgs.append("ffmpeg")
174
+ elif bin_name == "yt-dlp":
175
+ if "yt-dlp" not in pkgs:
176
+ pkgs.append("yt-dlp")
177
+ else:
178
+ pkgs.append(bin_name)
179
+ return pkgs
180
+
181
+
182
+ def _install_macos(missing: list[str]) -> tuple[bool, str]:
183
+ if _which("brew") is None:
184
+ return False, (
185
+ "Homebrew is not installed. Install it from https://brew.sh, then re-run setup. "
186
+ "Or install manually: `brew install " + " ".join(_brew_pkg(missing)) + "`"
187
+ )
188
+ pkgs = _brew_pkg(missing)
189
+ if not pkgs:
190
+ return True, "nothing to install"
191
+ cmd = ["brew", "install", *pkgs]
192
+ print(f"[setup] running: {' '.join(cmd)}", file=sys.stderr)
193
+ result = subprocess.run(cmd)
194
+ if result.returncode != 0:
195
+ return False, f"brew install failed with exit code {result.returncode}"
196
+ return True, f"installed via brew: {', '.join(pkgs)}"
197
+
198
+
199
+ def _install_hint_linux(missing: list[str]) -> str:
200
+ pkgs = _brew_pkg(missing)
201
+ hints = []
202
+ if "ffmpeg" in pkgs:
203
+ hints.append("apt: `sudo apt install ffmpeg` or dnf: `sudo dnf install ffmpeg`")
204
+ if "yt-dlp" in pkgs:
205
+ hints.append("`pipx install yt-dlp` (recommended) or `pip install --user yt-dlp`")
206
+ return "\n ".join(hints) if hints else "nothing to install"
207
+
208
+
209
+ def _install_hint_windows(missing: list[str]) -> str:
210
+ pkgs = _brew_pkg(missing)
211
+ hints = []
212
+ if "ffmpeg" in pkgs:
213
+ hints.append("winget: `winget install Gyan.FFmpeg`")
214
+ if "yt-dlp" in pkgs:
215
+ hints.append("winget: `winget install yt-dlp.yt-dlp` or pip: `pip install --user yt-dlp`")
216
+ return "\n ".join(hints) if hints else "nothing to install"
217
+
218
+
219
+ def _status() -> dict:
220
+ """Structured preflight snapshot.
221
+
222
+ `status` describes the *ideal* state (a Whisper key is encouraged), so a
223
+ keyless install still reports `needs_key` on the very first run — that's
224
+ the agent's cue to encourage adding one.
225
+
226
+ `can_proceed` is the operational gate: /watch can run as long as the
227
+ binaries are present AND the user has either set a key or already finished
228
+ setup (consciously opting out of Whisper). A keyless user who completed
229
+ setup is NOT nagged on every call.
230
+ """
231
+ missing = _check_binaries()
232
+ has_key, backend = _have_api_key()
233
+ setup_complete = not is_first_run()
234
+
235
+ if not missing and has_key:
236
+ status = "ready"
237
+ elif missing and not has_key:
238
+ status = "needs_install_and_key"
239
+ elif missing:
240
+ status = "needs_install"
241
+ else:
242
+ status = "needs_key"
243
+
244
+ can_proceed = (not missing) and (has_key or setup_complete)
245
+
246
+ cfg = get_config()
247
+ return {
248
+ "status": status,
249
+ "can_proceed": can_proceed,
250
+ "first_run": not setup_complete,
251
+ "setup_complete": setup_complete,
252
+ "missing_binaries": missing,
253
+ "whisper_backend": backend,
254
+ "has_api_key": has_key,
255
+ "config_file": str(config_file()),
256
+ "watch_detail": cfg["detail"],
257
+ "platform": platform.system(),
258
+ }
259
+
260
+
261
+ def cmd_check() -> int:
262
+ """Silent-on-success preflight.
263
+
264
+ Exit 0 with no output when /watch can run. A keyless user who already
265
+ finished setup (SETUP_COMPLETE=true) counts as ready — Whisper is
266
+ encouraged, not required — so they are never nagged on follow-up calls.
267
+
268
+ On a state that blocks /watch, print one actionable line to stderr:
269
+ 2 → binaries missing
270
+ 3 → genuine first run with no API key (encourage one)
271
+ 4 → both missing
272
+ """
273
+ s = _status()
274
+ if s["can_proceed"]:
275
+ return 0
276
+
277
+ parts = []
278
+ if s["missing_binaries"]:
279
+ parts.append(f"missing binaries: {', '.join(s['missing_binaries'])}")
280
+ if not s["has_api_key"] and not s["setup_complete"]:
281
+ parts.append("no Whisper API key (GROQ_API_KEY or OPENAI_API_KEY)")
282
+ installer = Path(__file__).resolve()
283
+ sys.stderr.write(
284
+ f"[watch] setup incomplete ({'; '.join(parts)}). "
285
+ f"Run: python3 {installer}\n"
286
+ )
287
+ sys.stderr.flush()
288
+
289
+ if s["missing_binaries"] and not s["has_api_key"]:
290
+ return 4
291
+ if s["missing_binaries"]:
292
+ return 2
293
+ return 3
294
+
295
+
296
+ def cmd_json() -> int:
297
+ json.dump(_status(), sys.stdout, indent=2)
298
+ sys.stdout.write("\n")
299
+ return 0
300
+
301
+
302
+ def cmd_install() -> int:
303
+ missing = _check_binaries()
304
+ installed_deps = False
305
+ if missing:
306
+ system = platform.system()
307
+ if system == "Darwin":
308
+ ok, msg = _install_macos(missing)
309
+ print(f"[setup] {msg}", file=sys.stderr)
310
+ if not ok:
311
+ return 2
312
+ still_missing = _check_binaries()
313
+ if still_missing:
314
+ print(
315
+ f"[setup] still missing after install: {', '.join(still_missing)}",
316
+ file=sys.stderr,
317
+ )
318
+ return 2
319
+ installed_deps = True
320
+ elif system == "Linux":
321
+ print("[setup] dependencies missing on Linux — please install:", file=sys.stderr)
322
+ print(" " + _install_hint_linux(missing), file=sys.stderr)
323
+ return 2
324
+ elif system == "Windows":
325
+ print("[setup] dependencies missing on Windows — please install:", file=sys.stderr)
326
+ print(" " + _install_hint_windows(missing), file=sys.stderr)
327
+ return 2
328
+ else:
329
+ print(
330
+ f"[setup] unsupported platform ({system}) for auto-install. Install manually:",
331
+ file=sys.stderr,
332
+ )
333
+ print(f" missing: {', '.join(missing)}", file=sys.stderr)
334
+ return 2
335
+
336
+ created = _scaffold_env()
337
+ if created:
338
+ print(f"[setup] created config: {config_file()}")
339
+ else:
340
+ print(f"[setup] config exists: {config_file()}")
341
+
342
+ has_key, backend = _have_api_key()
343
+ if has_key:
344
+ _write_setup_complete()
345
+ print(f"[setup] ready. whisper backend: {backend}")
346
+ if installed_deps:
347
+ print("[setup] installed dependencies; /watch is fully set up.")
348
+ return 0
349
+
350
+ print("")
351
+ print("[setup] one step left: add a Whisper API key.")
352
+ print("")
353
+ print(" Preferred: store it with `/arka keys` — either:")
354
+ print(" OPENAI_API_KEY (whisper-1; get one at platform.openai.com/api-keys)")
355
+ print(" GROQ_API_KEY (whisper-large-v3 — cheaper, faster; console.groq.com/keys)")
356
+ print(f" Fallback: set the same variable in {config_file()}")
357
+ print("")
358
+ print(" Without a key, /watch still works but videos without captions come back frames-only.")
359
+ return 3
360
+
361
+
362
+ def main() -> int:
363
+ if len(sys.argv) > 1:
364
+ arg = sys.argv[1]
365
+ if arg == "--check":
366
+ return cmd_check()
367
+ if arg == "--json":
368
+ return cmd_json()
369
+ return cmd_install()
370
+
371
+
372
+ if __name__ == "__main__":
373
+ raise SystemExit(main())
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+ """Parse a WebVTT subtitle file into a clean, timestamped transcript.
3
+
4
+ YouTube auto-subs emit rolling-duplicate cues (each line appears 2-3 times as it
5
+ scrolls). We dedupe consecutive identical cues and merge their time ranges.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ TS_RE = re.compile(
14
+ r"(\d{2}):(\d{2}):(\d{2})[.,](\d{3})\s+-->\s+(\d{2}):(\d{2}):(\d{2})[.,](\d{3})"
15
+ )
16
+ TAG_RE = re.compile(r"<[^>]+>")
17
+
18
+
19
+ def _to_seconds(h: str, m: str, s: str, ms: str) -> float:
20
+ return int(h) * 3600 + int(m) * 60 + int(s) + int(ms) / 1000.0
21
+
22
+
23
+ def parse_vtt(path: str) -> list[dict]:
24
+ text = Path(path).read_text(encoding="utf-8", errors="ignore")
25
+ lines = text.splitlines()
26
+
27
+ segments: list[dict] = []
28
+ i = 0
29
+ while i < len(lines):
30
+ match = TS_RE.match(lines[i])
31
+ if not match:
32
+ i += 1
33
+ continue
34
+
35
+ start = _to_seconds(*match.groups()[:4])
36
+ end = _to_seconds(*match.groups()[4:])
37
+ i += 1
38
+
39
+ cue_lines: list[str] = []
40
+ while i < len(lines) and lines[i].strip():
41
+ cleaned = TAG_RE.sub("", lines[i]).strip()
42
+ if cleaned:
43
+ cue_lines.append(cleaned)
44
+ i += 1
45
+
46
+ cue_text = " ".join(cue_lines).strip()
47
+ if cue_text:
48
+ segments.append({"start": round(start, 2), "end": round(end, 2), "text": cue_text})
49
+ i += 1
50
+
51
+ return _dedupe(segments)
52
+
53
+
54
+ def _dedupe(segments: list[dict]) -> list[dict]:
55
+ """Collapse rolling duplicates common in YouTube auto-subs."""
56
+ out: list[dict] = []
57
+ for seg in segments:
58
+ if out and seg["text"] == out[-1]["text"]:
59
+ out[-1]["end"] = seg["end"]
60
+ continue
61
+ if out and seg["text"].startswith(out[-1]["text"] + " "):
62
+ out[-1]["text"] = seg["text"]
63
+ out[-1]["end"] = seg["end"]
64
+ continue
65
+ out.append(seg)
66
+ return out
67
+
68
+
69
+ def filter_range(
70
+ segments: list[dict],
71
+ start_seconds: float | None,
72
+ end_seconds: float | None,
73
+ ) -> list[dict]:
74
+ """Return segments whose time range overlaps [start, end]."""
75
+ if start_seconds is None and end_seconds is None:
76
+ return segments
77
+ lo = start_seconds if start_seconds is not None else float("-inf")
78
+ hi = end_seconds if end_seconds is not None else float("inf")
79
+ return [seg for seg in segments if seg["end"] >= lo and seg["start"] <= hi]
80
+
81
+
82
+ def format_transcript(segments: list[dict]) -> str:
83
+ lines = []
84
+ for seg in segments:
85
+ start = int(seg["start"])
86
+ stamp = f"[{start // 60:02d}:{start % 60:02d}]"
87
+ lines.append(f"{stamp} {seg['text']}")
88
+ return "\n".join(lines)
89
+
90
+
91
+ if __name__ == "__main__":
92
+ if len(sys.argv) < 2:
93
+ print("usage: transcribe.py <vtt-path>", file=sys.stderr)
94
+ raise SystemExit(2)
95
+ print(format_transcript(parse_vtt(sys.argv[1])))