diff --git a/FEATURES.md b/FEATURES.md index f2d4889..6961e52 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1,6 +1,6 @@ # Sovereign AI Workstation — Full Product Breakdown -> Engine: **CODEC v2.3** — 367 features · 74 skills · 940+ tests · 58K+ lines of production code +> Engine: **CODEC v2.3** — 368 features · 75 skills · 940+ tests · 58K+ lines of production code The product name is **Sovereign AI Workstation**. Throughout this document and the codebase, **CODEC** refers to the underlying open-source engine / @@ -182,7 +182,7 @@ and resume-after-restart guarantees throughout. --- -## 6. CODEC Skills — 78 features (74 skills + 4 infrastructure) +## 6. CODEC Skills — 79 features (75 skills + 4 infrastructure) ### Infrastructure @@ -193,7 +193,7 @@ and resume-after-restart guarantees throughout. | 3 | Skill Marketplace (install, search, list, update, remove, publish) | | 4 | **`SKILL_OBSERVATION_TRIGGER` declarative trigger metadata** — skills opt into auto-fire via 5 trigger types (window_title_match, clipboard_pattern, file_change, time, compound) *(Phase 2 Step 6)* | -### 74 Built-in Skills +### 75 Built-in Skills MCP tool name shown where it differs from the file name. @@ -201,7 +201,7 @@ MCP tool name shown where it differs from the file name. |---|---| | **Google Workspace** (8) | google_calendar, google_docs, google_drive, google_gmail, google_keep, google_sheets, google_slides, google_tasks | | **Chrome Automation** (10) | chrome_automate, chrome_click_cdp, chrome_close, chrome_extract, chrome_fill, chrome_open, chrome_read, chrome_scroll, chrome_search, chrome_tabs | -| **System Control** (10) | app_switch, brightness, clipboard, file_ops, file_search, file_write, network_info, process_manager, `system` (system_info), terminal, `volume_brightness` (volume) | +| **System Control** (11) | app_switch, brightness, clipboard, file_ops, file_search, file_write, network_info, process_manager, `system` (system_info), terminal, `volume_brightness` (volume) | | **Vision & Mouse** (2) | mouse_control (UI-TARS vision click), screenshot_text | | **AI & Content** (6) | `AI_News_Digest` (ai_news_digest), create_skill, skill_forge, translate, web_search, memory_search | | **Memory Layer** (5) | memory_search (FTS5), memory_history (temporal facts), memory_entities (CCF map), memory_save, auto_memorize, fact_extract | @@ -494,7 +494,7 @@ notification dispatch. | 3. CODEC Dashboard | 32 | | 4. CODEC Vibe | 20 | | 5. CODEC Agents | 20 | -| 6. CODEC Skills | 78 | +| 6. CODEC Skills | 79 | | 7. CODEC Infrastructure | 36 | | 8. CODEC Dictate | 15 | | 9. CODEC Instant | 12 | @@ -502,9 +502,9 @@ notification dispatch. | 11. Phase 2 — Continuous Observation + Automation *(v2.3)* | 24 | | 12. Phase 3 — Drop-a-Project Autonomous Agents *(v2.3)* | 32 | | 13. Phase 3.5 — UX Polish + Proactive Overlay *(v2.3)* | 24 | -| **TOTAL** | **367** | +| **TOTAL** | **368** | -**367 features · 74 skills · 940+ tests · 58K+ lines of production code** +**368 features · 75 skills · 940+ tests · 58K+ lines of production code** ### What's new in v2.3 — Phase 1 + 2 + 3 + 3.5 diff --git a/README.md b/README.md index 88da21a..c898e7e 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@

