echo-agent 0.1.0__py3-none-any.whl

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 (219) hide show
  1. echo_agent/__init__.py +5 -0
  2. echo_agent/__main__.py +538 -0
  3. echo_agent/_bundled/skills/development/plan/SKILL.md +54 -0
  4. echo_agent/_bundled/skills/development/skill-creator/SKILL.md +270 -0
  5. echo_agent/_bundled/skills/development/skill-creator/scripts/init_skill.py +226 -0
  6. echo_agent/_bundled/skills/development/skill-creator/scripts/package_skill.py +146 -0
  7. echo_agent/_bundled/skills/development/skill-creator/scripts/quick_validate.py +222 -0
  8. echo_agent/_bundled/skills/productivity/summarize/SKILL.md +67 -0
  9. echo_agent/_bundled/skills/productivity/weather/SKILL.md +49 -0
  10. echo_agent/_bundled/skills/research/arxiv/SKILL.md +232 -0
  11. echo_agent/_bundled/skills/research/arxiv/scripts/search_arxiv.py +115 -0
  12. echo_agent/a2a/__init__.py +5 -0
  13. echo_agent/a2a/client.py +66 -0
  14. echo_agent/a2a/models.py +98 -0
  15. echo_agent/a2a/protocol.py +85 -0
  16. echo_agent/a2a/server.py +71 -0
  17. echo_agent/agent/__init__.py +0 -0
  18. echo_agent/agent/approval_gate.py +326 -0
  19. echo_agent/agent/compression/__init__.py +14 -0
  20. echo_agent/agent/compression/assembler.py +45 -0
  21. echo_agent/agent/compression/boundary.py +141 -0
  22. echo_agent/agent/compression/compressor.py +181 -0
  23. echo_agent/agent/compression/engine.py +88 -0
  24. echo_agent/agent/compression/pruner.py +150 -0
  25. echo_agent/agent/compression/summarizer.py +181 -0
  26. echo_agent/agent/compression/types.py +41 -0
  27. echo_agent/agent/compression/validator.py +96 -0
  28. echo_agent/agent/consolidation.py +96 -0
  29. echo_agent/agent/context.py +403 -0
  30. echo_agent/agent/executors/__init__.py +0 -0
  31. echo_agent/agent/executors/base.py +211 -0
  32. echo_agent/agent/executors/factory.py +34 -0
  33. echo_agent/agent/executors/remote.py +193 -0
  34. echo_agent/agent/loop.py +891 -0
  35. echo_agent/agent/multi_agent/__init__.py +15 -0
  36. echo_agent/agent/multi_agent/audit.py +19 -0
  37. echo_agent/agent/multi_agent/error_messages.py +35 -0
  38. echo_agent/agent/multi_agent/error_types.py +36 -0
  39. echo_agent/agent/multi_agent/models.py +37 -0
  40. echo_agent/agent/multi_agent/registry.py +41 -0
  41. echo_agent/agent/multi_agent/runtime.py +201 -0
  42. echo_agent/agent/pipeline/__init__.py +14 -0
  43. echo_agent/agent/pipeline/context_stage.py +219 -0
  44. echo_agent/agent/pipeline/inference_stage.py +433 -0
  45. echo_agent/agent/pipeline/response_stage.py +146 -0
  46. echo_agent/agent/pipeline/types.py +40 -0
  47. echo_agent/agent/planning/__init__.py +4 -0
  48. echo_agent/agent/planning/models.py +83 -0
  49. echo_agent/agent/planning/planner.py +57 -0
  50. echo_agent/agent/planning/reflection.py +54 -0
  51. echo_agent/agent/planning/strategies.py +183 -0
  52. echo_agent/agent/tools/__init__.py +167 -0
  53. echo_agent/agent/tools/base.py +149 -0
  54. echo_agent/agent/tools/circuit_breaker.py +82 -0
  55. echo_agent/agent/tools/clarify.py +42 -0
  56. echo_agent/agent/tools/code_exec.py +147 -0
  57. echo_agent/agent/tools/cronjob.py +93 -0
  58. echo_agent/agent/tools/delegate.py +393 -0
  59. echo_agent/agent/tools/filesystem.py +180 -0
  60. echo_agent/agent/tools/image_gen.py +65 -0
  61. echo_agent/agent/tools/knowledge.py +81 -0
  62. echo_agent/agent/tools/memory.py +198 -0
  63. echo_agent/agent/tools/message.py +39 -0
  64. echo_agent/agent/tools/notify.py +35 -0
  65. echo_agent/agent/tools/patch.py +178 -0
  66. echo_agent/agent/tools/process.py +139 -0
  67. echo_agent/agent/tools/registry.py +185 -0
  68. echo_agent/agent/tools/search.py +99 -0
  69. echo_agent/agent/tools/session_search.py +76 -0
  70. echo_agent/agent/tools/shell.py +164 -0
  71. echo_agent/agent/tools/skill_install.py +255 -0
  72. echo_agent/agent/tools/skills.py +177 -0
  73. echo_agent/agent/tools/task.py +104 -0
  74. echo_agent/agent/tools/todo.py +148 -0
  75. echo_agent/agent/tools/tts.py +77 -0
  76. echo_agent/agent/tools/vision.py +71 -0
  77. echo_agent/agent/tools/web.py +208 -0
  78. echo_agent/agent/tools/workflow.py +89 -0
  79. echo_agent/bus/__init__.py +11 -0
  80. echo_agent/bus/events.py +193 -0
  81. echo_agent/bus/queue.py +158 -0
  82. echo_agent/bus/rate_limiter.py +51 -0
  83. echo_agent/channels/__init__.py +0 -0
  84. echo_agent/channels/base.py +185 -0
  85. echo_agent/channels/cli.py +149 -0
  86. echo_agent/channels/cron.py +44 -0
  87. echo_agent/channels/dingtalk.py +195 -0
  88. echo_agent/channels/discord.py +359 -0
  89. echo_agent/channels/email.py +168 -0
  90. echo_agent/channels/feishu.py +240 -0
  91. echo_agent/channels/manager.py +417 -0
  92. echo_agent/channels/matrix.py +281 -0
  93. echo_agent/channels/qqbot.py +638 -0
  94. echo_agent/channels/qqbot_media.py +482 -0
  95. echo_agent/channels/slack.py +297 -0
  96. echo_agent/channels/telegram.py +275 -0
  97. echo_agent/channels/webhook.py +106 -0
  98. echo_agent/channels/wecom.py +152 -0
  99. echo_agent/channels/weixin.py +603 -0
  100. echo_agent/channels/whatsapp.py +138 -0
  101. echo_agent/cli/__init__.py +0 -0
  102. echo_agent/cli/colors.py +42 -0
  103. echo_agent/cli/evolution_cmd.py +299 -0
  104. echo_agent/cli/i18n/__init__.py +123 -0
  105. echo_agent/cli/i18n/en.py +275 -0
  106. echo_agent/cli/i18n/zh.py +275 -0
  107. echo_agent/cli/plugins_cmd.py +205 -0
  108. echo_agent/cli/prompt.py +102 -0
  109. echo_agent/cli/service.py +156 -0
  110. echo_agent/cli/setup.py +1111 -0
  111. echo_agent/cli/status.py +93 -0
  112. echo_agent/config/__init__.py +8 -0
  113. echo_agent/config/default.yaml +199 -0
  114. echo_agent/config/loader.py +125 -0
  115. echo_agent/config/schema.py +652 -0
  116. echo_agent/evaluation/__init__.py +4 -0
  117. echo_agent/evaluation/dataset.py +66 -0
  118. echo_agent/evaluation/metrics.py +70 -0
  119. echo_agent/evaluation/reporter.py +42 -0
  120. echo_agent/evaluation/runner.py +143 -0
  121. echo_agent/evolution/__init__.py +38 -0
  122. echo_agent/evolution/engine.py +335 -0
  123. echo_agent/evolution/evolver.py +397 -0
  124. echo_agent/evolution/gate.py +413 -0
  125. echo_agent/evolution/recorder.py +288 -0
  126. echo_agent/evolution/scheduler.py +133 -0
  127. echo_agent/evolution/store.py +331 -0
  128. echo_agent/evolution/tools.py +110 -0
  129. echo_agent/evolution/types.py +270 -0
  130. echo_agent/gateway/__init__.py +7 -0
  131. echo_agent/gateway/auth.py +178 -0
  132. echo_agent/gateway/editor.py +121 -0
  133. echo_agent/gateway/health.py +51 -0
  134. echo_agent/gateway/hooks.py +86 -0
  135. echo_agent/gateway/media.py +137 -0
  136. echo_agent/gateway/rate_limiter.py +72 -0
  137. echo_agent/gateway/router.py +86 -0
  138. echo_agent/gateway/server.py +570 -0
  139. echo_agent/gateway/session_context.py +57 -0
  140. echo_agent/gateway/session_policy.py +47 -0
  141. echo_agent/gateway/static/index.html +432 -0
  142. echo_agent/knowledge/__init__.py +5 -0
  143. echo_agent/knowledge/index.py +308 -0
  144. echo_agent/mcp/__init__.py +3 -0
  145. echo_agent/mcp/client.py +158 -0
  146. echo_agent/mcp/manager.py +161 -0
  147. echo_agent/mcp/oauth.py +208 -0
  148. echo_agent/mcp/security.py +79 -0
  149. echo_agent/mcp/tool_adapter.py +73 -0
  150. echo_agent/mcp/transport.py +353 -0
  151. echo_agent/memory/__init__.py +0 -0
  152. echo_agent/memory/consolidator.py +273 -0
  153. echo_agent/memory/contradiction.py +287 -0
  154. echo_agent/memory/forgetting.py +114 -0
  155. echo_agent/memory/retrieval.py +184 -0
  156. echo_agent/memory/reviewer.py +192 -0
  157. echo_agent/memory/store.py +706 -0
  158. echo_agent/memory/tiers.py +243 -0
  159. echo_agent/memory/types.py +168 -0
  160. echo_agent/memory/vectors.py +148 -0
  161. echo_agent/models/__init__.py +0 -0
  162. echo_agent/models/credential_pool.py +86 -0
  163. echo_agent/models/inference.py +98 -0
  164. echo_agent/models/provider.py +208 -0
  165. echo_agent/models/providers/__init__.py +209 -0
  166. echo_agent/models/providers/anthropic_provider.py +164 -0
  167. echo_agent/models/providers/bedrock_provider.py +261 -0
  168. echo_agent/models/providers/format_utils.py +198 -0
  169. echo_agent/models/providers/gemini_provider.py +159 -0
  170. echo_agent/models/providers/openai_provider.py +253 -0
  171. echo_agent/models/providers/openrouter_provider.py +38 -0
  172. echo_agent/models/rate_limiter.py +75 -0
  173. echo_agent/models/router.py +325 -0
  174. echo_agent/models/tokenizer.py +111 -0
  175. echo_agent/observability/__init__.py +0 -0
  176. echo_agent/observability/monitor.py +209 -0
  177. echo_agent/observability/spans.py +75 -0
  178. echo_agent/observability/telemetry.py +86 -0
  179. echo_agent/permissions/__init__.py +0 -0
  180. echo_agent/permissions/allowlist.py +97 -0
  181. echo_agent/permissions/manager.py +460 -0
  182. echo_agent/plugins/__init__.py +30 -0
  183. echo_agent/plugins/context.py +145 -0
  184. echo_agent/plugins/errors.py +23 -0
  185. echo_agent/plugins/hooks.py +126 -0
  186. echo_agent/plugins/loader.py +251 -0
  187. echo_agent/plugins/manager.py +216 -0
  188. echo_agent/plugins/manifest.py +70 -0
  189. echo_agent/runtime_paths.py +25 -0
  190. echo_agent/scheduler/__init__.py +0 -0
  191. echo_agent/scheduler/delivery.py +63 -0
  192. echo_agent/scheduler/service.py +398 -0
  193. echo_agent/security/__init__.py +11 -0
  194. echo_agent/security/capabilities.py +54 -0
  195. echo_agent/security/guards.py +265 -0
  196. echo_agent/security/path_policy.py +212 -0
  197. echo_agent/security/risk_classifier.py +75 -0
  198. echo_agent/security/smart_approval.py +60 -0
  199. echo_agent/security/tool_policy.py +159 -0
  200. echo_agent/session/__init__.py +0 -0
  201. echo_agent/session/manager.py +404 -0
  202. echo_agent/skills/__init__.py +0 -0
  203. echo_agent/skills/manager.py +279 -0
  204. echo_agent/skills/reviewer.py +163 -0
  205. echo_agent/skills/store.py +358 -0
  206. echo_agent/storage/__init__.py +0 -0
  207. echo_agent/storage/backend.py +111 -0
  208. echo_agent/storage/sqlite.py +523 -0
  209. echo_agent/tasks/__init__.py +20 -0
  210. echo_agent/tasks/manager.py +108 -0
  211. echo_agent/tasks/models.py +180 -0
  212. echo_agent/tasks/workflow.py +182 -0
  213. echo_agent/utils/__init__.py +0 -0
  214. echo_agent/utils/async_io.py +80 -0
  215. echo_agent/utils/text.py +91 -0
  216. echo_agent-0.1.0.dist-info/METADATA +286 -0
  217. echo_agent-0.1.0.dist-info/RECORD +219 -0
  218. echo_agent-0.1.0.dist-info/WHEEL +4 -0
  219. echo_agent-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Minimal validator for echo-agent skill folders.
