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 @@
-
-
+
+
@@ -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

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

@@ -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