arkaos 4.30.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 (47) 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/governance/evidence_checks.py +185 -1
  15. package/core/keys.py +17 -5
  16. package/departments/brand/agents/brand-director.yaml +1 -0
  17. package/departments/brand/skills/design-review/SKILL.md +6 -0
  18. package/departments/brand/skills/ux-audit/SKILL.md +4 -1
  19. package/departments/content/agents/content-strategist.yaml +1 -0
  20. package/departments/content/agents/production/trends-analyst.yaml +1 -0
  21. package/departments/content/agents/production/video-producer.yaml +1 -0
  22. package/departments/content/agents/scriptwriter.yaml +1 -0
  23. package/departments/dev/agents/frontend-dev.yaml +1 -0
  24. package/departments/dev/skills/animated-website/SKILL.md +14 -1
  25. package/departments/dev/skills/watch/SKILL.md +162 -0
  26. package/departments/dev/skills/watch/references/claude-video.LICENSE +21 -0
  27. package/departments/dev/skills/watch/scripts/config.py +145 -0
  28. package/departments/dev/skills/watch/scripts/download.py +179 -0
  29. package/departments/dev/skills/watch/scripts/frames.py +762 -0
  30. package/departments/dev/skills/watch/scripts/setup.py +373 -0
  31. package/departments/dev/skills/watch/scripts/transcribe.py +95 -0
  32. package/departments/dev/skills/watch/scripts/watch.py +531 -0
  33. package/departments/dev/skills/watch/scripts/whisper.py +466 -0
  34. package/departments/marketing/agents/marketing-director.yaml +1 -0
  35. package/departments/marketing/skills/ad-creative/SKILL.md +5 -0
  36. package/harness/codex/AGENTS.md +1 -1
  37. package/harness/copilot/copilot-instructions.md +1 -1
  38. package/harness/cursor/rules/arkaos.mdc +2 -2
  39. package/harness/gemini/GEMINI.md +1 -1
  40. package/harness/opencode/AGENTS.md +1 -1
  41. package/harness/zed/.rules +1 -1
  42. package/installer/doctor.js +38 -4
  43. package/installer/frontend-tooling.js +36 -0
  44. package/knowledge/agents-registry-v2.json +8 -1
  45. package/knowledge/skills-manifest.json +14 -1
  46. package/package.json +1 -1
  47. package/pyproject.toml +1 -1
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bradley Bonanno
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """Shared /watch configuration helpers (ArkaOS-native).
3
+
4
+ Every filesystem anchor resolves at CALL time, never import time, so a
5
+ redirected HOME (tests, sandboxes) always takes effect. Key resolution
6
+ order: environment -> ~/.arkaos/keys.json (managed by `/arka keys`) ->
7
+ ~/.arkaos/watch.env -> ./.env.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import sys
14
+ import time
15
+ from pathlib import Path
16
+
17
+ DEFAULT_DETAIL = "balanced"
18
+
19
+ DETAILS = {"transcript", "efficient", "balanced", "token-burner"}
20
+
21
+
22
+ def arkaos_home() -> Path:
23
+ """User-data root — ~/.arkaos, or ARKAOS_HOME when set."""
24
+ override = os.environ.get("ARKAOS_HOME")
25
+ if override:
26
+ return Path(override).expanduser()
27
+ return Path.home() / ".arkaos"
28
+
29
+
30
+ def config_file() -> Path:
31
+ return arkaos_home() / "watch.env"
32
+
33
+
34
+ def keys_file() -> Path:
35
+ return arkaos_home() / "keys.json"
36
+
37
+
38
+ def telemetry_file() -> Path:
39
+ return arkaos_home() / "telemetry" / "watch-usage.jsonl"
40
+
41
+
42
+ def read_env_file(path: Path | None = None) -> dict[str, str]:
43
+ if path is None:
44
+ path = config_file()
45
+ values: dict[str, str] = {}
46
+ if not path.exists():
47
+ return values
48
+ try:
49
+ lines = path.read_text(encoding="utf-8").splitlines()
50
+ except OSError:
51
+ return values
52
+ for line in lines:
53
+ raw = line.strip()
54
+ if not raw or raw.startswith("#") or "=" not in raw:
55
+ continue
56
+ key, _, value = raw.partition("=")
57
+ value = value.strip()
58
+ if len(value) >= 2 and value[0] in ('"', "'") and value[-1] == value[0]:
59
+ value = value[1:-1]
60
+ else:
61
+ # Strip an inline comment (a '#' preceded by whitespace) from an
62
+ # unquoted value. Without this, `WATCH_DETAIL=balanced # note`
63
+ # parses as "balanced # note", fails validation, and silently
64
+ # falls back to the default. Keeps '#' inside quotes / API keys.
65
+ for i, ch in enumerate(value):
66
+ if ch == "#" and i > 0 and value[i - 1] in " \t":
67
+ value = value[:i].rstrip()
68
+ break
69
+ values[key.strip()] = value
70
+ return values
71
+
72
+
73
+ def read_keys_json() -> dict[str, str]:
74
+ """API keys stored by `/arka keys` (~/.arkaos/keys.json). Fail-open."""
75
+ path = keys_file()
76
+ if not path.exists():
77
+ return {}
78
+ try:
79
+ data = json.loads(path.read_text(encoding="utf-8"))
80
+ except (OSError, ValueError):
81
+ return {}
82
+ if not isinstance(data, dict):
83
+ return {}
84
+ return {str(k): str(v) for k, v in data.items() if v}
85
+
86
+
87
+ def resolve_api_key(name: str) -> str | None:
88
+ """env -> ~/.arkaos/keys.json -> ~/.arkaos/watch.env -> ./.env."""
89
+ value = os.environ.get(name)
90
+ if value and value.strip():
91
+ return value.strip()
92
+ value = read_keys_json().get(name)
93
+ if value and value.strip():
94
+ return value.strip()
95
+ for path in (config_file(), Path.cwd() / ".env"):
96
+ value = read_env_file(path).get(name)
97
+ if value and value.strip():
98
+ return value.strip()
99
+ return None
100
+
101
+
102
+ def get_config() -> dict[str, object]:
103
+ file_values = read_env_file()
104
+
105
+ detail = (
106
+ os.environ.get("WATCH_DETAIL")
107
+ or file_values.get("WATCH_DETAIL")
108
+ or DEFAULT_DETAIL
109
+ )
110
+ if detail not in DETAILS:
111
+ detail = DEFAULT_DETAIL
112
+
113
+ return {
114
+ "detail": detail,
115
+ "config_file": str(config_file()),
116
+ }
117
+
118
+
119
+ def frame_cap(detail: str) -> int | None:
120
+ if detail == "efficient":
121
+ return 50
122
+ if detail == "balanced":
123
+ return 100
124
+ if detail == "token-burner":
125
+ return None
126
+ if detail == "transcript":
127
+ return None
128
+ return 100
129
+
130
+
131
+ def record_telemetry(event: dict) -> None:
132
+ """Append one usage record to ~/.arkaos/telemetry/watch-usage.jsonl.
133
+
134
+ Observability must never break a watch run: filesystem failures
135
+ (OSError) are reported to stderr and swallowed (fail-open). Callers
136
+ pass only JSON-serializable scalars, so serialization cannot raise.
137
+ """
138
+ try:
139
+ path = telemetry_file()
140
+ path.parent.mkdir(parents=True, exist_ok=True)
141
+ record = {"ts": round(time.time(), 3), **event}
142
+ with path.open("a", encoding="utf-8") as fh:
143
+ fh.write(json.dumps(record, ensure_ascii=False) + "\n")
144
+ except OSError as exc:
145
+ print(f"[watch] telemetry write skipped: {exc}", file=sys.stderr)
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env python3
2
+ """Download a video via yt-dlp, or resolve a local file path.
3
+
4
+ Also fetches subtitles (manual first, then auto-generated) in VTT format so
5
+ transcribe.py can parse them without needing Whisper.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ from pathlib import Path
14
+ from urllib.parse import urlparse
15
+
16
+ VIDEO_EXTS = {".mp4", ".mkv", ".webm", ".mov", ".m4v", ".avi", ".flv", ".wmv"}
17
+
18
+
19
+ def is_url(source: str) -> bool:
20
+ if source.startswith("-"):
21
+ return False
22
+ parsed = urlparse(source)
23
+ return parsed.scheme in ("http", "https") and bool(parsed.netloc)
24
+
25
+
26
+ def resolve_local(path: str) -> dict:
27
+ p = Path(path).expanduser().resolve()
28
+ if not p.exists():
29
+ raise SystemExit(f"File not found: {p}")
30
+ if p.suffix.lower() not in VIDEO_EXTS:
31
+ print(
32
+ f"[watch] warning: {p.suffix} is not a known video extension, proceeding anyway",
33
+ file=sys.stderr,
34
+ )
35
+ return {
36
+ "video_path": str(p),
37
+ "subtitle_path": None,
38
+ "info": {"title": p.name, "url": str(p)},
39
+ "downloaded": False,
40
+ }
41
+
42
+
43
+ def _pick_subtitle(out_dir: Path) -> Path | None:
44
+ candidates = sorted(out_dir.glob("video*.vtt"))
45
+ if not candidates:
46
+ return None
47
+ preferred = [
48
+ c for c in candidates
49
+ if any(marker in c.name for marker in (".en.", ".en-US.", ".en-GB.", ".en-orig."))
50
+ ]
51
+ return preferred[0] if preferred else candidates[0]
52
+
53
+
54
+ def _pick_video(out_dir: Path) -> Path | None:
55
+ for ext in (".mp4", ".mkv", ".webm", ".mov", ".m4a", ".mp3", ".opus"):
56
+ for candidate in out_dir.glob(f"video*{ext}"):
57
+ return candidate
58
+ for candidate in out_dir.glob("video.*"):
59
+ if candidate.suffix.lower() in VIDEO_EXTS:
60
+ return candidate
61
+ return None
62
+
63
+
64
+ def fetch_captions(url: str, out_dir: Path) -> dict:
65
+ """Fetch metadata and best available VTT captions without downloading video."""
66
+ if shutil.which("yt-dlp") is None:
67
+ raise SystemExit("yt-dlp is not installed. Install with: brew install yt-dlp")
68
+
69
+ out_dir.mkdir(parents=True, exist_ok=True)
70
+ output_template = str(out_dir / "video.%(ext)s")
71
+ cmd = [
72
+ "yt-dlp",
73
+ "--skip-download",
74
+ "--write-info-json",
75
+ "--write-subs",
76
+ "--write-auto-subs",
77
+ "--sub-langs", "en.*",
78
+ "--sub-format", "vtt",
79
+ "--convert-subs", "vtt",
80
+ "--no-playlist",
81
+ "--ignore-errors",
82
+ "-o", output_template,
83
+ "--",
84
+ url,
85
+ ]
86
+ subprocess.run(cmd, stdout=sys.stderr, stderr=sys.stderr)
87
+ subtitle = _pick_subtitle(out_dir)
88
+ info = _read_info(out_dir / "video.info.json", url)
89
+ return {
90
+ "video_path": None,
91
+ "subtitle_path": str(subtitle) if subtitle else None,
92
+ "info": info or {"url": url},
93
+ "downloaded": False,
94
+ }
95
+
96
+
97
+ def _read_info(info_path: Path, url: str) -> dict:
98
+ info: dict = {}
99
+ if info_path.exists():
100
+ try:
101
+ raw = json.loads(info_path.read_text(encoding="utf-8"))
102
+ info = {
103
+ "title": raw.get("title"),
104
+ "uploader": raw.get("uploader") or raw.get("channel"),
105
+ "duration": raw.get("duration"),
106
+ "url": raw.get("webpage_url") or url,
107
+ }
108
+ except Exception as exc:
109
+ print(f"[watch] info.json parse failed: {exc}", file=sys.stderr)
110
+ info = {"url": url}
111
+ return info
112
+
113
+
114
+ def download_url(
115
+ url: str,
116
+ out_dir: Path,
117
+ audio_only: bool = False,
118
+ ) -> dict:
119
+ if shutil.which("yt-dlp") is None:
120
+ raise SystemExit("yt-dlp is not installed. Install with: brew install yt-dlp")
121
+
122
+ out_dir.mkdir(parents=True, exist_ok=True)
123
+ output_template = str(out_dir / "video.%(ext)s")
124
+
125
+ fmt = "ba/bestaudio" if audio_only else "bv*[height<=720]+ba/b[height<=720]/bv+ba/b"
126
+ cmd = [
127
+ "yt-dlp",
128
+ "-N", "8",
129
+ "-f", fmt,
130
+ "--merge-output-format", "mp4",
131
+ "--write-info-json",
132
+ "--write-subs",
133
+ "--write-auto-subs",
134
+ "--sub-langs", "en.*",
135
+ "--sub-format", "vtt",
136
+ "--convert-subs", "vtt",
137
+ "--no-playlist",
138
+ "--ignore-errors",
139
+ "-o", output_template,
140
+ "--",
141
+ url,
142
+ ]
143
+
144
+ # yt-dlp may exit non-zero if a subtitle variant fails (e.g. 429) even when
145
+ # the video itself downloaded fine. Treat "video file present" as success.
146
+ result = subprocess.run(cmd, stdout=sys.stderr, stderr=sys.stderr)
147
+ video = _pick_video(out_dir)
148
+ if video is None:
149
+ raise SystemExit(
150
+ f"yt-dlp did not produce a video file in {out_dir} (exit {result.returncode})"
151
+ )
152
+
153
+ subtitle = _pick_subtitle(out_dir)
154
+ info = _read_info(out_dir / "video.info.json", url)
155
+
156
+ return {
157
+ "video_path": str(video),
158
+ "subtitle_path": str(subtitle) if subtitle else None,
159
+ "info": info or {"url": url},
160
+ "downloaded": True,
161
+ }
162
+
163
+
164
+ def download(
165
+ source: str,
166
+ out_dir: Path,
167
+ audio_only: bool = False,
168
+ ) -> dict:
169
+ if is_url(source):
170
+ return download_url(source, out_dir, audio_only=audio_only)
171
+ return resolve_local(source)
172
+
173
+
174
+ if __name__ == "__main__":
175
+ if len(sys.argv) < 3:
176
+ print("usage: download.py <url-or-path> <out-dir>", file=sys.stderr)
177
+ raise SystemExit(2)
178
+ result = download(sys.argv[1], Path(sys.argv[2]))
179
+ print(json.dumps(result, indent=2))