4
+ """
5
+
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ try:
12
+ import yaml
13
+ except ModuleNotFoundError:
14
+ yaml = None
15
+
16
+ MAX_SKILL_NAME_LENGTH = 64
17
+ ALLOWED_FRONTMATTER_KEYS = {
18
+ "name",
19
+ "description",
20
+ "metadata",
21
+ "always",
22
+ "license",
23
+ "allowed-tools",
24
+ "version",
25
+ "homepage",
26
+ "author",
27
+ }
28
+ ALLOWED_RESOURCE_DIRS = {"scripts", "references", "assets"}
29
+ PLACEHOLDER_MARKERS = (
30
+ "[complete this description",
31
+ "[write 1-2 sentences",
32
+ "[choose the structure",
33
+ "[replace with the first main section",
34
+ "[add content here",
35
+ )
36
+
37
+
38
+ def _extract_frontmatter(content: str) -> Optional[str]:
39
+ lines = content.splitlines()
40
+ if not lines or lines[0].strip() != "---":
41
+ return None
42
+ for i in range(1, len(lines)):
43
+ if lines[i].strip() == "---":
44
+ return "\n".join(lines[1:i])
45
+ return None
46
+
47
+
48
+ def _parse_simple_frontmatter(frontmatter_text: str) -> Optional[dict[str, str]]:
49
+ """Fallback parser for simple frontmatter when PyYAML is unavailable."""
50
+ parsed: dict[str, str] = {}
51
+ current_key: Optional[str] = None
52
+ multiline_key: Optional[str] = None
53
+
54
+ for raw_line in frontmatter_text.splitlines():
55
+ stripped = raw_line.strip()
56
+ if not stripped or stripped.startswith("#"):
57
+ continue
58
+
59
+ is_indented = raw_line[:1].isspace()
60
+ if is_indented:
61
+ if current_key is None:
62
+ return None
63
+ current_value = parsed[current_key]
64
+ parsed[current_key] = f"{current_value}\n{stripped}" if current_value else stripped
65
+ continue
66
+
67
+ if ":" not in stripped:
68
+ return None
69
+
70
+ key, value = stripped.split(":", 1)
71
+ key = key.strip()
72
+ value = value.strip()
73
+ if not key:
74
+ return None
75
+
76
+ if value in {"|", ">"}:
77
+ parsed[key] = ""
78
+ current_key = key
79
+ multiline_key = key
80
+ continue
81
+
82
+ if (value.startswith('"') and value.endswith('"')) or (
83
+ value.startswith("'") and value.endswith("'")
84
+ ):
85
+ value = value[1:-1]
86
+ parsed[key] = value
87
+ current_key = key
88
+ multiline_key = None
89
+
90
+ if multiline_key is not None and multiline_key not in parsed:
91
+ return None
92
+ return parsed
93
+
94
+
95
+ def _load_frontmatter(frontmatter_text: str) -> tuple[Optional[dict], Optional[str]]:
96
+ if yaml is not None:
97
+ try:
98
+ frontmatter = yaml.safe_load(frontmatter_text)
99
+ except yaml.YAMLError as exc:
100
+ return None, f"Invalid YAML in frontmatter: {exc}"
101
+ if not isinstance(frontmatter, dict):
102
+ return None, "Frontmatter must be a YAML dictionary"
103
+ return frontmatter, None
104
+
105
+ frontmatter = _parse_simple_frontmatter(frontmatter_text)
106
+ if frontmatter is None:
107
+ return None, "Invalid YAML in frontmatter: unsupported syntax without PyYAML installed"
108
+ return frontmatter, None
109
+
110
+
111
+ def _validate_skill_name(name: str, folder_name: str) -> Optional[str]:
112
+ if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name):
113
+ return (
114
+ f"Name '{name}' should be hyphen-case "
115
+ "(lowercase letters, digits, and single hyphens only)"
116
+ )
117
+ if len(name) > MAX_SKILL_NAME_LENGTH:
118
+ return (
119
+ f"Name is too long ({len(name)} characters). "
120
+ f"Maximum is {MAX_SKILL_NAME_LENGTH} characters."
121
+ )
122
+ if name != folder_name:
123
+ return f"Skill name '{name}' must match directory name '{folder_name}'"
124
+ return None
125
+
126
+
127
+ def _validate_description(description: str) -> Optional[str]:
128
+ trimmed = description.strip()
129
+ if not trimmed:
130
+ return "Description cannot be empty"
131
+ lowered = trimmed.lower()
132
+ if any(marker in lowered for marker in PLACEHOLDER_MARKERS):
133
+ return "Description still contains placeholder text"
134
+ if "<" in trimmed or ">" in trimmed:
135
+ return "Description cannot contain angle brackets (< or >)"
136
+ if len(trimmed) > 1024:
137
+ return f"Description is too long ({len(trimmed)} characters). Maximum is 1024 characters."
138
+ return None
139
+
140
+
141
+ def validate_skill(skill_path):
142
+ """Validate a skill folder structure and required frontmatter."""
143
+ skill_path = Path(skill_path).resolve()
144
+
145
+ if not skill_path.exists():
146
+ return False, f"Skill folder not found: {skill_path}"
147
+ if not skill_path.is_dir():
148
+ return False, f"Path is not a directory: {skill_path}"
149
+
150
+ skill_md = skill_path / "SKILL.md"
151
+ if not skill_md.exists():
152
+ return False, "SKILL.md not found"
153
+
154
+ try:
155
+ content = skill_md.read_text(encoding="utf-8")
156
+ except OSError as exc:
157
+ return False, f"Could not read SKILL.md: {exc}"
158
+
159
+ frontmatter_text = _extract_frontmatter(content)
160
+ if frontmatter_text is None:
161
+ return False, "Invalid frontmatter format"
162
+
163
+ frontmatter, error = _load_frontmatter(frontmatter_text)
164
+ if error:
165
+ return False, error
166
+
167
+ unexpected_keys = sorted(set(frontmatter.keys()) - ALLOWED_FRONTMATTER_KEYS)
168
+ if unexpected_keys:
169
+ allowed = ", ".join(sorted(ALLOWED_FRONTMATTER_KEYS))
170
+ unexpected = ", ".join(unexpected_keys)
171
+ return (
172
+ False,
173
+ f"Unexpected key(s) in SKILL.md frontmatter: {unexpected}. Allowed properties are: {allowed}",
174
+ )
175
+
176
+ if "name" not in frontmatter:
177
+ return False, "Missing 'name' in frontmatter"
178
+ if "description" not in frontmatter:
179
+ return False, "Missing 'description' in frontmatter"
180
+
181
+ name = frontmatter["name"]
182
+ if not isinstance(name, str):
183
+ return False, f"Name must be a string, got {type(name).__name__}"
184
+ name_error = _validate_skill_name(name.strip(), skill_path.name)
185
+ if name_error:
186
+ return False, name_error
187
+
188
+ description = frontmatter["description"]
189
+ if not isinstance(description, str):
190
+ return False, f"Description must be a string, got {type(description).__name__}"
191
+ description_error = _validate_description(description)
192
+ if description_error:
193
+ return False, description_error
194
+
195
+ always = frontmatter.get("always")
196
+ if always is not None and not isinstance(always, bool):
197
+ return False, f"'always' must be a boolean, got {type(always).__name__}"
198
+
199
+ for child in skill_path.iterdir():
200
+ if child.name == "SKILL.md":
201
+ continue
202
+ if child.is_dir() and child.name in ALLOWED_RESOURCE_DIRS:
203
+ continue
204
+ if child.is_symlink():
205
+ continue
206
+ return (
207
+ False,
208
+ f"Unexpected file or directory in skill root: {child.name}. "
209
+ "Only SKILL.md, scripts/, references/, and assets/ are allowed.",
210
+ )
211
+
212
+ return True, "Skill is valid!"
213
+
214
+
215
+ if __name__ == "__main__":
216
+ if len(sys.argv) != 2:
217
+ print("Usage: python quick_validate.py <skill_directory>")
218
+ sys.exit(1)
219
+
220
+ valid, message = validate_skill(sys.argv[1])
221
+ print(message)
222
+ sys.exit(0 if valid else 1)
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: summarize
3
+ description: Summarize or extract text/transcripts from URLs, podcasts, and local files (great fallback for "transcribe this YouTube/video").
4
+ homepage: https://summarize.sh
5
+ metadata: {"echo":{"emoji":"🧾","requires":{"bins":["summarize"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/summarize","bins":["summarize"],"label":"Install summarize (brew)"}]}}
6
+ ---
7
+
8
+ # Summarize
9
+
10
+ Fast CLI to summarize URLs, local files, and YouTube links.
11
+
12
+ ## When to use (trigger phrases)
13
+
14
+ Use this skill immediately when the user asks any of:
15
+ - "use summarize.sh"
16
+ - "what's this link/video about?"
17
+ - "summarize this URL/article"
18
+ - "transcribe this YouTube/video" (best-effort transcript extraction; no `yt-dlp` needed)
19
+
20
+ ## Quick start
21
+
22
+ ```bash
23
+ summarize "https://example.com" --model google/gemini-3-flash-preview
24
+ summarize "/path/to/file.pdf" --model google/gemini-3-flash-preview
25
+ summarize "https://youtu.be/dQw4w9WgXcQ" --youtube auto
26
+ ```
27
+
28
+ ## YouTube: summary vs transcript
29
+
30
+ Best-effort transcript (URLs only):
31
+
32
+ ```bash
33
+ summarize "https://youtu.be/dQw4w9WgXcQ" --youtube auto --extract-only
34
+ ```
35
+
36
+ If the user asked for a transcript but it's huge, return a tight summary first, then ask which section/time range to expand.
37
+
38
+ ## Model + keys
39
+
40
+ Set the API key for your chosen provider:
41
+ - OpenAI: `OPENAI_API_KEY`
42
+ - Anthropic: `ANTHROPIC_API_KEY`
43
+ - xAI: `XAI_API_KEY`
44
+ - Google: `GEMINI_API_KEY` (aliases: `GOOGLE_GENERATIVE_AI_API_KEY`, `GOOGLE_API_KEY`)
45
+
46
+ Default model is `google/gemini-3-flash-preview` if none is set.
47
+
48
+ ## Useful flags
49
+
50
+ - `--length short|medium|long|xl|xxl|<chars>`
51
+ - `--max-output-tokens <count>`
52
+ - `--extract-only` (URLs only)
53
+ - `--json` (machine readable)
54
+ - `--firecrawl auto|off|always` (fallback extraction)
55
+ - `--youtube auto` (Apify fallback if `APIFY_API_TOKEN` set)
56
+
57
+ ## Config
58
+
59
+ Optional config file: `~/.summarize/config.json`
60
+
61
+ ```json
62
+ { "model": "openai/gpt-5.2" }
63
+ ```
64
+
65
+ Optional services:
66
+ - `FIRECRAWL_API_KEY` for blocked sites
67
+ - `APIFY_API_TOKEN` for YouTube fallback
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: weather
3
+ description: Get current weather and forecasts (no API key required).
4
+ homepage: https://wttr.in/:help
5
+ metadata: {"echo":{"emoji":"🌤️","requires":{"bins":["curl"]}}}
6
+ ---
7
+
8
+ # Weather
9
+
10
+ Two free services, no API keys needed.
11
+
12
+ ## wttr.in (primary)
13
+
14
+ Quick one-liner:
15
+ ```bash
16
+ curl -s "wttr.in/London?format=3"
17
+ # Output: London: ⛅️ +8°C
18
+ ```
19
+
20
+ Compact format:
21
+ ```bash
22
+ curl -s "wttr.in/London?format=%l:+%c+%t+%h+%w"
23
+ # Output: London: ⛅️ +8°C 71% ↙5km/h
24
+ ```
25
+
26
+ Full forecast:
27
+ ```bash
28
+ curl -s "wttr.in/London?T"
29
+ ```
30
+
31
+ Format codes: `%c` condition · `%t` temp · `%h` humidity · `%w` wind · `%l` location · `%m` moon
32
+
33
+ Tips:
34
+ - URL-encode spaces: `wttr.in/New+York`
35
+ - Airport codes: `wttr.in/JFK`
36
+ - Units: `?m` (metric) `?u` (USCS)
37
+ - Today only: `?1` · Current only: `?0`
38
+ - PNG: `curl -s "wttr.in/Berlin.png" -o /tmp/weather.png`
39
+
40
+ ## Open-Meteo (fallback, JSON)
41
+
42
+ Free, no key, good for programmatic use:
43
+ ```bash
44
+ curl -s "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12&current_weather=true"
45
+ ```
46
+
47
+ Find coordinates for a city, then query. Returns JSON with temp, windspeed, weathercode.
48
+
49
+ Docs: https://open-meteo.com/en/docs
@@ -0,0 +1,232 @@
1
+ ---
2
+ name: arxiv
3
+ description: Search and retrieve academic papers from arXiv using their free REST API. No API key needed. Search by keyword, author, category, or ID. Combine with web_extract or the ocr-and-documents skill to read full paper content.
4
+ version: 1.0.0
5
+ metadata:
6
+ echo:
7
+ tags: [Research, Arxiv, Papers, Academic, Science, API]
8
+ ---
9
+
10
+ # arXiv Research
11
+
12
+ Search and retrieve academic papers from arXiv via their free REST API. No API key, no dependencies — just curl.
13
+
14
+ ## Quick Reference
15
+
16
+ | Action | Command |
17
+ |--------|---------|
18
+ | Search papers | `curl "https://export.arxiv.org/api/query?search_query=all:QUERY&max_results=5"` |
19
+ | Get specific paper | `curl "https://export.arxiv.org/api/query?id_list=2402.03300"` |
20
+ | Read abstract (web) | `web_extract(urls=["https://arxiv.org/abs/2402.03300"])` |
21
+ | Read full paper (PDF) | `web_extract(urls=["https://arxiv.org/pdf/2402.03300"])` |
22
+
23
+ ## Searching Papers
24
+
25
+ The API returns Atom XML. Parse with `grep`/`sed` or pipe through `python3` for clean output.
26
+
27
+ ### Basic search
28
+
29
+ ```bash
30
+ curl -s "https://export.arxiv.org/api/query?search_query=all:GRPO+reinforcement+learning&max_results=5"
31
+ ```
32
+
33
+ ### Clean output (parse XML to readable format)
34
+
35
+ ```bash
36
+ curl -s "https://export.arxiv.org/api/query?search_query=all:GRPO+reinforcement+learning&max_results=5&sortBy=submittedDate&sortOrder=descending" | python3 -c "
37
+ import sys, xml.etree.ElementTree as ET
38
+ ns = {'a': 'http://www.w3.org/2005/Atom'}
39
+ root = ET.parse(sys.stdin).getroot()
40
+ for i, entry in enumerate(root.findall('a:entry', ns)):
41
+ title = entry.find('a:title', ns).text.strip().replace('\n', ' ')
42
+ arxiv_id = entry.find('a:id', ns).text.strip().split('/abs/')[-1]
43
+ published = entry.find('a:published', ns).text[:10]
44
+ authors = ', '.join(a.find('a:name', ns).text for a in entry.findall('a:author', ns))
45
+ summary = entry.find('a:summary', ns).text.strip()[:200]
46
+ cats = ', '.join(c.get('term') for c in entry.findall('a:category', ns))
47
+ print(f'{i+1}. [{arxiv_id}] {title}')
48
+ print(f' Authors: {authors}')
49
+ print(f' Published: {published} | Categories: {cats}')
50
+ print(f' Abstract: {summary}...')
51
+ print(f' PDF: https://arxiv.org/pdf/{arxiv_id}')
52
+ print()
53
+ "
54
+ ```
55
+
56
+ ### Helper script
57
+
58
+ For repeated searches, use the bundled script:
59
+
60
+ ```bash
61
+ python3 scripts/search_arxiv.py "GRPO reinforcement learning"
62
+ python3 scripts/search_arxiv.py --author "Yann LeCun" --max 5
63
+ python3 scripts/search_arxiv.py --category cs.AI --sort date --max 10
64
+ python3 scripts/search_arxiv.py --id 2402.03300
65
+ ```
66
+
67
+ ## Search Query Syntax
68
+
69
+ | Prefix | Searches | Example |
70
+ |--------|----------|---------|
71
+ | `all:` | All fields | `all:transformer+attention` |
72
+ | `ti:` | Title | `ti:large+language+models` |
73
+ | `au:` | Author | `au:vaswani` |
74
+ | `abs:` | Abstract | `abs:reinforcement+learning` |
75
+ | `cat:` | Category | `cat:cs.AI` |
76
+ | `co:` | Comment | `co:accepted+NeurIPS` |
77
+
78
+ ### Boolean operators
79
+
80
+ ```
81
+ # AND (default when using +)
82
+ search_query=all:transformer+attention
83
+
84
+ # OR
85
+ search_query=all:GPT+OR+all:BERT
86
+
87
+ # AND NOT
88
+ search_query=all:language+model+ANDNOT+all:vision
89
+
90
+ # Exact phrase
91
+ search_query=ti:"chain+of+thought"
92
+
93
+ # Combined
94
+ search_query=au:hinton+AND+cat:cs.LG
95
+ ```
96
+
97
+ ## Sort and Pagination
98
+
99
+ | Parameter | Options |
100
+ |-----------|---------|
101
+ | `sortBy` | `relevance`, `lastUpdatedDate`, `submittedDate` |
102
+ | `sortOrder` | `ascending`, `descending` |
103
+ | `start` | Result offset (0-based) |
104
+ | `max_results` | Number of results (default 10, max 30000) |
105
+
106
+ ```bash
107
+ # Latest 10 papers in cs.AI
108
+ curl -s "https://export.arxiv.org/api/query?search_query=cat:cs.AI&sortBy=submittedDate&sortOrder=descending&max_results=10"
109
+ ```
110
+
111
+ ## Fetching Specific Papers
112
+
113
+ ```bash
114
+ # By arXiv ID
115
+ curl -s "https://export.arxiv.org/api/query?id_list=2402.03300"
116
+
117
+ # Multiple papers
118
+ curl -s "https://export.arxiv.org/api/query?id_list=2402.03300,2401.12345,2403.00001"
119
+ ```
120
+
121
+ ## BibTeX Generation
122
+
123
+ After fetching metadata for a paper, generate a BibTeX entry:
124
+
125
+ {% raw %}
126
+ ```bash
127
+ curl -s "https://export.arxiv.org/api/query?id_list=1706.03762" | python3 -c "
128
+ import sys, xml.etree.ElementTree as ET
129
+ ns = {'a': 'http://www.w3.org/2005/Atom', 'arxiv': 'http://arxiv.org/schemas/atom'}
130
+ root = ET.parse(sys.stdin).getroot()
131
+ entry = root.find('a:entry', ns)
132
+ if entry is None: sys.exit('Paper not found')
133
+ title = entry.find('a:title', ns).text.strip().replace('\n', ' ')
134
+ authors = ' and '.join(a.find('a:name', ns).text for a in entry.findall('a:author', ns))
135
+ year = entry.find('a:published', ns).text[:4]
136
+ raw_id = entry.find('a:id', ns).text.strip().split('/abs/')[-1]
137
+ cat = entry.find('arxiv:primary_category', ns)
138
+ primary = cat.get('term') if cat is not None else 'cs.LG'
139
+ last_name = entry.find('a:author', ns).find('a:name', ns).text.split()[-1]
140
+ print(f'@article{{{last_name}{year}_{raw_id.replace(\".\", \"\")},')
141
+ print(f' title = {{{title}}},')
142
+ print(f' author = {{{authors}}},')
143
+ print(f' year = {{{year}}},')
144
+ print(f' eprint = {{{raw_id}}},')
145
+ print(f' archivePrefix = {{arXiv}},')
146
+ print(f' primaryClass = {{{primary}}},')
147
+ print(f' url = {{https://arxiv.org/abs/{raw_id}}}')
148
+ print('}')
149
+ "
150
+ ```
151
+ {% endraw %}
152
+
153
+ ## Reading Paper Content
154
+
155
+ After finding a paper, read it:
156
+
157
+ ```
158
+ # Abstract page (fast, metadata + abstract)
159
+ web_extract(urls=["https://arxiv.org/abs/2402.03300"])
160
+
161
+ # Full paper (PDF → markdown via Firecrawl)
162
+ web_extract(urls=["https://arxiv.org/pdf/2402.03300"])
163
+ ```
164
+
165
+ ## Common Categories
166
+
167
+ | Category | Field |
168
+ |----------|-------|
169
+ | `cs.AI` | Artificial Intelligence |
170
+ | `cs.CL` | Computation and Language (NLP) |
171
+ | `cs.CV` | Computer Vision |
172
+ | `cs.LG` | Machine Learning |
173
+ | `cs.CR` | Cryptography and Security |
174
+ | `stat.ML` | Machine Learning (Statistics) |
175
+ | `math.OC` | Optimization and Control |
176
+ | `physics.comp-ph` | Computational Physics |
177
+
178
+ Full list: https://arxiv.org/category_taxonomy
179
+
180
+ ## Semantic Scholar Integration
181
+
182
+ For citation data and related papers (JSON, no auth needed for basic use):
183
+
184
+ ```bash
185
+ # Citation count
186
+ curl -s "https://api.semanticscholar.org/graph/v1/paper/arXiv:2402.03300?fields=citationCount,influentialCitationCount"
187
+
188
+ # References
189
+ curl -s "https://api.semanticscholar.org/graph/v1/paper/arXiv:2402.03300/references?fields=title,citationCount&limit=20"
190
+
191
+ # Author search
192
+ curl -s "https://api.semanticscholar.org/graph/v1/author/search?query=Yann+LeCun"
193
+ ```
194
+
195
+ ## Typical Research Workflow
196
+
197
+ 1. **Search**: `curl` the arXiv API or use `scripts/search_arxiv.py`
198
+ 2. **Assess impact**: `curl -s "https://api.semanticscholar.org/graph/v1/paper/arXiv:ID?fields=citationCount,influentialCitationCount"`
199
+ 3. **Read abstract**: `web_extract(urls=["https://arxiv.org/abs/ID"])`
200
+ 4. **Read full paper**: `web_extract(urls=["https://arxiv.org/pdf/ID"])`
201
+ 5. **Find related work**: `curl -s "https://api.semanticscholar.org/graph/v1/paper/arXiv:ID/references?fields=title,citationCount&limit=20"`
202
+ 6. **Get recommendations**: POST to Semantic Scholar recommendations endpoint
203
+ 7. **Track authors**: `curl -s "https://api.semanticscholar.org/graph/v1/author/search?query=NAME"`
204
+
205
+ ## Rate Limits
206
+
207
+ | API | Rate | Auth |
208
+ |-----|------|------|
209
+ | arXiv | ~1 req / 3 seconds | None needed |
210
+ | Semantic Scholar | 1 req / second | None (100/sec with API key) |
211
+
212
+ ## Notes
213
+
214
+ - arXiv returns Atom XML — use the helper script or parsing snippet for clean output
215
+ - Semantic Scholar returns JSON — pipe through `python3 -m json.tool` for readability
216
+ - arXiv IDs: old format (`hep-th/0601001`) vs new (`2402.03300`)
217
+ - PDF: `https://arxiv.org/pdf/{id}` — Abstract: `https://arxiv.org/abs/{id}`
218
+ - HTML (when available): `https://arxiv.org/html/{id}`
219
+
220
+ ## ID Versioning
221
+
222
+ - `arxiv.org/abs/1706.03762` always resolves to the **latest** version
223
+ - `arxiv.org/abs/1706.03762v1` points to a **specific** immutable version
224
+ - When generating citations, preserve the version suffix you actually read to prevent citation drift (a later version may substantially change content)
225
+ - The API `<id>` field returns the versioned URL (e.g., `http://arxiv.org/abs/1706.03762v7`)
226
+
227
+ ## Withdrawn Papers
228
+
229
+ Papers can be withdrawn after submission. When this happens:
230
+ - The `<summary>` field contains a withdrawal notice (look for "withdrawn" or "retracted")
231
+ - Metadata fields may be incomplete
232
+ - Always check the summary before treating a result as a valid paper
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env python3
2
+ """Search arXiv and display results in a clean format.
3
+
4
+ Usage:
5
+ python search_arxiv.py "GRPO reinforcement learning"
6
+ python search_arxiv.py "GRPO reinforcement learning" --max 10
7
+ python search_arxiv.py "GRPO reinforcement learning" --sort date
8
+ python search_arxiv.py --author "Yann LeCun" --max 5
9
+ python search_arxiv.py --category cs.AI --sort date --max 10
10
+ python search_arxiv.py --id 2402.03300
11
+ python search_arxiv.py --id 2402.03300,2401.12345
12
+ """
13
+ import sys
14
+ import urllib.request
15
+ import urllib.parse
16
+ import xml.etree.ElementTree as ET
17
+
18
+ NS = {'a': 'http://www.w3.org/2005/Atom'}
19
+
20
+
21
+ def search(query=None, author=None, category=None, ids=None, max_results=5, sort="relevance"):
22
+ params = {}
23
+
24
+ if ids:
25
+ params['id_list'] = ids
26
+ else:
27
+ parts = []
28
+ if query:
29
+ parts.append(f'all:{urllib.parse.quote(query)}')
30
+ if author:
31
+ parts.append(f'au:{urllib.parse.quote(author)}')
32
+ if category:
33
+ parts.append(f'cat:{category}')
34
+ if not parts:
35
+ print("Error: provide a query, --author, --category, or --id")
36
+ sys.exit(1)
37
+ params['search_query'] = '+AND+'.join(parts)
38
+
39
+ params['max_results'] = str(max_results)
40
+
41
+ sort_map = {"relevance": "relevance", "date": "submittedDate", "updated": "lastUpdatedDate"}
42
+ params['sortBy'] = sort_map.get(sort, sort)
43
+ params['sortOrder'] = 'descending'
44
+
45
+ url = "https://export.arxiv.org/api/query?" + "&".join(f"{k}={v}" for k, v in params.items())
46
+
47
+ req = urllib.request.Request(url, headers={'User-Agent': 'EchoAgent/1.0'})
48
+ with urllib.request.urlopen(req, timeout=15) as resp:
49
+ data = resp.read()
50
+
51
+ root = ET.fromstring(data)
52
+ entries = root.findall('a:entry', NS)
53
+
54
+ if not entries:
55
+ print("No results found.")
56
+ return
57
+
58
+ total = root.find('{http://a9.com/-/spec/opensearch/1.1/}totalResults')
59
+ if total is not None:
60
+ print(f"Found {total.text} results (showing {len(entries)})\n")
61
+
62
+ for i, entry in enumerate(entries):
63
+ title = entry.find('a:title', NS).text.strip().replace('\n', ' ')
64
+ raw_id = entry.find('a:id', NS).text.strip()
65
+ full_id = raw_id.split('/abs/')[-1] if '/abs/' in raw_id else raw_id
66
+ arxiv_id = full_id.split('v')[0]
67
+ published = entry.find('a:published', NS).text[:10]
68
+ updated = entry.find('a:updated', NS).text[:10]
69
+ authors = ', '.join(a.find('a:name', NS).text for a in entry.findall('a:author', NS))
70
+ summary = entry.find('a:summary', NS).text.strip().replace('\n', ' ')
71
+ cats = ', '.join(c.get('term') for c in entry.findall('a:category', NS))
72
+
73
+ version = full_id[len(arxiv_id):] if full_id != arxiv_id else ""
74
+ print(f"{i+1}. {title}")
75
+ print(f" ID: {arxiv_id}{version} | Published: {published} | Updated: {updated}")
76
+ print(f" Authors: {authors}")
77
+ print(f" Categories: {cats}")
78
+ print(f" Abstract: {summary[:300]}{'...' if len(summary) > 300 else ''}")
79
+ print(f" Links: https://arxiv.org/abs/{arxiv_id} | https://arxiv.org/pdf/{arxiv_id}")
80
+ print()
81
+
82
+
83
+ if __name__ == "__main__":
84
+ args = sys.argv[1:]
85
+ if not args or args[0] in ("-h", "--help"):
86
+ print(__doc__)
87
+ sys.exit(0)
88
+
89
+ query = None
90
+ author = None
91
+ category = None
92
+ ids = None
93
+ max_results = 5
94
+ sort = "relevance"
95
+
96
+ i = 0
97
+ positional = []
98
+ while i < len(args):
99
+ if args[i] == "--max" and i + 1 < len(args):
100
+ max_results = int(args[i + 1]); i += 2
101
+ elif args[i] == "--sort" and i + 1 < len(args):
102
+ sort = args[i + 1]; i += 2
103
+ elif args[i] == "--author" and i + 1 < len(args):
104
+ author = args[i + 1]; i += 2
105
+ elif args[i] == "--category" and i + 1 < len(args):
106
+ category = args[i + 1]; i += 2
107
+ elif args[i] == "--id" and i + 1 < len(args):
108
+ ids = args[i + 1]; i += 2
109
+ else:
110
+ positional.append(args[i]); i += 1
111
+
112
+ if positional:
113
+ query = " ".join(positional)
114
+
115
+ search(query=query, author=author, category=category, ids=ids, max_results=max_results, sort=sort)
@@ -0,0 +1,5 @@
1
+ from echo_agent.a2a.models import AgentCard, A2ATask, TaskState
2
+ from echo_agent.a2a.server import A2AServer
3
+ from echo_agent.a2a.client import A2AClient
4
+
5
+ __all__ = ["AgentCard", "A2ATask", "TaskState", "A2AServer", "A2AClient"]