diff --git a/9p b/9p index 6b57fe1..c221426 160000 --- a/9p +++ b/9p @@ -1 +1 @@ -Subproject commit 6b57fe1f7774bca1a1acebe019b615780987acd4 +Subproject commit c221426af45cc028ceb9a7ff0fd41aeb297930f4 diff --git a/tools/denote_view b/tools/denote_view index 7502e9d..08370fb 100755 --- a/tools/denote_view +++ b/tools/denote_view @@ -26,7 +26,7 @@ while [[ $# -gt 0 ]]; do done if [ -z "$ID" ]; then - echo "usage: view_denote.sh --id " >&2 + echo '{"schema": {"type": "array", "items": [{"name": "--id", "type": "string", "description": "Denote identifier", "required": true}], "examples": [["--id", "20250601T1234"]]}}' >&2 exit 1 fi diff --git a/tools/env b/tools/env deleted file mode 100755 index 91260d7..0000000 --- a/tools/env +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -# ollie:parallel read -# ollie:prompt -# ## env -# -# Print the agent's runtime environment variables. -# -# **Args**: (none) -# -# ``` -# call_tool: calls=[{tool: "env", args: []}] -# ``` -# ollie:end -set -euo pipefail - -env | sort diff --git a/tools/file_edit b/tools/file_edit index fc8e9e5..010cc54 100644 --- a/tools/file_edit +++ b/tools/file_edit @@ -91,7 +91,7 @@ def robust_replace(content, old_string, new_string, replace_all): return content.replace(match, new_string, 1), None, False if len(sys.argv) < 4: - print("usage: file_edit [replace_all=true|1|yes]") + print('{"schema": {"type": "array", "items": [{"name": "file_path", "type": "string", "description": "Absolute path to the file to edit", "required": true}, {"name": "old_string", "type": "string", "description": "Text to find (fuzzy whitespace matching)", "required": true}, {"name": "new_string", "type": "string", "description": "Replacement text", "required": true}, {"name": "replace_all", "type": "string", "description": "Set to true/1/yes to replace all occurrences", "required": false}], "examples": [["/abs/path", "old text", "new text"], ["/abs/path", "old", "new", "true"]]}}') sys.exit(1) file_path = sys.argv[1] diff --git a/tools/file_glob b/tools/file_glob index e6658da..4819726 100644 --- a/tools/file_glob +++ b/tools/file_glob @@ -19,7 +19,7 @@ pattern="$1" search_path="${2:-$PWD}" -[ -z "$pattern" ] && echo "usage: file_glob [path]" && exit 1 +[ -z "$pattern" ] && echo '{"schema": {"type": "array", "items": [{"name": "pattern", "type": "string", "description": "Glob pattern (e.g. \"*.go\", \"**/*.ts\")", "required": true}, {"name": "search_dir", "type": "string", "description": "Absolute search directory path (optional, defaults to cwd)", "required": false}], "examples": [["*.go", "/abs/search/dir"], ["**/*.go"]]}}' && exit 1 if [ ! -d "$search_path" ]; then echo "STATUS=error" diff --git a/tools/file_grep b/tools/file_grep index 9823997..32576ce 100644 --- a/tools/file_grep +++ b/tools/file_grep @@ -28,7 +28,12 @@ import subprocess import shutil import argparse -parser = argparse.ArgumentParser(prog='file_grep', add_help=False) +class _Schema(argparse.ArgumentParser): + def error(self, msg): + print('{"schema": {"type": "array", "items": [{"name": "pattern", "type": "string", "description": "Regex pattern to search", "required": true}, {"name": "--path=DIR", "type": "string", "description": "Search directory", "required": false}, {"name": "--mode=MODE", "type": "string", "description": "content|files_with_matches|count", "required": false}, {"name": "--glob=PATTERN", "type": "string", "description": "File glob filter", "required": false}, {"name": "-i", "type": "string", "description": "Case-insensitive", "required": false}, {"name": "-A N", "type": "string", "description": "Lines after match", "required": false}, {"name": "-B N", "type": "string", "description": "Lines before match", "required": false}, {"name": "-C N", "type": "string", "description": "Lines around match", "required": false}], "examples": [["TODO", "--path=src/", "--glob=*.go"]]}}') + sys.exit(1) + +parser = _Schema(prog='file_grep', add_help=False) parser.add_argument('pattern') parser.add_argument('--path', default='') parser.add_argument('--mode', default='content', diff --git a/tools/file_read b/tools/file_read index d173176..5d3d014 100644 --- a/tools/file_read +++ b/tools/file_read @@ -43,7 +43,7 @@ for a in sys.argv[1:]: args.append(a) if not args: - print("usage: file_read [start] [end] [--head=N]") + print('{"schema": {"type": "array", "items": [{"name": "file_path", "type": "string", "description": "Absolute path to the file to read", "required": true}, {"name": "start", "type": "string", "description": "1-indexed start line (optional, default: 1)", "required": false}, {"name": "end", "type": "string", "description": "1-indexed end line, inclusive (optional, max 2000 lines)", "required": false}, {"name": "--head=N", "type": "string", "description": "Read first N lines instead of start/end", "required": false}], "examples": [["/abs/path"], ["/abs/path", "100", "200"], ["/abs/path", "--head=50"]]}}') sys.exit(1) file_path = args[0] diff --git a/tools/file_write b/tools/file_write index 95e2ec8..981a31c 100644 --- a/tools/file_write +++ b/tools/file_write @@ -28,7 +28,7 @@ def format_diff(path, old, new): return f"```diff\n{diff}```" if len(sys.argv) < 3: - print("usage: file_write ") + print('{"schema": {"type": "array", "items": [{"name": "file_path", "type": "string", "description": "Absolute path to create or overwrite", "required": true}, {"name": "content", "type": "string", "description": "File content to write", "required": true}], "examples": [["/abs/path", "file content here"]]}}') sys.exit(1) file_path = sys.argv[1] diff --git a/tools/grep b/tools/grep deleted file mode 100755 index 215a54c..0000000 --- a/tools/grep +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -# ollie:parallel read -# ollie:prompt -# ## grep -# -# Run grep directly. Use for piped input or when file_grep is insufficient. -# -# **Args**: `[grep_options..., pattern, file...]` -# -# ``` -# call_tool: calls=[{tool: "grep", args: ["-rn", "TODO", "src/"]}] -# ``` -# ollie:end -set -uo pipefail - -exec grep "$@" diff --git a/tools/gui_accessibility b/tools/gui_accessibility index 628477e..76b55e6 100755 --- a/tools/gui_accessibility +++ b/tools/gui_accessibility @@ -391,7 +391,7 @@ def main(): sys.exit(1) if len(sys.argv) < 2: - print("usage: gui_accessibility [app] [args...]", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "apps|tree|inspect|find|read|click|type|action", "required": true}, {"name": "app", "type": "string", "description": "App name", "required": false}, {"name": "args...", "type": "string", "description": "Action-specific arguments", "required": false}], "examples": [["apps"], ["tree", "firefox"]]}}') sys.exit(1) cmd = sys.argv[1] @@ -401,7 +401,7 @@ def main(): return if len(sys.argv) < 3: - print(f"usage: gui_accessibility {cmd} [args...]", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "apps|tree|inspect|find|read|click|type|action", "required": true}, {"name": "app", "type": "string", "description": "App name", "required": false}, {"name": "args...", "type": "string", "description": "Action-specific arguments", "required": false}], "examples": [["apps"], ["tree", "firefox"]]}}') sys.exit(1) app_name = sys.argv[2] @@ -421,7 +421,7 @@ def main(): elif cmd == "find": if len(sys.argv) < 4: - print("usage: gui_accessibility find [name]", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "apps|tree|inspect|find|read|click|type|action", "required": true}, {"name": "app", "type": "string", "description": "App name", "required": false}, {"name": "args...", "type": "string", "description": "Action-specific arguments", "required": false}], "examples": [["apps"], ["tree", "firefox"]]}}') sys.exit(1) target_role = sys.argv[3] target_name = sys.argv[4] if len(sys.argv) > 4 else "" @@ -435,7 +435,7 @@ def main(): elif cmd == "read": if len(sys.argv) < 4: - print("usage: gui_accessibility read ", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "apps|tree|inspect|find|read|click|type|action", "required": true}, {"name": "app", "type": "string", "description": "App name", "required": false}, {"name": "args...", "type": "string", "description": "Action-specific arguments", "required": false}], "examples": [["apps"], ["tree", "firefox"]]}}') sys.exit(1) path = sys.argv[3] element = resolve_path(app, path) @@ -443,7 +443,7 @@ def main(): elif cmd == "click": if len(sys.argv) < 4: - print("usage: gui_accessibility click ", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "apps|tree|inspect|find|read|click|type|action", "required": true}, {"name": "app", "type": "string", "description": "App name", "required": false}, {"name": "args...", "type": "string", "description": "Action-specific arguments", "required": false}], "examples": [["apps"], ["tree", "firefox"]]}}') sys.exit(1) path = sys.argv[3] element = resolve_path(app, path) @@ -451,7 +451,7 @@ def main(): elif cmd == "type": if len(sys.argv) < 5: - print("usage: gui_accessibility type ", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "apps|tree|inspect|find|read|click|type|action", "required": true}, {"name": "app", "type": "string", "description": "App name", "required": false}, {"name": "args...", "type": "string", "description": "Action-specific arguments", "required": false}], "examples": [["apps"], ["tree", "firefox"]]}}') sys.exit(1) path = sys.argv[3] text = sys.argv[4] @@ -460,7 +460,7 @@ def main(): elif cmd == "action": if len(sys.argv) < 5: - print("usage: gui_accessibility action ", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "apps|tree|inspect|find|read|click|type|action", "required": true}, {"name": "app", "type": "string", "description": "App name", "required": false}, {"name": "args...", "type": "string", "description": "Action-specific arguments", "required": false}], "examples": [["apps"], ["tree", "firefox"]]}}') sys.exit(1) path = sys.argv[3] action_name = sys.argv[4] diff --git a/tools/gui_activities b/tools/gui_activities index df2df3c..2e7b68d 100755 --- a/tools/gui_activities +++ b/tools/gui_activities @@ -65,7 +65,8 @@ case "$cmd" in echo "$id $name" ;; switch) - target="${2:?usage: gui_activities switch }" + if [ $# -lt 2 ]; then echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|current|switch|create|remove", "required": false}, {"name": "name_or_id", "type": "string", "description": "Activity name or ID (for switch/create/remove)", "required": false}], "examples": [["list"], ["switch", "Development"]]}}'; exit 1; fi + target="${2}" # Try as ID first result=$(gdbus call --session --dest "$SERVICE" \ @@ -100,7 +101,8 @@ case "$cmd" in exit 1 ;; create) - name="${2:?usage: gui_activities create }" + if [ $# -lt 2 ]; then echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|current|switch|create|remove", "required": false}, {"name": "name_or_id", "type": "string", "description": "Activity name or ID (for switch/create/remove)", "required": false}], "examples": [["list"], ["switch", "Development"]]}}'; exit 1; fi + name="${2}" id=$(gdbus call --session --dest "$SERVICE" \ --object-path "$OBJ_PATH" \ --method "$IFACE.AddActivity" "$name" 2>/dev/null | \ @@ -108,7 +110,8 @@ case "$cmd" in echo "created: $id ($name)" ;; remove) - target="${2:?usage: gui_activities remove }" + if [ $# -lt 2 ]; then echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|current|switch|create|remove", "required": false}, {"name": "name_or_id", "type": "string", "description": "Activity name or ID (for switch/create/remove)", "required": false}], "examples": [["list"], ["switch", "Development"]]}}'; exit 1; fi + target="${2}" # Try as ID directly gdbus call --session --dest "$SERVICE" \ @@ -143,7 +146,7 @@ case "$cmd" in exit 1 ;; *) - echo "usage: gui_activities [name_or_id]" >&2 + echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|current|switch|create|remove", "required": false}, {"name": "name_or_id", "type": "string", "description": "Activity name or ID (for switch/create/remove)", "required": false}], "examples": [["list"], ["switch", "Development"]]}}' >&2 exit 1 ;; esac diff --git a/tools/gui_apps b/tools/gui_apps index 4e12bb7..3163b6e 100755 --- a/tools/gui_apps +++ b/tools/gui_apps @@ -37,7 +37,8 @@ case "$cmd" in fi ;; launch) - app="${2:?usage: gui_apps launch }" + if [ $# -lt 2 ]; then echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|launch|activate|installed", "required": true}, {"name": "app", "type": "string", "description": "App name or desktop file (for launch/activate)", "required": false}], "examples": [["list"], ["launch", "firefox"], ["activate", "Firefox"]]}}'; exit 1; fi + app="${2}" # Try gtk-launch first (handles .desktop files) if command -v gtk-launch &>/dev/null; then @@ -77,7 +78,8 @@ case "$cmd" in fi ;; activate) - app="${2:?usage: gui_apps activate }" + if [ $# -lt 2 ]; then echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|launch|activate|installed", "required": true}, {"name": "app", "type": "string", "description": "App name or desktop file (for launch/activate)", "required": false}], "examples": [["list"], ["launch", "firefox"], ["activate", "Firefox"]]}}'; exit 1; fi + app="${2}" # Focus existing window if command -v wmctrl &>/dev/null; then # Try by class first @@ -110,7 +112,7 @@ case "$cmd" in done | sort ;; *) - echo "usage: gui_apps [app]" >&2 + echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|launch|activate|installed", "required": true}, {"name": "app", "type": "string", "description": "App name or desktop file (for launch/activate)", "required": false}], "examples": [["list"], ["launch", "firefox"], ["activate", "Firefox"]]}}' >&2 exit 1 ;; esac diff --git a/tools/gui_brightness b/tools/gui_brightness index 4d5b0f1..9bd261e 100755 --- a/tools/gui_brightness +++ b/tools/gui_brightness @@ -44,7 +44,8 @@ case "$cmd" in echo "${percent}% ($current/$max)" ;; set) - percent="${2:?usage: gui_brightness set }" + if [ $# -lt 2 ]; then echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "get|set", "required": true}, {"name": "percent", "type": "string", "description": "Brightness percent 0-100 (for set)", "required": false}], "examples": [["get"], ["set", "75"]]}}'; exit 1; fi + percent="${2}" if [ "$percent" -lt 0 ] || [ "$percent" -gt 100 ]; then echo "ERROR: percent must be 0-100" >&2 exit 1 @@ -83,7 +84,7 @@ case "$cmd" in echo "brightness: ${new_pct}% ($value/$max)" ;; *) - echo "usage: gui_brightness [get|set |up [step]|down [step]]" >&2 + echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "get|set", "required": true}, {"name": "percent", "type": "string", "description": "Brightness percent 0-100 (for set)", "required": false}], "examples": [["get"], ["set", "75"]]}}' >&2 echo "" >&2 echo " get - show current brightness as percentage" >&2 echo " set <0-100> - set brightness to percentage" >&2 diff --git a/tools/gui_clipboard b/tools/gui_clipboard index 50e2478..09ad26c 100755 --- a/tools/gui_clipboard +++ b/tools/gui_clipboard @@ -29,7 +29,8 @@ case "$cmd" in sed "s/^('//;s/')$//;s/\\\\n/\n/g" ;; set) - text="${2:?usage: gui_clipboard set }" + if [ $# -lt 2 ]; then echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "get|set|history|clear", "required": true}, {"name": "text", "type": "string", "description": "Text to set (for set action)", "required": false}], "examples": [["get"], ["set", "hello world"], ["history"], ["clear"]]}}'; exit 1; fi + text="${2}" gdbus call --session --dest "$SERVICE" \ --object-path "$PATH_OBJ" \ --method "$IFACE.setClipboardContents" "$text" >/dev/null @@ -49,7 +50,7 @@ case "$cmd" in echo "OK: clipboard history cleared" ;; *) - echo "usage: gui_clipboard [get|set|history|clear] [text|index]" >&2 + echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "get|set|history|clear", "required": true}, {"name": "text", "type": "string", "description": "Text to set (for set action)", "required": false}], "examples": [["get"], ["set", "hello world"], ["history"], ["clear"]]}}' >&2 exit 1 ;; esac diff --git a/tools/gui_files b/tools/gui_files index f8ee307..5b15cd3 100755 --- a/tools/gui_files +++ b/tools/gui_files @@ -14,7 +14,8 @@ # ollie:end set -euo pipefail -query="${1:?usage: gui_files [--type=TYPE] [--path=DIR] [--limit=N]}" +if [ $# -lt 1 ]; then echo '{"schema": {"type": "array", "items": [{"name": "query", "type": "string", "description": "Search query", "required": true}, {"name": "--type=TYPE", "type": "string", "description": "Filter by file type", "required": false}, {"name": "--path=DIR", "type": "string", "description": "Search directory path", "required": false}, {"name": "--limit=N", "type": "string", "description": "Max results", "required": false}], "examples": [["config", "--type=text"], ["TODO", "--path=/home/user"]]}}'; exit 1; fi +query="${1}" shift # Parse flags diff --git a/tools/gui_input b/tools/gui_input index 1a78d3e..006b374 100755 --- a/tools/gui_input +++ b/tools/gui_input @@ -14,7 +14,8 @@ # ollie:end set -euo pipefail -cmd="${1:?usage: gui_input [args...]}" +if [ $# -lt 1 ]; then echo '{"schema": {"type": "array", "items": [{"name": "subcommand", "type": "string", "description": "key|type|click|move|scroll", "required": true}, {"name": "args", "type": "array", "description": "Subcommand-specific arguments", "required": true}], "examples": [["key", "ctrl+s"], ["type", "hello"], ["click", "left"], ["move", "100", "200"], ["scroll", "down", "3"]]}}'; exit 1; fi +cmd="${1}" shift # Detect input method: prefer ydotool (Wayland-safe), fall back to xdotool @@ -30,7 +31,8 @@ fi case "$cmd" in key) # Simulate key press. Examples: "Return", "ctrl+c", "alt+F4", "super" - keys="${1:?usage: gui_input key }" + if [ $# -lt 1 ]; then echo '{"schema": {"type": "array", "items": [{"name": "subcommand", "type": "string", "description": "key|type|click|move|scroll", "required": true}, {"name": "args", "type": "array", "description": "Subcommand-specific arguments", "required": true}], "examples": [["key", "ctrl+s"], ["type", "hello"], ["click", "left"], ["move", "100", "200"], ["scroll", "down", "3"]]}}'; exit 1; fi + keys="${1}" if [ "$INPUT_TOOL" = "ydotool" ]; then # ydotool uses key codes; map common names ydotool key "$keys" @@ -41,7 +43,8 @@ case "$cmd" in ;; type) # Type a string - text="${1:?usage: gui_input type }" + if [ $# -lt 1 ]; then echo '{"schema": {"type": "array", "items": [{"name": "subcommand", "type": "string", "description": "key|type|click|move|scroll", "required": true}, {"name": "args", "type": "array", "description": "Subcommand-specific arguments", "required": true}], "examples": [["key", "ctrl+s"], ["type", "hello"], ["click", "left"], ["move", "100", "200"], ["scroll", "down", "3"]]}}'; exit 1; fi + text="${1}" if [ "$INPUT_TOOL" = "ydotool" ]; then ydotool type -- "$text" else @@ -75,8 +78,10 @@ case "$cmd" in ;; move) # Move mouse. Args: [--relative] - x="${1:?usage: gui_input move [--relative]}" - y="${2:?usage: gui_input move [--relative]}" + if [ $# -lt 1 ]; then echo '{"schema": {"type": "array", "items": [{"name": "subcommand", "type": "string", "description": "key|type|click|move|scroll", "required": true}, {"name": "args", "type": "array", "description": "Subcommand-specific arguments", "required": true}], "examples": [["key", "ctrl+s"], ["type", "hello"], ["click", "left"], ["move", "100", "200"], ["scroll", "down", "3"]]}}'; exit 1; fi + x="${1}" + if [ $# -lt 2 ]; then echo '{"schema": {"type": "array", "items": [{"name": "subcommand", "type": "string", "description": "key|type|click|move|scroll", "required": true}, {"name": "args", "type": "array", "description": "Subcommand-specific arguments", "required": true}], "examples": [["key", "ctrl+s"], ["type", "hello"], ["click", "left"], ["move", "100", "200"], ["scroll", "down", "3"]]}}'; exit 1; fi + y="${2}" relative="${3:-}" if [ "$INPUT_TOOL" = "ydotool" ]; then if [ "$relative" = "--relative" ]; then @@ -95,7 +100,8 @@ case "$cmd" in ;; scroll) # Scroll. Args: [amount] - direction="${1:?usage: gui_input scroll [amount]}" + if [ $# -lt 1 ]; then echo '{"schema": {"type": "array", "items": [{"name": "subcommand", "type": "string", "description": "key|type|click|move|scroll", "required": true}, {"name": "args", "type": "array", "description": "Subcommand-specific arguments", "required": true}], "examples": [["key", "ctrl+s"], ["type", "hello"], ["click", "left"], ["move", "100", "200"], ["scroll", "down", "3"]]}}'; exit 1; fi + direction="${1}" amount="${2:-3}" if [ "$INPUT_TOOL" = "ydotool" ]; then case "$direction" in @@ -115,7 +121,7 @@ case "$cmd" in echo "scroll: $direction $amount" ;; *) - echo "usage: gui_input [args...]" >&2 + echo '{"schema": {"type": "array", "items": [{"name": "subcommand", "type": "string", "description": "key|type|click|move|scroll", "required": true}, {"name": "args", "type": "array", "description": "Subcommand-specific arguments", "required": true}], "examples": [["key", "ctrl+s"], ["type", "hello"], ["click", "left"], ["move", "100", "200"], ["scroll", "down", "3"]]}}' >&2 echo "" >&2 echo "Commands:" >&2 echo " key - press key combo (e.g. ctrl+c, Return, alt+F4)" >&2 diff --git a/tools/gui_media b/tools/gui_media index 192a01f..f347dd8 100755 --- a/tools/gui_media +++ b/tools/gui_media @@ -256,8 +256,14 @@ def build_playlist(dest, description, root, count, output=None): print(f"{index:02d}. {track['artist']} — {track['title']}") +class _Schema(argparse.ArgumentParser): + def error(self, msg): + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|status|open|playlist|play|pause|stop|next|previous", "required": true}, {"name": "target", "type": "string", "description": "Target file or player", "required": false}], "examples": [["status"], ["open", "/path/to/song.mp3"]]}}') + sys.exit(1) + + def main(): - ap = argparse.ArgumentParser() + ap = _Schema() ap.add_argument("command", choices=["list", "status", "open", "playlist", "play", "pause", "stop", "next", "previous"]) ap.add_argument("target", nargs="?", help="path, URI, or playlist description") diff --git a/tools/gui_notify b/tools/gui_notify index 53cd8a6..a343bb2 100755 --- a/tools/gui_notify +++ b/tools/gui_notify @@ -20,8 +20,14 @@ import argparse import subprocess import json +class _Schema(argparse.ArgumentParser): + def error(self, msg): + print('{"schema": {"type": "array", "items": [{"name": "summary", "type": "string", "description": "Notification title", "required": true}, {"name": "body", "type": "string", "description": "Notification body", "required": false}, {"name": "--timeout=MS", "type": "string", "description": "Timeout in ms", "required": false}, {"name": "--actions=LABELS", "type": "string", "description": "Comma-separated actions", "required": false}], "examples": [["Build complete", "All tests passed"]]}}') + sys.exit(1) + + def parse_args(): - p = argparse.ArgumentParser(description="Desktop notification with optional actions") + p = _Schema(description="Desktop notification with optional actions") p.add_argument("summary", help="Notification title") p.add_argument("body", nargs="?", default="", help="Notification body text") p.add_argument("--actions", default="", help="Comma-separated action labels") diff --git a/tools/gui_screenshot b/tools/gui_screenshot index 3d0af7d..dfce675 100755 --- a/tools/gui_screenshot +++ b/tools/gui_screenshot @@ -34,8 +34,14 @@ OBJ_PATH = "/org/kde/KWin/ScreenShot2" IFACE = "org.kde.KWin.ScreenShot2" +class _Schema(argparse.ArgumentParser): + def error(self, msg): + print('{"schema": {"type": "array", "items": [{"name": "mode", "type": "string", "description": "active|screen|area|window", "required": false}, {"name": "--output=PATH", "type": "string", "description": "Output path", "required": false}, {"name": "--title=SUBSTR", "type": "string", "description": "Window title", "required": false}], "examples": [["active"], ["screen", "--output=/tmp/shot.png"]]}}') + sys.exit(1) + + def parse_args(): - p = argparse.ArgumentParser(description="KDE screenshot via KWin D-Bus") + p = _Schema(description="KDE screenshot via KWin D-Bus") p.add_argument("mode", nargs="?", default="active", choices=["active", "screen", "area", "window"], help="Capture mode (default: active)") diff --git a/tools/gui_shortcuts b/tools/gui_shortcuts index ace1da3..614afa3 100755 --- a/tools/gui_shortcuts +++ b/tools/gui_shortcuts @@ -51,8 +51,10 @@ case "$cmd" in ;; get) # Get key binding for a shortcut - comp="${2:?usage: gui_shortcuts get }" - name="${3:?usage: gui_shortcuts get }" + if [ $# -lt 2 ]; then echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|get|trigger|set", "required": true}, {"name": "component", "type": "string", "description": "Component name (for get/trigger/set)", "required": false}, {"name": "shortcut", "type": "string", "description": "Shortcut name (for get/trigger/set)", "required": false}, {"name": "keys", "type": "string", "description": "Key combination (for set)", "required": false}], "examples": [["list"], ["get", "kwin", "switch window"], ["trigger", "kwin", "switch window"]]}}'; exit 1; fi + comp="${2}" + if [ $# -lt 3 ]; then echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|get|trigger|set", "required": true}, {"name": "component", "type": "string", "description": "Component name (for get/trigger/set)", "required": false}, {"name": "shortcut", "type": "string", "description": "Shortcut name (for get/trigger/set)", "required": false}, {"name": "keys", "type": "string", "description": "Key combination (for set)", "required": false}], "examples": [["list"], ["get", "kwin", "switch window"], ["trigger", "kwin", "switch window"]]}}'; exit 1; fi + name="${3}" gdbus call --session --dest "$SERVICE" \ --object-path "$OBJ_PATH" \ @@ -61,8 +63,10 @@ case "$cmd" in ;; trigger) # Trigger (invoke) a global shortcut by component/name - comp="${2:?usage: gui_shortcuts trigger }" - name="${3:?usage: gui_shortcuts trigger }" + if [ $# -lt 2 ]; then echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|get|trigger|set", "required": true}, {"name": "component", "type": "string", "description": "Component name (for get/trigger/set)", "required": false}, {"name": "shortcut", "type": "string", "description": "Shortcut name (for get/trigger/set)", "required": false}, {"name": "keys", "type": "string", "description": "Key combination (for set)", "required": false}], "examples": [["list"], ["get", "kwin", "switch window"], ["trigger", "kwin", "switch window"]]}}'; exit 1; fi + comp="${2}" + if [ $# -lt 3 ]; then echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|get|trigger|set", "required": true}, {"name": "component", "type": "string", "description": "Component name (for get/trigger/set)", "required": false}, {"name": "shortcut", "type": "string", "description": "Shortcut name (for get/trigger/set)", "required": false}, {"name": "keys", "type": "string", "description": "Key combination (for set)", "required": false}], "examples": [["list"], ["get", "kwin", "switch window"], ["trigger", "kwin", "switch window"]]}}'; exit 1; fi + name="${3}" # First get the shortcut keys, then simulate them # Or use the invokeShortcut method if available @@ -78,9 +82,12 @@ case "$cmd" in ;; set) # Set key binding for a shortcut - comp="${2:?usage: gui_shortcuts set }" - name="${3:?usage: gui_shortcuts set }" - keys="${4:?usage: gui_shortcuts set }" + if [ $# -lt 2 ]; then echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|get|trigger|set", "required": true}, {"name": "component", "type": "string", "description": "Component name (for get/trigger/set)", "required": false}, {"name": "shortcut", "type": "string", "description": "Shortcut name (for get/trigger/set)", "required": false}, {"name": "keys", "type": "string", "description": "Key combination (for set)", "required": false}], "examples": [["list"], ["get", "kwin", "switch window"], ["trigger", "kwin", "switch window"]]}}'; exit 1; fi + comp="${2}" + if [ $# -lt 3 ]; then echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|get|trigger|set", "required": true}, {"name": "component", "type": "string", "description": "Component name (for get/trigger/set)", "required": false}, {"name": "shortcut", "type": "string", "description": "Shortcut name (for get/trigger/set)", "required": false}, {"name": "keys", "type": "string", "description": "Key combination (for set)", "required": false}], "examples": [["list"], ["get", "kwin", "switch window"], ["trigger", "kwin", "switch window"]]}}'; exit 1; fi + name="${3}" + if [ $# -lt 4 ]; then echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|get|trigger|set", "required": true}, {"name": "component", "type": "string", "description": "Component name (for get/trigger/set)", "required": false}, {"name": "shortcut", "type": "string", "description": "Shortcut name (for get/trigger/set)", "required": false}, {"name": "keys", "type": "string", "description": "Key combination (for set)", "required": false}], "examples": [["list"], ["get", "kwin", "switch window"], ["trigger", "kwin", "switch window"]]}}'; exit 1; fi + keys="${4}" gdbus call --session --dest "$SERVICE" \ --object-path "$OBJ_PATH" \ @@ -95,7 +102,7 @@ case "$cmd" in exit 1 ;; *) - echo "usage: gui_shortcuts [component] [shortcut] [keys]" >&2 + echo '{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|get|trigger|set", "required": true}, {"name": "component", "type": "string", "description": "Component name (for get/trigger/set)", "required": false}, {"name": "shortcut", "type": "string", "description": "Shortcut name (for get/trigger/set)", "required": false}, {"name": "keys", "type": "string", "description": "Key combination (for set)", "required": false}], "examples": [["list"], ["get", "kwin", "switch window"], ["trigger", "kwin", "switch window"]]}}' >&2 echo "" >&2 echo "Examples:" >&2 echo " gui_shortcuts list - list all components" >&2 diff --git a/tools/gui_windows b/tools/gui_windows index bb7b8be..413cfde 100755 --- a/tools/gui_windows +++ b/tools/gui_windows @@ -373,7 +373,7 @@ def switch_desktop(num): def main(): if len(sys.argv) < 2: - print("usage: gui_windows [args...]", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|focus|close|minimize|maximize|move|resize|desktops|desktop", "required": true}, {"name": "args...", "type": "string", "description": "Action-specific arguments", "required": false}], "examples": [["list"], ["focus", "Firefox"]]}}') sys.exit(1) cmd = sys.argv[1] @@ -382,39 +382,39 @@ def main(): list_windows() elif cmd == "focus": if len(sys.argv) < 3: - print("usage: gui_windows focus ", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|focus|close|minimize|maximize|move|resize|desktops|desktop", "required": true}, {"name": "args...", "type": "string", "description": "Action-specific arguments", "required": false}], "examples": [["list"], ["focus", "Firefox"]]}}') sys.exit(1) focus_window(sys.argv[2]) elif cmd == "close": if len(sys.argv) < 3: - print("usage: gui_windows close ", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|focus|close|minimize|maximize|move|resize|desktops|desktop", "required": true}, {"name": "args...", "type": "string", "description": "Action-specific arguments", "required": false}], "examples": [["list"], ["focus", "Firefox"]]}}') sys.exit(1) close_window(sys.argv[2]) elif cmd == "minimize": if len(sys.argv) < 3: - print("usage: gui_windows minimize ", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|focus|close|minimize|maximize|move|resize|desktops|desktop", "required": true}, {"name": "args...", "type": "string", "description": "Action-specific arguments", "required": false}], "examples": [["list"], ["focus", "Firefox"]]}}') sys.exit(1) minimize_window(sys.argv[2]) elif cmd == "maximize": if len(sys.argv) < 3: - print("usage: gui_windows maximize ", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|focus|close|minimize|maximize|move|resize|desktops|desktop", "required": true}, {"name": "args...", "type": "string", "description": "Action-specific arguments", "required": false}], "examples": [["list"], ["focus", "Firefox"]]}}') sys.exit(1) maximize_window(sys.argv[2]) elif cmd == "move": if len(sys.argv) < 5: - print("usage: gui_windows move ", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|focus|close|minimize|maximize|move|resize|desktops|desktop", "required": true}, {"name": "args...", "type": "string", "description": "Action-specific arguments", "required": false}], "examples": [["list"], ["focus", "Firefox"]]}}') sys.exit(1) move_window(sys.argv[2], sys.argv[3], sys.argv[4]) elif cmd == "resize": if len(sys.argv) < 5: - print("usage: gui_windows resize ", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|focus|close|minimize|maximize|move|resize|desktops|desktop", "required": true}, {"name": "args...", "type": "string", "description": "Action-specific arguments", "required": false}], "examples": [["list"], ["focus", "Firefox"]]}}') sys.exit(1) resize_window(sys.argv[2], sys.argv[3], sys.argv[4]) elif cmd == "desktops": list_desktops() elif cmd == "desktop": if len(sys.argv) < 3: - print("usage: gui_windows desktop ", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "action", "type": "string", "description": "list|focus|close|minimize|maximize|move|resize|desktops|desktop", "required": true}, {"name": "args...", "type": "string", "description": "Action-specific arguments", "required": false}], "examples": [["list"], ["focus", "Firefox"]]}}') sys.exit(1) switch_desktop(sys.argv[2]) else: diff --git a/tools/image_read b/tools/image_read index c5c32b5..28ad476 100644 --- a/tools/image_read +++ b/tools/image_read @@ -25,7 +25,7 @@ for a in sys.argv[1:]: file_path = a if not file_path: - print("usage: image_read ") + print('{"schema": {"type": "array", "items": [{"name": "file_path", "type": "string", "description": "Absolute path to image file", "required": true}, {"name": "--region=x,y,w,h", "type": "string", "description": "Region to capture (optional)", "required": false}, {"name": "--scale=N", "type": "string", "description": "Scale factor (optional)", "required": false}], "examples": [["/abs/path/image.png"], ["/abs/path/image.png", "--region=0,0,100,100"]]}}') sys.exit(1) if not os.path.isabs(file_path): diff --git a/tools/lsp_completion b/tools/lsp_completion index b311eec..ba460a0 100755 --- a/tools/lsp_completion +++ b/tools/lsp_completion @@ -21,7 +21,7 @@ sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.ab from _lib.lsp.client import request if len(sys.argv) < 4: - print("usage: lsp_completion ") + print('{"schema": {"type": "array", "items": [{"name": "file", "type": "string", "description": "File path", "required": true}, {"name": "line", "type": "string", "description": "Line number (1-indexed)", "required": true}, {"name": "col", "type": "string", "description": "Column number", "required": true}], "examples": [["/path/to/file.go", "42", "5"]]}}') sys.exit(1) filepath = os.path.abspath(sys.argv[1]) diff --git a/tools/lsp_definition b/tools/lsp_definition index 87cc08d..4250869 100755 --- a/tools/lsp_definition +++ b/tools/lsp_definition @@ -21,7 +21,7 @@ sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.ab from _lib.lsp.client import request if len(sys.argv) < 4: - print("usage: lsp_definition ") + print('{"schema": {"type": "array", "items": [{"name": "file", "type": "string", "description": "File path", "required": true}, {"name": "line", "type": "string", "description": "Line number (1-indexed)", "required": true}, {"name": "col", "type": "string", "description": "Column number", "required": true}], "examples": [["/path/to/file.go", "42", "5"]]}}') sys.exit(1) filepath = os.path.abspath(sys.argv[1]) diff --git a/tools/lsp_diagnostics b/tools/lsp_diagnostics index 407f952..a682ded 100755 --- a/tools/lsp_diagnostics +++ b/tools/lsp_diagnostics @@ -19,7 +19,7 @@ sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.ab from _lib.lsp.client import request if len(sys.argv) < 2: - print("usage: lsp_diagnostics ") + print('{"schema": {"type": "array", "items": [{"name": "file", "type": "string", "description": "File path", "required": true}], "examples": [["/path/to/file.go"]]}}') sys.exit(1) filepath = os.path.abspath(sys.argv[1]) diff --git a/tools/lsp_hover b/tools/lsp_hover index 65d9c87..162193e 100755 --- a/tools/lsp_hover +++ b/tools/lsp_hover @@ -19,7 +19,7 @@ sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.ab from _lib.lsp.client import request if len(sys.argv) < 4: - print("usage: lsp_hover ") + print('{"schema": {"type": "array", "items": [{"name": "file", "type": "string", "description": "File path", "required": true}, {"name": "line", "type": "string", "description": "Line number (1-indexed)", "required": true}, {"name": "col", "type": "string", "description": "Column number", "required": true}], "examples": [["/path/to/file.go", "42", "5"]]}}') sys.exit(1) filepath = os.path.abspath(sys.argv[1]) diff --git a/tools/lsp_references b/tools/lsp_references index e591639..8124655 100755 --- a/tools/lsp_references +++ b/tools/lsp_references @@ -23,7 +23,7 @@ sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.ab from _lib.lsp.client import request if len(sys.argv) < 4: - print("usage: lsp_references ") + print('{"schema": {"type": "array", "items": [{"name": "file", "type": "string", "description": "File path", "required": true}, {"name": "line", "type": "string", "description": "Line number (1-indexed)", "required": true}, {"name": "col", "type": "string", "description": "Column number", "required": true}], "examples": [["/path/to/file.go", "42", "5"]]}}') sys.exit(1) filepath = os.path.abspath(sys.argv[1]) diff --git a/tools/lsp_rename b/tools/lsp_rename index 098979e..2b8e09b 100755 --- a/tools/lsp_rename +++ b/tools/lsp_rename @@ -20,7 +20,7 @@ sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.ab from _lib.lsp.client import request if len(sys.argv) < 5: - print("usage: lsp_rename ") + print('{"schema": {"type": "array", "items": [{"name": "file", "type": "string", "description": "File path", "required": true}, {"name": "line", "type": "string", "description": "Line number (1-indexed)", "required": true}, {"name": "col", "type": "string", "description": "Column number", "required": true}, {"name": "new_name", "type": "string", "description": "New symbol name", "required": true}], "examples": [["/path/to/file.go", "42", "5", "newName"]]}}') sys.exit(1) filepath = os.path.abspath(sys.argv[1]) diff --git a/tools/lsp_symbols b/tools/lsp_symbols index 85c034c..5eab448 100755 --- a/tools/lsp_symbols +++ b/tools/lsp_symbols @@ -20,7 +20,7 @@ sys.path.insert(0, os.environ.get('OLLIE_TOOLS_PATH', os.path.dirname(os.path.ab from _lib.lsp.client import request if len(sys.argv) < 2: - print("usage: lsp_symbols [--workspace]") + print('{"schema": {"type": "array", "items": [{"name": "file_or_query", "type": "string", "description": "File path or search query", "required": true}, {"name": "--workspace", "type": "string", "description": "Search entire workspace", "required": false}], "examples": [["/path/to/file.go"], ["function_name", "--workspace"]]}}') sys.exit(1) workspace = "--workspace" in sys.argv diff --git a/tools/memory_recall b/tools/memory_recall index f508314..1517e7f 100755 --- a/tools/memory_recall +++ b/tools/memory_recall @@ -21,7 +21,7 @@ # - If recall returns nothing relevant, proceed without it. # ollie:end -[ -z "$*" ] && echo "usage: memory_recall " && exit 1 +[ -z "$*" ] && echo '{"schema": {"type": "array", "items": [{"name": "query", "type": "string", "description": "Search keyword or phrase", "required": true}], "examples": [["architecture"], ["config"]]}}' && exit 1 mem_dir="${OLLIE_MEMORY_PATH:-$HOME/.config/ollie/memory}" diff --git a/tools/memory_remember b/tools/memory_remember index c8e70bd..c3101db 100755 --- a/tools/memory_remember +++ b/tools/memory_remember @@ -30,7 +30,7 @@ import re from datetime import datetime if len(sys.argv) < 4: - print("usage: memory_remember <tags_csv> <body>") + print('{"schema": {"type": "array", "items": [{"name": "title", "type": "string", "description": "Short noun phrase", "required": true}, {"name": "tags_csv", "type": "string", "description": "Comma-separated tags", "required": true}, {"name": "body", "type": "string", "description": "The fact to remember", "required": true}], "examples": [["API endpoint", "api,backend", "The /users endpoint returns..."]]}}') sys.exit(1) title = sys.argv[1] diff --git a/tools/process_dismiss b/tools/process_dismiss index 39cbb6f..d721b13 100755 --- a/tools/process_dismiss +++ b/tools/process_dismiss @@ -13,7 +13,8 @@ set -euo pipefail -pid="${1:?usage: process_dismiss <pid>}" +if [ $# -lt 1 ]; then echo '{"schema": {"type": "array", "items": [{"name": "pid", "type": "string", "description": "Process PID", "required": true}], "examples": [["12345"]]}}'; exit 1; fi +pid="${1}" session="${OLLIE_SESSION_ID:?OLLIE_SESSION_ID not set}" dbus-send --session --dest=org.ollie.SessionManager --type=method_call --print-reply \ diff --git a/tools/process_output b/tools/process_output index 2fe18b5..353e442 100755 --- a/tools/process_output +++ b/tools/process_output @@ -14,7 +14,8 @@ set -euo pipefail -pid="${1:?usage: process_output <pid>}" +if [ $# -lt 1 ]; then echo '{"schema": {"type": "array", "items": [{"name": "pid", "type": "string", "description": "Process PID", "required": true}], "examples": [["12345"]]}}'; exit 1; fi +pid="${1}" session="${OLLIE_SESSION_ID:?OLLIE_SESSION_ID not set}" # Use gdbus for proper parsing of string with newlines diff --git a/tools/process_signal b/tools/process_signal index 3424f1e..e299caa 100755 --- a/tools/process_signal +++ b/tools/process_signal @@ -14,7 +14,8 @@ set -euo pipefail -pid="${1:?usage: process_signal <pid> [TERM|KILL]}" +if [ $# -lt 1 ]; then echo '{"schema": {"type": "array", "items": [{"name": "pid", "type": "string", "description": "Process PID", "required": true}, {"name": "signal", "type": "string", "description": "Signal name (TERM or KILL, default: TERM)", "required": false}], "examples": [["12345", "TERM"], ["12345"]]}}'; exit 1; fi +pid="${1}" signal="${2:-TERM}" session="${OLLIE_SESSION_ID:?OLLIE_SESSION_ID not set}" diff --git a/tools/reasoning_think b/tools/reasoning_think index 62d5500..27fab53 100755 --- a/tools/reasoning_think +++ b/tools/reasoning_think @@ -20,5 +20,5 @@ # - Keep under 200 words. State the decision and key tradeoffs, not stream-of-consciousness. # ollie:end -[ -z "$*" ] && echo "usage: reasoning_think <thought>" && exit 1 +[ -z "$*" ] && echo '{"schema": {"type": "array", "items": [{"name": "thought", "type": "string", "description": "Your reasoning", "required": true}], "examples": [["Need to check if config file exists first"]]}}' && exit 1 exit 0 diff --git a/tools/route b/tools/route index 26edcfc..b8ac57e 100755 --- a/tools/route +++ b/tools/route @@ -18,7 +18,7 @@ SERVICE="org.ollie.SessionManager" OBJ="/org/ollie/SessionManager" IFACE="org.ollie.SessionManager" -die() { printf 'route: %s\n' "$*" >&2; exit 1; } +die() { echo '{"schema": {"type": "array", "items": [{"name": "instruction", "type": "string", "description": "Task description to route", "required": true}, {"name": "--tier=worker|expert", "type": "string", "description": "Model tier (optional)", "required": false}, {"name": "--backend=<name>", "type": "string", "description": "Backend override (optional)", "required": false}], "examples": [["implement login page"], ["refactor auth", "--tier=expert"]]}}'; exit 1; } dbus_call() { dbus-send --session --dest=$SERVICE --type=method_call --print-reply \ diff --git a/tools/subagent_generate b/tools/subagent_generate index d18c8c4..eeeddeb 100755 --- a/tools/subagent_generate +++ b/tools/subagent_generate @@ -33,7 +33,7 @@ TEMPLATE = { def die(msg): - print(f"subagent_generate: {msg}", file=sys.stderr) + print('{"schema": {"type": "array", "items": [{"name": "json_config", "type": "string", "description": "Optional JSON config. Empty = prints template", "required": false}], "examples": [[], ["{\"role\": \"backend dev\"}"]]}}') sys.exit(1) diff --git a/tools/subagent_spawn b/tools/subagent_spawn index b882fcd..8791be6 100755 --- a/tools/subagent_spawn +++ b/tools/subagent_spawn @@ -19,7 +19,7 @@ SERVICE="org.ollie.SessionManager" OBJ="/org/ollie/SessionManager" IFACE="org.ollie.SessionManager" -die() { printf 'subagent_spawn: %s\n' "$*" >&2; exit 1; } +die() { echo '{"schema": {"type": "array", "items": [{"name": "prompt", "type": "string", "description": "Task prompt for the subagent", "required": true}, {"name": "-name", "type": "string", "description": "Subagent name (optional)", "required": false}, {"name": "-agent", "type": "string", "description": "Agent config (optional)", "required": false}, {"name": "-backend", "type": "string", "description": "Backend (optional)", "required": false}, {"name": "-model", "type": "string", "description": "Model (optional)", "required": false}], "examples": [["-name", "worker", "implement login page"], ["fix tests"]]}}'; exit 1; } dbus_call() { dbus-send --session --dest=$SERVICE --type=method_call --print-reply \