- 367+ Features - 74 Skills + 368+ Features + 75 Skills 940+ Tests 58K+ Lines MIT License @@ -46,7 +46,7 @@ No cloud dependency. No data leaving the machine unless you choose. No subscript | # | Product | What It Does | |:-:|---|---| -| 1 | **CODEC Core** | Voice command layer + vision mouse control — 74 skills, screen clicks by voice | +| 1 | **CODEC Core** | Voice command layer + vision mouse control — 75 skills, screen clicks by voice | | 2 | **CODEC Dictate** | Hold, speak, paste — hands-free F5 live typing at cursor, draft refinement, floating overlays | | 3 | **CODEC Instant** | Right-click → 8 AI services system-wide — proofread, translate, reply, explain | | 4 | **CODEC Chat** | 250K-context conversational AI + 12 autonomous agent crews | @@ -60,7 +60,7 @@ No cloud dependency. No data leaving the machine unless you choose. No subscript Always-on voice assistant. Say *"Hey CODEC"* or press F13 to activate. F18 for voice commands. F16 for text input. -74 skills fire instantly: Google Calendar, Gmail, Drive, Docs, Sheets, Tasks, Keep, Chrome automation, web search, Hue lights, timers, Spotify, clipboard, terminal commands, PM2 control, and more. Most skills bypass the LLM entirely — direct action, zero latency. Skills are matched by trigger specificity — longer, more specific triggers always win over generic ones. +75 skills fire instantly: Google Calendar, Gmail, Drive, Docs, Sheets, Tasks, Keep, Chrome automation, web search, Hue lights, timers, Spotify, clipboard, terminal commands, PM2 control, and more. Most skills bypass the LLM entirely — direct action, zero latency. Skills are matched by trigger specificity — longer, more specific triggers always win over generic ones. **Vision Mouse Control — See & Click** @@ -216,7 +216,7 @@ Three smart agents ship built-in: Daily Briefing, Restaurant Decider (location-a

Terminal
- 74 skills loaded at startup + 75 skills loaded at startup

