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.
- echo_agent/__init__.py +5 -0
- echo_agent/__main__.py +538 -0
- echo_agent/_bundled/skills/development/plan/SKILL.md +54 -0
- echo_agent/_bundled/skills/development/skill-creator/SKILL.md +270 -0
- echo_agent/_bundled/skills/development/skill-creator/scripts/init_skill.py +226 -0
- echo_agent/_bundled/skills/development/skill-creator/scripts/package_skill.py +146 -0
- echo_agent/_bundled/skills/development/skill-creator/scripts/quick_validate.py +222 -0
- echo_agent/_bundled/skills/productivity/summarize/SKILL.md +67 -0
- echo_agent/_bundled/skills/productivity/weather/SKILL.md +49 -0
- echo_agent/_bundled/skills/research/arxiv/SKILL.md +232 -0
- echo_agent/_bundled/skills/research/arxiv/scripts/search_arxiv.py +115 -0
- echo_agent/a2a/__init__.py +5 -0
- echo_agent/a2a/client.py +66 -0
- echo_agent/a2a/models.py +98 -0
- echo_agent/a2a/protocol.py +85 -0
- echo_agent/a2a/server.py +71 -0
- echo_agent/agent/__init__.py +0 -0
- echo_agent/agent/approval_gate.py +326 -0
- echo_agent/agent/compression/__init__.py +14 -0
- echo_agent/agent/compression/assembler.py +45 -0
- echo_agent/agent/compression/boundary.py +141 -0
- echo_agent/agent/compression/compressor.py +181 -0
- echo_agent/agent/compression/engine.py +88 -0
- echo_agent/agent/compression/pruner.py +150 -0
- echo_agent/agent/compression/summarizer.py +181 -0
- echo_agent/agent/compression/types.py +41 -0
- echo_agent/agent/compression/validator.py +96 -0
- echo_agent/agent/consolidation.py +96 -0
- echo_agent/agent/context.py +403 -0
- echo_agent/agent/executors/__init__.py +0 -0
- echo_agent/agent/executors/base.py +211 -0
- echo_agent/agent/executors/factory.py +34 -0
- echo_agent/agent/executors/remote.py +193 -0
- echo_agent/agent/loop.py +891 -0
- echo_agent/agent/multi_agent/__init__.py +15 -0
- echo_agent/agent/multi_agent/audit.py +19 -0
- echo_agent/agent/multi_agent/error_messages.py +35 -0
- echo_agent/agent/multi_agent/error_types.py +36 -0
- echo_agent/agent/multi_agent/models.py +37 -0
- echo_agent/agent/multi_agent/registry.py +41 -0
- echo_agent/agent/multi_agent/runtime.py +201 -0
- echo_agent/agent/pipeline/__init__.py +14 -0
- echo_agent/agent/pipeline/context_stage.py +219 -0
- echo_agent/agent/pipeline/inference_stage.py +433 -0
- echo_agent/agent/pipeline/response_stage.py +146 -0
- echo_agent/agent/pipeline/types.py +40 -0
- echo_agent/agent/planning/__init__.py +4 -0
- echo_agent/agent/planning/models.py +83 -0
- echo_agent/agent/planning/planner.py +57 -0
- echo_agent/agent/planning/reflection.py +54 -0
- echo_agent/agent/planning/strategies.py +183 -0
- echo_agent/agent/tools/__init__.py +167 -0
- echo_agent/agent/tools/base.py +149 -0
- echo_agent/agent/tools/circuit_breaker.py +82 -0
- echo_agent/agent/tools/clarify.py +42 -0
- echo_agent/agent/tools/code_exec.py +147 -0
- echo_agent/agent/tools/cronjob.py +93 -0
- echo_agent/agent/tools/delegate.py +393 -0
- echo_agent/agent/tools/filesystem.py +180 -0
- echo_agent/agent/tools/image_gen.py +65 -0
- echo_agent/agent/tools/knowledge.py +81 -0
- echo_agent/agent/tools/memory.py +198 -0
- echo_agent/agent/tools/message.py +39 -0
- echo_agent/agent/tools/notify.py +35 -0
- echo_agent/agent/tools/patch.py +178 -0
- echo_agent/agent/tools/process.py +139 -0
- echo_agent/agent/tools/registry.py +185 -0
- echo_agent/agent/tools/search.py +99 -0
- echo_agent/agent/tools/session_search.py +76 -0
- echo_agent/agent/tools/shell.py +164 -0
- echo_agent/agent/tools/skill_install.py +255 -0
- echo_agent/agent/tools/skills.py +177 -0
- echo_agent/agent/tools/task.py +104 -0
- echo_agent/agent/tools/todo.py +148 -0
- echo_agent/agent/tools/tts.py +77 -0
- echo_agent/agent/tools/vision.py +71 -0
- echo_agent/agent/tools/web.py +208 -0
- echo_agent/agent/tools/workflow.py +89 -0
- echo_agent/bus/__init__.py +11 -0
- echo_agent/bus/events.py +193 -0
- echo_agent/bus/queue.py +158 -0
- echo_agent/bus/rate_limiter.py +51 -0
- echo_agent/channels/__init__.py +0 -0
- echo_agent/channels/base.py +185 -0
- echo_agent/channels/cli.py +149 -0
- echo_agent/channels/cron.py +44 -0
- echo_agent/channels/dingtalk.py +195 -0
- echo_agent/channels/discord.py +359 -0
- echo_agent/channels/email.py +168 -0
- echo_agent/channels/feishu.py +240 -0
- echo_agent/channels/manager.py +417 -0
- echo_agent/channels/matrix.py +281 -0
- echo_agent/channels/qqbot.py +638 -0
- echo_agent/channels/qqbot_media.py +482 -0
- echo_agent/channels/slack.py +297 -0
- echo_agent/channels/telegram.py +275 -0
- echo_agent/channels/webhook.py +106 -0
- echo_agent/channels/wecom.py +152 -0
- echo_agent/channels/weixin.py +603 -0
- echo_agent/channels/whatsapp.py +138 -0
- echo_agent/cli/__init__.py +0 -0
- echo_agent/cli/colors.py +42 -0
- echo_agent/cli/evolution_cmd.py +299 -0
- echo_agent/cli/i18n/__init__.py +123 -0
- echo_agent/cli/i18n/en.py +275 -0
- echo_agent/cli/i18n/zh.py +275 -0
- echo_agent/cli/plugins_cmd.py +205 -0
- echo_agent/cli/prompt.py +102 -0
- echo_agent/cli/service.py +156 -0
- echo_agent/cli/setup.py +1111 -0
- echo_agent/cli/status.py +93 -0
- echo_agent/config/__init__.py +8 -0
- echo_agent/config/default.yaml +199 -0
- echo_agent/config/loader.py +125 -0
- echo_agent/config/schema.py +652 -0
- echo_agent/evaluation/__init__.py +4 -0
- echo_agent/evaluation/dataset.py +66 -0
- echo_agent/evaluation/metrics.py +70 -0
- echo_agent/evaluation/reporter.py +42 -0
- echo_agent/evaluation/runner.py +143 -0
- echo_agent/evolution/__init__.py +38 -0
- echo_agent/evolution/engine.py +335 -0
- echo_agent/evolution/evolver.py +397 -0
- echo_agent/evolution/gate.py +413 -0
- echo_agent/evolution/recorder.py +288 -0
- echo_agent/evolution/scheduler.py +133 -0
- echo_agent/evolution/store.py +331 -0
- echo_agent/evolution/tools.py +110 -0
- echo_agent/evolution/types.py +270 -0
- echo_agent/gateway/__init__.py +7 -0
- echo_agent/gateway/auth.py +178 -0
- echo_agent/gateway/editor.py +121 -0
- echo_agent/gateway/health.py +51 -0
- echo_agent/gateway/hooks.py +86 -0
- echo_agent/gateway/media.py +137 -0
- echo_agent/gateway/rate_limiter.py +72 -0
- echo_agent/gateway/router.py +86 -0
- echo_agent/gateway/server.py +570 -0
- echo_agent/gateway/session_context.py +57 -0
- echo_agent/gateway/session_policy.py +47 -0
- echo_agent/gateway/static/index.html +432 -0
- echo_agent/knowledge/__init__.py +5 -0
- echo_agent/knowledge/index.py +308 -0
- echo_agent/mcp/__init__.py +3 -0
- echo_agent/mcp/client.py +158 -0
- echo_agent/mcp/manager.py +161 -0
- echo_agent/mcp/oauth.py +208 -0
- echo_agent/mcp/security.py +79 -0
- echo_agent/mcp/tool_adapter.py +73 -0
- echo_agent/mcp/transport.py +353 -0
- echo_agent/memory/__init__.py +0 -0
- echo_agent/memory/consolidator.py +273 -0
- echo_agent/memory/contradiction.py +287 -0
- echo_agent/memory/forgetting.py +114 -0
- echo_agent/memory/retrieval.py +184 -0
- echo_agent/memory/reviewer.py +192 -0
- echo_agent/memory/store.py +706 -0
- echo_agent/memory/tiers.py +243 -0
- echo_agent/memory/types.py +168 -0
- echo_agent/memory/vectors.py +148 -0
- echo_agent/models/__init__.py +0 -0
- echo_agent/models/credential_pool.py +86 -0
- echo_agent/models/inference.py +98 -0
- echo_agent/models/provider.py +208 -0
- echo_agent/models/providers/__init__.py +209 -0
- echo_agent/models/providers/anthropic_provider.py +164 -0
- echo_agent/models/providers/bedrock_provider.py +261 -0
- echo_agent/models/providers/format_utils.py +198 -0
- echo_agent/models/providers/gemini_provider.py +159 -0
- echo_agent/models/providers/openai_provider.py +253 -0
- echo_agent/models/providers/openrouter_provider.py +38 -0
- echo_agent/models/rate_limiter.py +75 -0
- echo_agent/models/router.py +325 -0
- echo_agent/models/tokenizer.py +111 -0
- echo_agent/observability/__init__.py +0 -0
- echo_agent/observability/monitor.py +209 -0
- echo_agent/observability/spans.py +75 -0
- echo_agent/observability/telemetry.py +86 -0
- echo_agent/permissions/__init__.py +0 -0
- echo_agent/permissions/allowlist.py +97 -0
- echo_agent/permissions/manager.py +460 -0
- echo_agent/plugins/__init__.py +30 -0
- echo_agent/plugins/context.py +145 -0
- echo_agent/plugins/errors.py +23 -0
- echo_agent/plugins/hooks.py +126 -0
- echo_agent/plugins/loader.py +251 -0
- echo_agent/plugins/manager.py +216 -0
- echo_agent/plugins/manifest.py +70 -0
- echo_agent/runtime_paths.py +25 -0
- echo_agent/scheduler/__init__.py +0 -0
- echo_agent/scheduler/delivery.py +63 -0
- echo_agent/scheduler/service.py +398 -0
- echo_agent/security/__init__.py +11 -0
- echo_agent/security/capabilities.py +54 -0
- echo_agent/security/guards.py +265 -0
- echo_agent/security/path_policy.py +212 -0
- echo_agent/security/risk_classifier.py +75 -0
- echo_agent/security/smart_approval.py +60 -0
- echo_agent/security/tool_policy.py +159 -0
- echo_agent/session/__init__.py +0 -0
- echo_agent/session/manager.py +404 -0
- echo_agent/skills/__init__.py +0 -0
- echo_agent/skills/manager.py +279 -0
- echo_agent/skills/reviewer.py +163 -0
- echo_agent/skills/store.py +358 -0
- echo_agent/storage/__init__.py +0 -0
- echo_agent/storage/backend.py +111 -0
- echo_agent/storage/sqlite.py +523 -0
- echo_agent/tasks/__init__.py +20 -0
- echo_agent/tasks/manager.py +108 -0
- echo_agent/tasks/models.py +180 -0
- echo_agent/tasks/workflow.py +182 -0
- echo_agent/utils/__init__.py +0 -0
- echo_agent/utils/async_io.py +80 -0
- echo_agent/utils/text.py +91 -0
- echo_agent-0.1.0.dist-info/METADATA +286 -0
- echo_agent-0.1.0.dist-info/RECORD +219 -0
- echo_agent-0.1.0.dist-info/WHEEL +4 -0
- 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¤t_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)
|