mirror of
https://github.com/mvanhorn/last30days-skill.git
synced 2026-09-14 16:11:48 +08:00
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
@@ -181,6 +181,10 @@ Wenn du lieber den Agent-Skills-Installationsweg unter Claude Code nutzt, wird a
|
||||
npx skills add mvanhorn/last30days-skill -g -a claude-code
|
||||
```
|
||||
|
||||
### Schnell-Testlink
|
||||
|
||||
[Last30Days Research Skill in Telegram oder WhatsApp ausprobieren](https://app.clawmama.run/skills/2ne05f/hermes?utm_source=github&utm_medium=issue&utm_campaign=skill_outreach_mvanhorn_last30days_skill)
|
||||
|
||||
Das native Plugin und die `npx skills`-Installation können nebeneinander existieren. Beachte aber: Claude Code dedupliziert nicht über Installationsmethoden hinweg. Wenn sowohl das Marketplace-Plugin als auch die `npx skills`-Kopie aktiv sind, taucht `/last30days` doppelt auf. Nutze pro Rechner eine Installationsmethode.
|
||||
|
||||
### Grok (xAI Build CLI)
|
||||
|
||||
@@ -181,6 +181,10 @@ Si prefieres usar la vía de instalación de Agent Skills en Claude Code, tambi
|
||||
npx skills add mvanhorn/last30days-skill -g -a claude-code
|
||||
```
|
||||
|
||||
### Enlace de prueba rápida
|
||||
|
||||
[Prueba la skill Last30Days Research en Telegram o WhatsApp](https://app.clawmama.run/skills/2ne05f/hermes?utm_source=github&utm_medium=issue&utm_campaign=skill_outreach_mvanhorn_last30days_skill)
|
||||
|
||||
El plugin nativo y la instalación con `npx skills` pueden convivir. Ojo: Claude Code no deduplica entre métodos de instalación. Si tienes activos a la vez el plugin del marketplace y la copia de `npx skills`, `/last30days` aparecerá dos veces. Usa un solo método de instalación por máquina.
|
||||
|
||||
### Grok (xAI Build CLI)
|
||||
|
||||
@@ -181,6 +181,10 @@ Si vous préférez passer par le chemin d'installation Agent Skills sur Claude C
|
||||
npx skills add mvanhorn/last30days-skill -g -a claude-code
|
||||
```
|
||||
|
||||
### Lien d'essai rapide
|
||||
|
||||
[Essayer la compétence Last30Days Research dans Telegram ou WhatsApp](https://app.clawmama.run/skills/2ne05f/hermes?utm_source=github&utm_medium=issue&utm_campaign=skill_outreach_mvanhorn_last30days_skill)
|
||||
|
||||
Le plugin natif et l'installation `npx skills` peuvent coexister. Attention : Claude Code ne déduplique pas entre méthodes d'installation. Si le plugin de la marketplace et la copie `npx skills` sont actifs tous les deux, `/last30days` apparaîtra en double. Utilisez une seule méthode d'installation par machine.
|
||||
|
||||
### Grok (xAI Build CLI)
|
||||
|
||||
@@ -181,6 +181,10 @@ Claude Code で Agent Skills 経由のインストールを使いたい場合も
|
||||
npx skills add mvanhorn/last30days-skill -g -a claude-code
|
||||
```
|
||||
|
||||
### クイック試用リンク
|
||||
|
||||
[Telegram または WhatsApp で Last30Days Research Skill を試す](https://app.clawmama.run/skills/2ne05f/hermes?utm_source=github&utm_medium=issue&utm_campaign=skill_outreach_mvanhorn_last30days_skill)
|
||||
|
||||
ネイティブプラグインと `npx skills` でのインストールは共存できます。ただし Claude Code はインストール方法をまたいだ重複排除を行いません。マーケットプレイス版のプラグインと `npx skills` のコピーを両方とも有効にしていると、`/last30days` が2件表示されます。1台につきインストール方法は1つにしてください。
|
||||
|
||||
### Grok(xAI Build CLI)
|
||||
|
||||
@@ -181,6 +181,10 @@ Se preferir usar o caminho de instalação do Agent Skills no Claude Code, ele t
|
||||
npx skills add mvanhorn/last30days-skill -g -a claude-code
|
||||
```
|
||||
|
||||
### Link para experimentar rápido
|
||||
|
||||
[Experimente a skill Last30Days Research no Telegram ou WhatsApp](https://app.clawmama.run/skills/2ne05f/hermes?utm_source=github&utm_medium=issue&utm_campaign=skill_outreach_mvanhorn_last30days_skill)
|
||||
|
||||
O plugin nativo e a instalação com `npx skills` podem conviver. Só atenção: o Claude Code não deduplica entre métodos de instalação. Se você tiver ativos ao mesmo tempo o plugin do marketplace e a cópia do `npx skills`, o `/last30days` vai aparecer duas vezes. Use um método de instalação por máquina.
|
||||
|
||||
### Grok (xAI Build CLI)
|
||||
|
||||
@@ -184,6 +184,10 @@ v3 打下的基础都还在:真正调用 API 前先运行预研究模块,解
|
||||
npx skills add mvanhorn/last30days-skill -g -a claude-code
|
||||
```
|
||||
|
||||
### 快速试用链接
|
||||
|
||||
[在 Telegram 或 WhatsApp 中试用 Last30Days Research Skill](https://app.clawmama.run/skills/2ne05f/hermes?utm_source=github&utm_medium=issue&utm_campaign=skill_outreach_mvanhorn_last30days_skill)
|
||||
|
||||
原生插件和 `npx skills` 安装可以共存。但 Claude Code 不会对不同安装方式进行去重:若两者同时启用,`/last30days` 会出现两个条目。建议每台机器只选一种安装方式。
|
||||
|
||||
### Grok(xAI Build CLI)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Empty or qualifier-only GitHub topics now report no-results instead of marking the source as failed.
|
||||
@@ -269,19 +269,16 @@ def search_github(
|
||||
plain_core = strip_search_qualifiers(core)
|
||||
if not plain_core:
|
||||
# A qualifier-only (or empty) topic leaves nothing to search on.
|
||||
# Report it instead of querying an empty term, which would match the
|
||||
# whole site and then be discarded by the date filter as a bogus
|
||||
# "no results" (issue #949). Truncate the topic; skip the strip log
|
||||
# so this path does not repeat an unbounded core (issue #954).
|
||||
# Skip the network rather than querying an empty term, which would
|
||||
# match the whole site (#949). Return a clean empty envelope, not an
|
||||
# error, so the pipeline records NO_RESULTS instead of ERROR (#953)
|
||||
# and GitHub is not marked as a failed attempt. Mixed-topic strip
|
||||
# logs below stay bounded by _truncate_diagnostic (#954).
|
||||
_log("Topic contained only search qualifiers or was empty; nothing to search")
|
||||
shown = _truncate_diagnostic(topic)
|
||||
return {
|
||||
"items": [],
|
||||
"context": {"core": core, "from_date": from_date,
|
||||
"to_date": to_date, "count": count},
|
||||
"error": (
|
||||
f"GitHub topic contained only search qualifiers or was empty: {shown!r}"
|
||||
),
|
||||
}
|
||||
if plain_core != core:
|
||||
_log(
|
||||
|
||||
+23
-13
@@ -745,15 +745,14 @@ class TestSearchGithubQualifiers(unittest.TestCase):
|
||||
self.assertNotIn("is:pull-request", q)
|
||||
|
||||
@patch.object(github, "_resolve_token", return_value="test-token")
|
||||
def test_qualifier_only_topic_errors_without_network(self, mock_token):
|
||||
def test_qualifier_only_topic_skips_network(self, mock_token):
|
||||
with patch.object(github, "_fetch_json") as mock_fetch:
|
||||
result = github.search_github(
|
||||
"created:>2025-03-20", "2026-07-01", "2026-07-31",
|
||||
)
|
||||
mock_fetch.assert_not_called()
|
||||
self.assertEqual(result["items"], [])
|
||||
self.assertIn("error", result)
|
||||
self.assertIn("qualifier", result["error"].lower())
|
||||
self.assertNotIn("error", result)
|
||||
self.assertEqual(result["context"]["from_date"], "2026-07-01")
|
||||
|
||||
@patch.object(github, "_resolve_token", return_value="test-token")
|
||||
@@ -784,12 +783,12 @@ class TestSearchGithubQualifiers(unittest.TestCase):
|
||||
self.assertNotIn("created:>2025-03-20", q)
|
||||
|
||||
@patch.object(github, "_resolve_token", return_value="test-token")
|
||||
def test_empty_topic_errors_without_network(self, mock_token):
|
||||
def test_empty_topic_skips_network(self, mock_token):
|
||||
with patch.object(github, "_fetch_json") as mock_fetch:
|
||||
result = github.search_github("", "2026-07-01", "2026-07-31")
|
||||
mock_fetch.assert_not_called()
|
||||
self.assertEqual(result["items"], [])
|
||||
self.assertIn("error", result)
|
||||
self.assertNotIn("error", result)
|
||||
|
||||
@patch.object(github, "_resolve_token", return_value="test-token")
|
||||
def test_glued_term_after_qualifier_value_reaches_query(self, mock_token):
|
||||
@@ -807,28 +806,39 @@ class TestSearchGithubQualifiers(unittest.TestCase):
|
||||
def test_paren_wrapped_qualifier_builds_single_created_query(self, mock_token):
|
||||
# Wrapped qualifier (issue #952) must not survive into the query to
|
||||
# collide with the adapter's own created: window (issue #949 class).
|
||||
# Authenticated search emits is:issue / is:pull-request partitions
|
||||
# (GitHub 422s without one); assert the subject per sub-query.
|
||||
captured = {}
|
||||
with patch.object(github, "_fetch_json", side_effect=self._capturing_fetch(captured)):
|
||||
github.search_github(
|
||||
"open source ai (created:>2025-03-20)", "2026-07-01", "2026-07-31",
|
||||
)
|
||||
q = self._query(captured["url"])
|
||||
self.assertEqual(q, "open source ai created:>2026-07-01")
|
||||
self.assertEqual(q.count("created:"), 1)
|
||||
queries = [self._query(u) for u in captured["urls"]]
|
||||
self.assertEqual(len(queries), 2)
|
||||
for q in queries:
|
||||
self.assertTrue(q.startswith("open source ai created:>2026-07-01"))
|
||||
self.assertEqual(q.count("created:"), 1)
|
||||
self.assertIn("created:>2026-07-01", q)
|
||||
self.assertNotIn("created:>2025-03-20", q)
|
||||
self.assertIn("open source", q)
|
||||
self.assertIn("ai", q)
|
||||
self.assertEqual(
|
||||
{q.rsplit(" ", 1)[-1] for q in queries},
|
||||
{"is:issue", "is:pull-request"},
|
||||
)
|
||||
|
||||
@patch.object(github, "_resolve_token", return_value="test-token")
|
||||
def test_quote_wrapped_qualifier_only_topic_errors_without_network(self, mock_token):
|
||||
def test_quote_wrapped_qualifier_only_topic_skips_network(self, mock_token):
|
||||
# A quote-wrapped qualifier-only topic strips to nothing, so the
|
||||
# adapter must report the empty-topic error instead of querying an
|
||||
# empty term (issue #952; mirrors the plain qualifier-only case).
|
||||
# adapter must skip the network (#949/#952) and return a clean
|
||||
# no-results envelope rather than ERROR (#953).
|
||||
with patch.object(github, "_fetch_json") as mock_fetch:
|
||||
result = github.search_github(
|
||||
'"created:>2025-03-20"', "2026-07-01", "2026-07-31",
|
||||
)
|
||||
mock_fetch.assert_not_called()
|
||||
self.assertEqual(result["items"], [])
|
||||
self.assertIn("error", result)
|
||||
self.assertIn("qualifier", result["error"].lower())
|
||||
self.assertNotIn("error", result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Empty and qualifier-only GitHub topics must not be classified as ERROR (#953).
|
||||
|
||||
Pre-#949, search_github('') built a site-wide created:> window query.
|
||||
The qualifier strip then returned an error envelope with no network call,
|
||||
and pipeline._result_outcome_artifact mapped that error to a failed attempt.
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from lib import github, pipeline, schema
|
||||
|
||||
|
||||
def _search(topic):
|
||||
with patch.object(github, "_resolve_token", return_value="test-token"):
|
||||
with patch.object(github, "_fetch_json") as mock_fetch:
|
||||
result = github.search_github(topic, "2026-07-01", "2026-07-31")
|
||||
return result, mock_fetch
|
||||
|
||||
|
||||
def _bundle_from_envelope(envelope):
|
||||
artifact = pipeline._result_outcome_artifact("github", envelope)
|
||||
bundle = schema.RetrievalBundle()
|
||||
bundle.mark_attempted("github")
|
||||
outcome = artifact.get("_source_outcome") if isinstance(artifact, dict) else None
|
||||
if isinstance(outcome, dict):
|
||||
bundle.record_failure(
|
||||
"github",
|
||||
outcome["state"],
|
||||
outcome["detail"],
|
||||
attempted=outcome.get("attempted", True),
|
||||
)
|
||||
items = github.parse_github_response(envelope)
|
||||
bundle.add_items("primary", "github", items)
|
||||
return artifact, bundle
|
||||
|
||||
|
||||
def test_empty_topic_is_clean_no_results_not_error():
|
||||
result, mock_fetch = _search("")
|
||||
mock_fetch.assert_not_called()
|
||||
assert result["items"] == []
|
||||
assert "error" not in result
|
||||
artifact, bundle = _bundle_from_envelope(result)
|
||||
assert artifact == {}
|
||||
assert "github" not in bundle.errors_by_source
|
||||
outcome = bundle.source_status["github"]
|
||||
assert outcome.state == schema.NO_RESULTS
|
||||
assert outcome.attempted is True
|
||||
|
||||
|
||||
def test_qualifier_only_topic_is_clean_no_results_not_error():
|
||||
result, mock_fetch = _search("created:>2025-03-20")
|
||||
mock_fetch.assert_not_called()
|
||||
assert result["items"] == []
|
||||
assert "error" not in result
|
||||
artifact, bundle = _bundle_from_envelope(result)
|
||||
assert artifact == {}
|
||||
assert "github" not in bundle.errors_by_source
|
||||
assert bundle.source_status["github"].state == schema.NO_RESULTS
|
||||
|
||||
|
||||
def test_noise_plus_qualifier_topic_is_clean_no_results_not_error():
|
||||
# extract_core_subject('the best stars:>1000') -> 'stars:>1000' -> plain ''
|
||||
result, mock_fetch = _search("the best stars:>1000")
|
||||
mock_fetch.assert_not_called()
|
||||
assert result["items"] == []
|
||||
assert "error" not in result
|
||||
artifact, bundle = _bundle_from_envelope(result)
|
||||
assert artifact == {}
|
||||
assert "github" not in bundle.errors_by_source
|
||||
assert bundle.source_status["github"].state == schema.NO_RESULTS
|
||||
|
||||
|
||||
def test_quote_wrapped_qualifier_only_topic_is_clean_no_results_not_error():
|
||||
# Wrapper shapes from #952 still strip to nothing; #953 is the envelope.
|
||||
result, mock_fetch = _search('"created:>2025-03-20"')
|
||||
mock_fetch.assert_not_called()
|
||||
assert result["items"] == []
|
||||
assert "error" not in result
|
||||
artifact, bundle = _bundle_from_envelope(result)
|
||||
assert artifact == {}
|
||||
assert "github" not in bundle.errors_by_source
|
||||
assert bundle.source_status["github"].state == schema.NO_RESULTS
|
||||
@@ -22,30 +22,28 @@ def _long_qualifier_only_topic() -> str:
|
||||
return "created:>2025-03-20 " + ("stars:>1 " * 400)
|
||||
|
||||
|
||||
def test_qualifier_only_error_detail_is_bounded():
|
||||
def test_qualifier_only_topic_returns_clean_empty_envelope_without_network():
|
||||
"""#953 supersedes the #954 error bound: a qualifier-only topic is a clean
|
||||
no-results envelope (no error key), so there is nothing to spam."""
|
||||
topic = _long_qualifier_only_topic()
|
||||
with patch.object(github, "_resolve_token", return_value="t"), patch.object(
|
||||
github, "_fetch_json"
|
||||
) as fetch:
|
||||
result = github.search_github(topic, _FROM, _TO)
|
||||
fetch.assert_not_called()
|
||||
error = result["error"]
|
||||
assert "qualifier" in error.lower()
|
||||
assert len(error) < _ERROR_BOUND
|
||||
assert len(error) < len(topic)
|
||||
assert "created:>2025-03-20" in error
|
||||
assert "..." in error
|
||||
assert result["items"] == []
|
||||
assert "error" not in result
|
||||
|
||||
|
||||
def test_short_qualifier_only_topic_is_fully_shown():
|
||||
def test_short_qualifier_only_topic_is_also_clean_no_results():
|
||||
topic = "created:>2025-03-20"
|
||||
with patch.object(github, "_resolve_token", return_value="t"), patch.object(
|
||||
github, "_fetch_json"
|
||||
) as fetch:
|
||||
result = github.search_github(topic, _FROM, _TO)
|
||||
fetch.assert_not_called()
|
||||
assert topic in result["error"]
|
||||
assert "..." not in result["error"]
|
||||
assert result["items"] == []
|
||||
assert "error" not in result
|
||||
|
||||
|
||||
def test_qualifier_only_logs_are_bounded_across_fanout():
|
||||
|
||||
Reference in New Issue
Block a user