feat: Add Codex CLI compatibility
- Add agents/openai.yaml for Codex skill discovery
- Make SKILL.md script path portable (repo, Claude, Codex, agents dirs)
- Platform-neutral output text ("assistant" instead of "Claude")
- Sandbox-friendly cache/output dirs with env var overrides and tempdir fallback
- Add Codex installation docs to README
Inspired by PR #24 (el-analista) and PR #5 (jblwilliams).
Zero impact on existing Claude Code behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,16 @@ node ~/.claude/skills/last30days/scripts/lib/vendor/bird-search/bird-search.mjs
|
|||||||
|
|
||||||
**Requirements:** Node.js 22+ (for the vendored Twitter GraphQL client).
|
**Requirements:** Node.js 22+ (for the vendored Twitter GraphQL client).
|
||||||
|
|
||||||
|
### Codex CLI
|
||||||
|
|
||||||
|
This skill also works in OpenAI Codex CLI. Install to the Codex skills directory instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/mvanhorn/last30days-skill.git ~/.agents/skills/last30days
|
||||||
|
```
|
||||||
|
|
||||||
|
Same SKILL.md, same Python engine, same scripts. The `agents/openai.yaml` provides Codex-specific discovery metadata. Invoke with `$last30days` or through the `/skills` menu.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -61,7 +61,22 @@ This text MUST appear before you call any tools. It confirms to the user that yo
|
|||||||
|
|
||||||
**Step 1: Run the research script**
|
**Step 1: Run the research script**
|
||||||
```bash
|
```bash
|
||||||
python3 "${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/skills/last30days}/scripts/last30days.py" "$ARGUMENTS" --emit=compact 2>&1
|
# Find skill root — works in repo checkout, Claude Code, or Codex install
|
||||||
|
for dir in \
|
||||||
|
"." \
|
||||||
|
"${CLAUDE_PLUGIN_ROOT:-}" \
|
||||||
|
"$HOME/.claude/skills/last30days" \
|
||||||
|
"$HOME/.agents/skills/last30days" \
|
||||||
|
"$HOME/.codex/skills/last30days"; do
|
||||||
|
[ -n "$dir" ] && [ -f "$dir/scripts/last30days.py" ] && SKILL_ROOT="$dir" && break
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "${SKILL_ROOT:-}" ]; then
|
||||||
|
echo "ERROR: Could not find scripts/last30days.py" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 "${SKILL_ROOT}/scripts/last30days.py" "$ARGUMENTS" --emit=compact 2>&1
|
||||||
```
|
```
|
||||||
|
|
||||||
The script will automatically:
|
The script will automatically:
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Last 30 Days"
|
||||||
|
short_description: "Research any topic across Reddit, X, YouTube, and the web from the last 30 days. Returns synthesized expert answers and copy-paste prompts."
|
||||||
|
default_prompt: "Research this topic from the last 30 days across Reddit, X, YouTube, and web. Synthesize what people are actually saying, upvoting, and sharing right now."
|
||||||
|
brand_color: "#FF6B35"
|
||||||
|
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: true
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
---
|
||||||
|
title: "feat: Add Codex CLI compatibility"
|
||||||
|
type: feat
|
||||||
|
date: 2026-02-14
|
||||||
|
---
|
||||||
|
|
||||||
|
# feat: Add Codex CLI Compatibility
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Make /last30days work as a Codex CLI skill alongside Claude Code. Both platforms use `SKILL.md` with YAML frontmatter — the gap is small but the details matter. Inspired by PR #24 (el-analista) and PR #5 (jblwilliams) on the public repo, applied to the v2.1 codebase.
|
||||||
|
|
||||||
|
## Research Findings
|
||||||
|
|
||||||
|
### How Codex Skills Work (from [official docs](https://developers.openai.com/codex/skills))
|
||||||
|
|
||||||
|
**Format:** Identical to Claude Code — `SKILL.md` with YAML frontmatter + Markdown body.
|
||||||
|
|
||||||
|
**Required frontmatter:** Only `name` and `description`. The official skill-creator guidance says "Do not include any other fields in YAML frontmatter." This is stricter than Claude Code which allows `version`, `allowed-tools`, `argument-hint`, etc.
|
||||||
|
|
||||||
|
**Discovery:** Codex uses "progressive disclosure" — it reads ONLY the `description` field to decide whether to invoke a skill. The body loads only after triggering. This means the description must be comprehensive about when to use/not use the skill.
|
||||||
|
|
||||||
|
**Invocation:** Users invoke with `$skill-name` or `/skills` menu. Codex can also implicitly match based on the description (configurable via `agents/openai.yaml`).
|
||||||
|
|
||||||
|
**Installation paths** (scanned in order):
|
||||||
|
| Scope | Path |
|
||||||
|
|-------|------|
|
||||||
|
| Folder | `$CWD/.agents/skills/` |
|
||||||
|
| Repo | `$REPO_ROOT/.agents/skills/` |
|
||||||
|
| User | `$HOME/.agents/skills/` |
|
||||||
|
| Admin | `/etc/codex/skills/` |
|
||||||
|
| System | Bundled |
|
||||||
|
|
||||||
|
Note: Some docs also mention `~/.codex/skills/` as an alias for `$HOME/.agents/skills/`. Both should be checked.
|
||||||
|
|
||||||
|
**`agents/openai.yaml`** (optional sidecar):
|
||||||
|
```yaml
|
||||||
|
interface:
|
||||||
|
display_name: "User-facing name"
|
||||||
|
short_description: "Brief description"
|
||||||
|
default_prompt: "Surrounding prompt template"
|
||||||
|
brand_color: "#hex"
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: true
|
||||||
|
dependencies:
|
||||||
|
tools:
|
||||||
|
- type: "mcp"
|
||||||
|
value: "toolName"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Size guidance:** Keep SKILL.md under 500 lines. Use `references/` directory for detailed docs that load on demand.
|
||||||
|
|
||||||
|
**Scripts:** Put executable code in `scripts/`. These can run without being loaded into context — good for our Python research engine.
|
||||||
|
|
||||||
|
### What Real Codex Skills Look Like (from [openai/skills catalog](https://github.com/openai/skills))
|
||||||
|
|
||||||
|
**openai-docs skill** — Uses MCP tools (`mcp__openaiDeveloperDocs__search_openai_docs`). Has a workflow section, fallback instructions if MCP isn't set up, and quality rules. Clean and focused.
|
||||||
|
|
||||||
|
**pdf skill** — Runs scripts (`pdftoppm`, `reportlab`), has file conventions (`tmp/pdfs/`, `output/pdf/`), specifies dependencies. Good example of a skill that shells out to tools like we do.
|
||||||
|
|
||||||
|
**skill-creator** — The meta-skill. Emphasizes "the context window is a public good" and treating the LLM as "already very smart — only add information it genuinely lacks." Has 6 creation steps, validation scripts, and naming conventions.
|
||||||
|
|
||||||
|
### Key Insight: Frontmatter Compatibility Problem
|
||||||
|
|
||||||
|
Claude Code SKILL.md uses:
|
||||||
|
```yaml
|
||||||
|
name: last30days
|
||||||
|
version: "2.1"
|
||||||
|
description: Research a topic...
|
||||||
|
argument-hint: 'nano banana pro prompts...'
|
||||||
|
allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch
|
||||||
|
```
|
||||||
|
|
||||||
|
Codex wants only `name` and `description`. The question: does Codex error on unknown frontmatter fields, or ignore them?
|
||||||
|
|
||||||
|
**Safe answer:** Codex uses standard YAML parsing and likely ignores unknown keys. But the official guidance says "Do not include any other fields" — meaning it's untested territory and could break in future Codex updates.
|
||||||
|
|
||||||
|
**Our approach:** Keep one SKILL.md with Claude-specific fields. If Codex chokes, we add a thin wrapper. This is pragmatic — maintaining two SKILL.md files defeats the purpose of cross-platform compatibility.
|
||||||
|
|
||||||
|
### PR #24 Analysis (el-analista)
|
||||||
|
|
||||||
|
Good ideas to incorporate:
|
||||||
|
- Portable script path resolution (repo → Claude → Codex → agents)
|
||||||
|
- `agents/openai.yaml` for Codex discovery
|
||||||
|
- Platform-neutral output text ("assistant" instead of "Claude")
|
||||||
|
- Sandbox-friendly cache/output dir fallbacks with env var overrides
|
||||||
|
- Last-chance retry for Bird search (better query noise stripping)
|
||||||
|
|
||||||
|
Not applicable to v2.1:
|
||||||
|
- Based on v2.0 codebase — doesn't have YouTube, vendored Bird, or pipeline changes
|
||||||
|
- We'll cherry-pick the ideas, not the code
|
||||||
|
|
||||||
|
### PR #5 Analysis (jblwilliams)
|
||||||
|
|
||||||
|
Not needed:
|
||||||
|
- Codex JWT auth — our OpenAI API calls work natively in Codex already
|
||||||
|
- SSE response handling — we don't stream responses
|
||||||
|
- The 403 enrichment issues they hit are specific to Codex-hosted auth, not our use case
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
Five changes, all additive — zero impact on existing Claude Code behavior:
|
||||||
|
|
||||||
|
### 1. Add `agents/openai.yaml` for Codex discovery
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
interface:
|
||||||
|
display_name: "Last 30 Days"
|
||||||
|
short_description: "Research any topic across Reddit, X, YouTube, and the web from the last 30 days. Returns synthesized expert answers and copy-paste prompts."
|
||||||
|
default_prompt: "Research this topic from the last 30 days across Reddit, X, YouTube, and web. Synthesize what people are actually saying, upvoting, and sharing right now."
|
||||||
|
brand_color: "#FF6B35"
|
||||||
|
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Make SKILL.md script path portable
|
||||||
|
|
||||||
|
Replace the hardcoded Claude path with a lookup that checks multiple install locations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find the skill root
|
||||||
|
for dir in \
|
||||||
|
"." \
|
||||||
|
"${CLAUDE_PLUGIN_ROOT:-}" \
|
||||||
|
"$HOME/.claude/skills/last30days" \
|
||||||
|
"$HOME/.agents/skills/last30days" \
|
||||||
|
"$HOME/.codex/skills/last30days"; do
|
||||||
|
[ -n "$dir" ] && [ -f "$dir/scripts/last30days.py" ] && SKILL_ROOT="$dir" && break
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "${SKILL_ROOT:-}" ]; then
|
||||||
|
echo "ERROR: Could not find scripts/last30days.py" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 "${SKILL_ROOT}/scripts/last30days.py" "$ARGUMENTS" --emit=compact 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Platform-neutral Python output text
|
||||||
|
|
||||||
|
Replace "Claude" with "assistant" in LLM-facing output strings only. Human-facing docs (README, etc.) stay as-is.
|
||||||
|
|
||||||
|
Files:
|
||||||
|
- `scripts/last30days.py` — web search marker text (~3 lines)
|
||||||
|
- `scripts/lib/render.py` — docstrings + web-only banner (~4 lines)
|
||||||
|
- `scripts/lib/http.py` — User-Agent string (~1 line)
|
||||||
|
|
||||||
|
### 4. Sandbox-friendly cache/output dirs
|
||||||
|
|
||||||
|
Codex runs sandboxed. Add env var overrides + tempdir fallback (from PR #24):
|
||||||
|
|
||||||
|
**`scripts/lib/cache.py`:**
|
||||||
|
- Check `LAST30DAYS_CACHE_DIR` env var
|
||||||
|
- Catch `PermissionError`, fall back to `tempfile.gettempdir()/last30days/cache`
|
||||||
|
|
||||||
|
**`scripts/lib/render.py`:**
|
||||||
|
- Check `LAST30DAYS_OUTPUT_DIR` env var
|
||||||
|
- Catch `PermissionError`, fall back to `tempfile.gettempdir()/last30days/out`
|
||||||
|
|
||||||
|
### 5. README + installation docs
|
||||||
|
|
||||||
|
Add a "Codex Compatibility" section to README:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Codex Compatibility
|
||||||
|
|
||||||
|
This skill works in both Claude Code and OpenAI Codex CLI.
|
||||||
|
|
||||||
|
**Claude Code:** `git clone` into `~/.claude/skills/last30days`
|
||||||
|
**Codex CLI:** `git clone` into `~/.agents/skills/last30days`
|
||||||
|
|
||||||
|
Both use the same SKILL.md, same Python engine, same scripts.
|
||||||
|
The `agents/openai.yaml` provides Codex-specific discovery metadata.
|
||||||
|
```
|
||||||
|
|
||||||
|
## What We're NOT Doing
|
||||||
|
|
||||||
|
- **Separate SKILL.md for Codex** — One file, both platforms. Claude-specific frontmatter fields (`allowed-tools`, `version`, `argument-hint`) are likely ignored by Codex's YAML parser. If this breaks, we'll address it then.
|
||||||
|
- **Codex JWT auth (PR #5)** — Our OpenAI Responses API calls work natively in Codex. No special handling needed.
|
||||||
|
- **SSE streaming (PR #5)** — Not our use case.
|
||||||
|
- **Codex-specific tool names in SKILL.md** — Both LLMs understand "do a web search" and "run this bash command." The instructions work cross-platform as-is.
|
||||||
|
- **Publishing to openai/skills catalog** — Out of scope for now. Users install via git clone.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] `agents/openai.yaml` exists with proper `interface` and `policy` sections
|
||||||
|
- [x] SKILL.md uses portable path resolution (repo checkout, `~/.claude/skills/`, `~/.agents/skills/`, `~/.codex/skills/`)
|
||||||
|
- [x] Python scripts use "assistant" instead of "Claude" in LLM-facing output (~8 string replacements)
|
||||||
|
- [x] Cache dir falls back gracefully in sandboxed environments (`LAST30DAYS_CACHE_DIR` env var + `PermissionError` catch)
|
||||||
|
- [x] Output dir falls back gracefully in sandboxed environments (`LAST30DAYS_OUTPUT_DIR` env var + `PermissionError` catch)
|
||||||
|
- [x] Existing Claude Code behavior is unchanged (zero regressions)
|
||||||
|
- [x] README documents Codex installation path (`~/.agents/skills/last30days`)
|
||||||
|
- [x] `python3 scripts/last30days.py "test topic" --mock --emit=compact` still works
|
||||||
|
|
||||||
|
## Files to Create/Modify
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
- `agents/openai.yaml` — Codex discovery metadata (~10 lines)
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
- `SKILL.md` — Portable script path resolution (~15 lines changed)
|
||||||
|
- `README.md` — Add "Codex Compatibility" section (~15 lines)
|
||||||
|
- `scripts/last30days.py` — "Claude" → "assistant" in output strings (~3 lines)
|
||||||
|
- `scripts/lib/render.py` — "Claude" → "assistant" + output dir fallback (~15 lines)
|
||||||
|
- `scripts/lib/cache.py` — Cache dir env override + fallback (~12 lines)
|
||||||
|
- `scripts/lib/http.py` — User-Agent string (~1 line)
|
||||||
|
|
||||||
|
### Total scope: ~70 lines changed across 7 files. Small, additive, low risk.
|
||||||
|
|
||||||
|
## Dependencies & Risks
|
||||||
|
|
||||||
|
| Risk | Likelihood | Mitigation |
|
||||||
|
|------|-----------|------------|
|
||||||
|
| Codex rejects unknown YAML frontmatter (`allowed-tools`, etc.) | Low-Medium | Standard YAML parsers ignore unknown keys. If it breaks, strip Claude-specific fields and use `agents/openai.yaml` for metadata. |
|
||||||
|
| Codex sandbox blocks Node.js (vendored Bird) | Medium | Bird failure already falls back to xAI API. If no xAI key, X search skipped gracefully. |
|
||||||
|
| yt-dlp not in Codex sandbox PATH | Medium | YouTube already degrades gracefully — "yt-dlp not installed, skipping YouTube." |
|
||||||
|
| Codex sandbox blocks `~/.cache/` writes | Medium | Env var override + tempdir fallback handles this. (Proven approach from PR #24) |
|
||||||
|
| Codex changes skill discovery paths | Low | We check 5 paths. Easy to add more. |
|
||||||
|
| Codex description matching triggers on wrong queries | Low | Write description with clear "use when" / "do not use when" boundaries per official guidance. |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
### Community PRs
|
||||||
|
- [PR #24](https://github.com/mvanhorn/last30days-skill/pull/24) (el-analista) — Codex compatibility, portable paths, platform-neutral text
|
||||||
|
- [PR #5](https://github.com/mvanhorn/last30days-skill/pull/5) (jblwilliams) — Codex auth support
|
||||||
|
|
||||||
|
### Official Codex Docs
|
||||||
|
- [Agent Skills](https://developers.openai.com/codex/skills) — SKILL.md format, discovery, installation paths
|
||||||
|
- [AGENTS.md Guide](https://developers.openai.com/codex/guides/agents-md/) — Custom instructions, hierarchical loading
|
||||||
|
- [Codex CLI Features](https://developers.openai.com/codex/cli/features/) — Overview of CLI capabilities
|
||||||
|
- [Configuration Reference](https://developers.openai.com/codex/config-reference/) — config.toml, skill enable/disable
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
- [openai/skills catalog](https://github.com/openai/skills) — Official curated skills
|
||||||
|
- [skill-creator](https://github.com/openai/skills/blob/main/skills/.system/skill-creator/SKILL.md) — Meta-skill for creating skills, best practices
|
||||||
|
- [pdf skill](https://github.com/openai/skills/blob/main/skills/.curated/pdf/SKILL.md) — Example of skill that runs external scripts
|
||||||
|
- [openai-docs skill](https://github.com/openai/skills/blob/main/skills/.curated/openai-docs/SKILL.md) — Example of MCP-backed skill
|
||||||
|
|
||||||
|
### Community Analysis
|
||||||
|
- [Skills in OpenAI Codex](https://blog.fsck.com/2025/12/19/codex-skills/) — Jesse Vincent's deep dive on skill internals
|
||||||
|
- [Simon Willison on skills adoption](https://simonw.substack.com/p/openai-are-quietly-adopting-skills) — Cross-platform skill format analysis
|
||||||
|
- [SkillsMP marketplace](https://skillsmp.com/) — Community marketplace supporting both Claude Code and Codex skills
|
||||||
@@ -376,8 +376,8 @@ def run_research(
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple of (reddit_items, x_items, youtube_items, web_needed, raw_openai, raw_xai, raw_reddit_enriched, reddit_error, x_error, youtube_error)
|
Tuple of (reddit_items, x_items, youtube_items, web_needed, raw_openai, raw_xai, raw_reddit_enriched, reddit_error, x_error, youtube_error)
|
||||||
|
|
||||||
Note: web_needed is True when WebSearch should be performed by Claude.
|
Note: web_needed is True when web search should be performed by the assistant.
|
||||||
The script outputs a marker and Claude handles WebSearch in its session.
|
The script outputs a marker and the assistant handles web search in its session.
|
||||||
"""
|
"""
|
||||||
reddit_items = []
|
reddit_items = []
|
||||||
x_items = []
|
x_items = []
|
||||||
@@ -392,7 +392,7 @@ def run_research(
|
|||||||
# Check if WebSearch is needed (always needed in web-only mode)
|
# Check if WebSearch is needed (always needed in web-only mode)
|
||||||
web_needed = sources in ("all", "web", "reddit-web", "x-web")
|
web_needed = sources in ("all", "web", "reddit-web", "x-web")
|
||||||
|
|
||||||
# Web-only mode: no API calls needed, Claude handles everything
|
# Web-only mode: no API calls needed, assistant handles everything
|
||||||
if sources == "web":
|
if sources == "web":
|
||||||
if progress:
|
if progress:
|
||||||
progress.start_web_only()
|
progress.start_web_only()
|
||||||
@@ -803,7 +803,7 @@ def output_result(
|
|||||||
print(f"Topic: {topic}")
|
print(f"Topic: {topic}")
|
||||||
print(f"Date range: {from_date} to {to_date}")
|
print(f"Date range: {from_date} to {to_date}")
|
||||||
print("")
|
print("")
|
||||||
print("Claude: Use your WebSearch tool to find 8-15 relevant web pages.")
|
print("Assistant: Use your web search tool to find 8-15 relevant web pages.")
|
||||||
print("EXCLUDE: reddit.com, x.com, twitter.com (already covered above)")
|
print("EXCLUDE: reddit.com, x.com, twitter.com (already covered above)")
|
||||||
print(f"INCLUDE: blogs, docs, news, tutorials from the last {days} days")
|
print(f"INCLUDE: blogs, docs, news, tutorials from the last {days} days")
|
||||||
print("")
|
print("")
|
||||||
|
|||||||
+16
-3
@@ -3,6 +3,7 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
@@ -10,10 +11,22 @@ from typing import Any, Optional
|
|||||||
CACHE_DIR = Path.home() / ".cache" / "last30days"
|
CACHE_DIR = Path.home() / ".cache" / "last30days"
|
||||||
DEFAULT_TTL_HOURS = 24
|
DEFAULT_TTL_HOURS = 24
|
||||||
MODEL_CACHE_TTL_DAYS = 7
|
MODEL_CACHE_TTL_DAYS = 7
|
||||||
|
MODEL_CACHE_FILE = CACHE_DIR / "model_selection.json"
|
||||||
|
|
||||||
|
|
||||||
def ensure_cache_dir():
|
def ensure_cache_dir():
|
||||||
"""Ensure cache directory exists."""
|
"""Ensure cache directory exists. Supports env override and sandbox fallback."""
|
||||||
|
global CACHE_DIR, MODEL_CACHE_FILE
|
||||||
|
env_dir = os.environ.get("LAST30DAYS_CACHE_DIR")
|
||||||
|
if env_dir:
|
||||||
|
CACHE_DIR = Path(env_dir)
|
||||||
|
MODEL_CACHE_FILE = CACHE_DIR / "model_selection.json"
|
||||||
|
|
||||||
|
try:
|
||||||
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
except PermissionError:
|
||||||
|
CACHE_DIR = Path(tempfile.gettempdir()) / "last30days" / "cache"
|
||||||
|
MODEL_CACHE_FILE = CACHE_DIR / "model_selection.json"
|
||||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -112,8 +125,8 @@ def clear_cache():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Model selection cache (longer TTL)
|
# Model selection cache (longer TTL) — MODEL_CACHE_FILE is set at module level
|
||||||
MODEL_CACHE_FILE = CACHE_DIR / "model_selection.json"
|
# and updated by ensure_cache_dir() if env override or fallback is needed.
|
||||||
|
|
||||||
|
|
||||||
def load_model_cache() -> dict:
|
def load_model_cache() -> dict:
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ def log(msg: str):
|
|||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
MAX_RETRIES = 3
|
MAX_RETRIES = 3
|
||||||
RETRY_DELAY = 1.0
|
RETRY_DELAY = 1.0
|
||||||
USER_AGENT = "last30days-skill/2.0 (Claude Code Skill)"
|
USER_AGENT = "last30days-skill/2.1 (Assistant Skill)"
|
||||||
|
|
||||||
|
|
||||||
class HTTPError(Exception):
|
class HTTPError(Exception):
|
||||||
|
|||||||
+18
-7
@@ -1,6 +1,8 @@
|
|||||||
"""Output rendering for last30days skill."""
|
"""Output rendering for last30days skill."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
@@ -10,7 +12,16 @@ OUTPUT_DIR = Path.home() / ".local" / "share" / "last30days" / "out"
|
|||||||
|
|
||||||
|
|
||||||
def ensure_output_dir():
|
def ensure_output_dir():
|
||||||
"""Ensure output directory exists."""
|
"""Ensure output directory exists. Supports env override and sandbox fallback."""
|
||||||
|
global OUTPUT_DIR
|
||||||
|
env_dir = os.environ.get("LAST30DAYS_OUTPUT_DIR")
|
||||||
|
if env_dir:
|
||||||
|
OUTPUT_DIR = Path(env_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
except PermissionError:
|
||||||
|
OUTPUT_DIR = Path(tempfile.gettempdir()) / "last30days" / "out"
|
||||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -35,7 +46,7 @@ def _assess_data_freshness(report: schema.Report) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def render_compact(report: schema.Report, limit: int = 15, missing_keys: str = "none") -> str:
|
def render_compact(report: schema.Report, limit: int = 15, missing_keys: str = "none") -> str:
|
||||||
"""Render compact output for Claude to synthesize.
|
"""Render compact output for the assistant to synthesize.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
report: Report data
|
report: Report data
|
||||||
@@ -61,7 +72,7 @@ def render_compact(report: schema.Report, limit: int = 15, missing_keys: str = "
|
|||||||
|
|
||||||
# Web-only mode banner (when no API keys)
|
# Web-only mode banner (when no API keys)
|
||||||
if report.mode == "web-only":
|
if report.mode == "web-only":
|
||||||
lines.append("**🌐 WEB SEARCH MODE** - Claude will search blogs, docs & news")
|
lines.append("**🌐 WEB SEARCH MODE** - assistant will search blogs, docs & news")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("---")
|
lines.append("---")
|
||||||
lines.append("**⚡ Want better results?** Add API keys to unlock Reddit & X data:")
|
lines.append("**⚡ Want better results?** Add API keys to unlock Reddit & X data:")
|
||||||
@@ -204,7 +215,7 @@ def render_compact(report: schema.Report, limit: int = 15, missing_keys: str = "
|
|||||||
lines.append(f" *{item.why_relevant}*")
|
lines.append(f" *{item.why_relevant}*")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Web items (if any - populated by Claude)
|
# Web items (if any - populated by the assistant)
|
||||||
if report.web_error:
|
if report.web_error:
|
||||||
lines.append("### Web Results")
|
lines.append("### Web Results")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
@@ -356,15 +367,15 @@ def render_full_report(report: schema.Report) -> str:
|
|||||||
lines.append(f"> {item.snippet}")
|
lines.append(f"> {item.snippet}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Placeholders for Claude synthesis
|
# Placeholders for assistant synthesis
|
||||||
lines.append("## Best Practices")
|
lines.append("## Best Practices")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("*To be synthesized by Claude*")
|
lines.append("*To be synthesized by assistant*")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
lines.append("## Prompt Pack")
|
lines.append("## Prompt Pack")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("*To be synthesized by Claude*")
|
lines.append("*To be synthesized by assistant*")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|||||||
+2
-2
@@ -344,7 +344,7 @@ class ProgressDisplay:
|
|||||||
def end_web_only(self):
|
def end_web_only(self):
|
||||||
"""End web-only spinner."""
|
"""End web-only spinner."""
|
||||||
if self.spinner:
|
if self.spinner:
|
||||||
self.spinner.stop(f"{Colors.GREEN}Web{Colors.RESET} Claude will search the web")
|
self.spinner.stop(f"{Colors.GREEN}Web{Colors.RESET} assistant will search the web")
|
||||||
|
|
||||||
def show_web_only_complete(self):
|
def show_web_only_complete(self):
|
||||||
"""Show completion for web-only mode."""
|
"""Show completion for web-only mode."""
|
||||||
@@ -352,7 +352,7 @@ class ProgressDisplay:
|
|||||||
if IS_TTY:
|
if IS_TTY:
|
||||||
sys.stderr.write(f"\n{Colors.GREEN}{Colors.BOLD}✓ Ready for web search{Colors.RESET} ")
|
sys.stderr.write(f"\n{Colors.GREEN}{Colors.BOLD}✓ Ready for web search{Colors.RESET} ")
|
||||||
sys.stderr.write(f"{Colors.DIM}({elapsed:.1f}s){Colors.RESET}\n")
|
sys.stderr.write(f"{Colors.DIM}({elapsed:.1f}s){Colors.RESET}\n")
|
||||||
sys.stderr.write(f" {Colors.GREEN}Web:{Colors.RESET} Claude will search blogs, docs & news\n\n")
|
sys.stderr.write(f" {Colors.GREEN}Web:{Colors.RESET} assistant will search blogs, docs & news\n\n")
|
||||||
else:
|
else:
|
||||||
sys.stderr.write(f"✓ Ready for web search ({elapsed:.1f}s)\n")
|
sys.stderr.write(f"✓ Ready for web search ({elapsed:.1f}s)\n")
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"""WebSearch module for last30days skill.
|
"""WebSearch module for last30days skill.
|
||||||
|
|
||||||
NOTE: WebSearch uses Claude's built-in WebSearch tool, which runs INSIDE Claude Code.
|
NOTE: WebSearch uses the assistant's built-in web search tool, which runs inside the host environment.
|
||||||
Unlike Reddit/X which use external APIs, WebSearch results are obtained by Claude
|
Unlike Reddit/X which use external APIs, web search results are obtained by the assistant
|
||||||
directly and passed to this module for normalization and scoring.
|
directly and passed to this module for normalization and scoring.
|
||||||
|
|
||||||
The typical flow is:
|
The typical flow is:
|
||||||
1. Claude invokes WebSearch tool with the topic
|
1. The assistant invokes its web search tool with the topic
|
||||||
2. Claude passes results to parse_websearch_results()
|
2. The assistant passes results to parse_websearch_results()
|
||||||
3. Results are normalized into WebSearchItem objects
|
3. Results are normalized into WebSearchItem objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user