Cortex Neural Map
@@ -366,7 +366,7 @@ Claude Desktop/Code/Cursor gain — through this one MCP bridge — everything C - **Your Mac, your apps** — native macOS control: mouse/keyboard via vision model, screenshot text extraction, app switching, clipboard, brightness/volume, Philips Hue, Spotify, Apple Notes, Reminders, Clock timers, music. No browser sandbox. - **Your memory** — FTS5-searchable history of every CODEC conversation. Claude can recall what *you* said weeks ago, not just this chat. -- **Your skills, not Anthropic's** — 74 pluggable CODEC skills instantly callable as tools. Write one locally in Python, it shows up in Claude without a deploy. +- **Your skills, not Anthropic's** — 75 pluggable CODEC skills instantly callable as tools. Write one locally in Python, it shows up in Claude without a deploy. - **Your LLM, your choice** — same skill catalog works whether the brain is local Qwen (offline, private) or cloud Claude. The toolkit outlives the model. - **Your voice pipeline** — Whisper STT, Kokoro TTS, wake-word — all reachable from the chat loop if you want voice output of a Claude answer. @@ -572,7 +572,7 @@ codec_marketplace.py — Skill marketplace CLI codec_overlays.py — AppKit overlay notifications (fullscreen compatible) ax_bridge/ — Swift AX accessibility bridge swift-overlay/ — Native macOS status bar app (NSPanel, event JSONL poller) -skills/ — 74 built-in skills (incl. vision mouse control) +skills/ — 75 built-in skills (incl. vision mouse control) tests/ — 940+ pytest tests across 53 files request_mic.py — macOS microphone permission helper (AVFoundation) install.sh — One-line installer @@ -633,7 +633,7 @@ python3 setup_codec.py ## Contributing -All skill contributions welcome. 74 built-in skills, 940+ tests, marketplace growing. +All skill contributions welcome. 75 built-in skills, 940+ tests, marketplace growing. ```bash git clone https://github.com/AVADSA25/codec.git diff --git a/skills/file_write.py b/skills/file_write.py new file mode 100644 index 0000000..f0eb1b7 --- /dev/null +++ b/skills/file_write.py @@ -0,0 +1,258 @@ +"""CODEC Skill: File Write — save content to a file anywhere on the Mac. + +Purpose-built for remote callers (claude.ai over HTTP MCP). Writes only — +no read, no delete, no list. Every write is logged to ~/.codec/file_write.log +so you can audit what the remote Claude has been saving. + +Usage patterns the skill understands (pass in the `task` string): + + save this to ~/Documents/notes/plan.md + ``` + # Plan + - step 1 + - step 2 + ``` + + write file ~/Desktop/scratch.txt content: hello world + + path: ~/Projects/foo/bar.md + mode: append + content: + --- + new entry + --- + +The skill accepts `mode: write` (default, overwrites) or `mode: append`. +""" +SKILL_NAME = "file_write" +SKILL_DESCRIPTION = ( + "Save text content to a file anywhere on the Mac (creates parent dirs). " + "Input: a task string that includes the destination path and the content " + "to write. Example: \"save to ~/Documents/plan.md\\n```\\n# Plan\\n- ...\\n```\". " + "Supports mode: write (default, overwrites) or mode: append. " + "Blocks system paths (/System, /etc, /usr, /Library) and credential files " + "(.ssh, .env, id_rsa, keychain, etc.). Every write is audited." +) +SKILL_MCP_EXPOSE = True +SKILL_TRIGGERS = [ + "save to file", "save this to", "write to file", "write file", + "create file", "save file", "store in file", "export to", + "dump to", "put in file", "append to file", +] + +import os +import re +import json +import time +from datetime import datetime + +# ── Configurable limits ── +_MAX_WRITE_BYTES = 500_000 # 500 KB per call — plenty for notes, code, docs +_AUDIT_LOG = os.path.expanduser("~/.codec/file_write.log") + +# ── Path safety ── +# These directory roots are ALWAYS blocked, regardless of who's calling. +_BLOCKED_ROOTS = [ + "/System", "/Library", "/usr", "/bin", "/sbin", "/etc", + "/var", "/private", "/dev", "/Volumes", +] +# Any filename (case-insensitive substring) in this list is blocked. +_BLOCKED_FILENAME_PATTERNS = [ + ".ssh", ".gnupg", ".env", "credentials", "secrets", "secret", + ".aws", ".gcloud", ".kube", "id_rsa", "id_ed25519", "id_dsa", + ".netrc", ".npmrc", ".pypirc", "keychain", "password", "token", + "api_key", "apikey", "private_key", +] +# Block extensions that could be executable shells / trust-sensitive. +_BLOCKED_EXTS = [".pem", ".key", ".p12", ".pfx", ".keystore"] + + +def _is_safe_target(path: str): + """Return (True, "") if safe to write; (False, reason) otherwise. + + Resolves symlinks via realpath so a symlink into /etc can't slip through. + """ + if not path: + return False, "Empty path." + expanded = os.path.expanduser(path) + # If parent exists, realpath the parent and append basename — the file + # itself may not exist yet, so we can't realpath(path) directly. + parent = os.path.dirname(expanded) or "." + try: + real_parent = os.path.realpath(parent) + except Exception: + real_parent = parent + real_path = os.path.join(real_parent, os.path.basename(expanded)) + + for blocked in _BLOCKED_ROOTS: + if real_path == blocked or real_path.startswith(blocked + os.sep): + return False, f"Blocked system path: {blocked}" + + base_lower = os.path.basename(real_path).lower() + for pat in _BLOCKED_FILENAME_PATTERNS: + if pat in base_lower: + return False, f"Blocked filename pattern: {pat!r}" + + for ext in _BLOCKED_EXTS: + if base_lower.endswith(ext): + return False, f"Blocked extension: {ext}" + + # Sanity: must be under $HOME or /tmp (broad but not everything). + home = os.path.realpath(os.path.expanduser("~")) + tmp = "/tmp" + if not (real_path.startswith(home + os.sep) or real_path.startswith(tmp + os.sep)): + return False, ( + f"Target must live under $HOME or /tmp (got: {real_path}). " + "Adjust file_write._BLOCKED_ROOTS if you need wider scope." + ) + + return True, "" + + +# ── Parsing ── + +_PATH_HINTS = [ + r'(?:^|\s)(?:path|file|to|into|at|destination|dest)\s*[:=]\s*["\']?([^"\'\n]+?)["\']?(?:\s|$)', + r'save\s+(?:this\s+|that\s+|it\s+)?(?:to\s+|into\s+|at\s+)["\']?([^"\'\n]+?)["\']?(?:\s|$)', + r'write\s+(?:this\s+|to\s+|into\s+)?["\']?(~?[/\w][\w./\s_-]*?\.[\w]{1,8})["\']?', + r'(["\'])(~?/[^"\'\n]+)\1', + r'(~?/[\w./_-]+\.[\w]{1,8})', +] + +_MODE_RE = re.compile(r'(?:^|\s)mode\s*[:=]\s*(write|append|overwrite)\b', re.I) + + +def _extract_path(task: str): + """Best-effort path extraction from a natural-language instruction.""" + for pat in _PATH_HINTS: + m = re.search(pat, task, re.IGNORECASE | re.MULTILINE) + if m: + # Last group is always the captured path + groups = [g for g in m.groups() if g] + if groups: + candidate = groups[-1].strip().rstrip(".,;:") + if candidate and ("/" in candidate or candidate.startswith("~")): + return os.path.expanduser(candidate) + return None + + +def _extract_content(task: str): + """Pull the content out. Preference order: + 1) Triple-backtick fenced block (optionally with language tag) + 2) After an explicit 'content:' / 'body:' / 'data:' / 'text:' marker + 3) After a markdown '---' separator + """ + # 1) ```[lang]\n ... \n``` + fence = re.search(r'```[\w+-]*\s*\n?(.*?)\n?```', task, re.DOTALL) + if fence: + return fence.group(1).rstrip("\n") + + # 2) explicit marker — take everything after it (trailing newline trimmed) + for kw in ("content:", "body:", "data:", "text:"): + idx = task.lower().find(kw) + if idx >= 0: + after = task[idx + len(kw):].lstrip("\n").rstrip() + # strip a leading space + if after.startswith(" "): + after = after[1:] + if after: + return after + + # 3) --- separator (take everything AFTER the last ---) + if "\n---\n" in task: + return task.rsplit("\n---\n", 1)[-1].rstrip() + + return None + + +def _extract_mode(task: str) -> str: + m = _MODE_RE.search(task) + if not m: + return "write" + val = m.group(1).lower() + if val in ("write", "overwrite"): + return "write" + if val == "append": + return "append" + return "write" + + +# ── Audit log ── + +def _audit_write(path: str, size: int, mode: str, transport: str): + """Append one JSON line per successful write.""" + try: + os.makedirs(os.path.dirname(_AUDIT_LOG), exist_ok=True) + entry = { + "ts": datetime.utcnow().isoformat() + "Z", + "path": path, + "size": size, + "mode": mode, + "transport": transport, + } + with open(_AUDIT_LOG, "a", encoding="utf-8") as f: + f.write(json.dumps(entry) + "\n") + except Exception: + # Never fail a write because of audit-log problems. + pass + + +# ── Entry point ── + +def run(task: str, context: str = "") -> str: + if not isinstance(task, str) or not task.strip(): + return "file_write: empty task. Example: \"save to ~/notes.txt content: hello\"" + + path = _extract_path(task) + if not path: + return ( + "file_write: couldn't find a destination path in the task. " + "Try: 'save to ~/Documents/foo.md\\n```\\n\\n```' " + "or 'path: ~/Desktop/x.txt\\ncontent: hi'" + ) + + content = _extract_content(task) + if content is None: + return ( + f"file_write: resolved path '{path}' but found no content. " + "Put the content in a triple-backtick block, or after 'content:'." + ) + + size = len(content.encode("utf-8")) + if size > _MAX_WRITE_BYTES: + return ( + f"file_write: content too large ({size:,} bytes > " + f"{_MAX_WRITE_BYTES:,} cap). Split into smaller chunks or raise " + "_MAX_WRITE_BYTES in skills/file_write.py." + ) + + safe, reason = _is_safe_target(path) + if not safe: + return f"file_write: refused — {reason}" + + mode_label = _extract_mode(task) + fmode = "a" if mode_label == "append" else "w" + + # Ensure parent dir exists. + parent = os.path.dirname(path) + if parent and not os.path.exists(parent): + try: + os.makedirs(parent, exist_ok=True) + except Exception as e: + return f"file_write: cannot create directory {parent}: {e}" + + try: + with open(path, fmode, encoding="utf-8") as f: + f.write(content) + except PermissionError as e: + return f"file_write: permission denied for {path}: {e}" + except OSError as e: + return f"file_write: OS error writing {path}: {e}" + except Exception as e: + return f"file_write: unexpected error writing {path}: {type(e).__name__}: {e}" + + transport = os.environ.get("CODEC_MCP_TRANSPORT", "stdio") + _audit_write(path, size, mode_label, transport) + + verb = "Appended to" if mode_label == "append" else "Saved" + return f"{verb} {path} ({size:,} bytes)."