Skip to content

Commit bf37822

Browse files
aborrusoclaude
andauthored
feat(cup-cig): add cig-fetch.sh script for full CIG detail (#19)
* feat(cup-cig): add cig-fetch.sh script and Source 5 in SKILL.md scripts/cig-fetch.sh downloads the full CIG JSON from ANAC dettaglio-cig via agent-browser automation (20+ sections vs SCP-MIT). SKILL.md updated: decision tree, Source 5 section, compatibility field. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(cup-cig): address Copilot review on cig-fetch.sh and SKILL.md - Add timeout check (exit 127 with macOS hint) - Use unique session name (CIG + PID) to avoid concurrent run races - Fix JSON validation to require object/array, not just parseable value - Reconcile decision tree: Source 5 covers all CIG types (Z and alphanumeric) - Add timeout to compatibility field in SKILL.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3abfb41 commit bf37822

2 files changed

Lines changed: 169 additions & 4 deletions

File tree

skills/cup-cig/SKILL.md

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
name: cup-cig
33
description: Guide users monitoring Italian public procurement to extract detailed information from lists of CUP (Codice Unico di Progetto) and CIG (Codice Identificativo Gara). Use when the user wants to look up project metadata, financial status, or tender details for Italian public contracts.
4-
compatibility: Requires curl, jq, bash, internet access. OpenCUP API requires OPENCUP_API_CLIENT_ID and OPENCUP_API_CLIENT_SECRET environment variables.
4+
compatibility: Requires curl, jq, bash, internet access. OpenCUP API requires OPENCUP_API_CLIENT_ID and OPENCUP_API_CLIENT_SECRET environment variables. scripts/cig-fetch.sh requires agent-browser and timeout (GNU coreutils; macOS: brew install coreutils).
55
license: CC BY-SA 4.0 (Creative Commons Attribution-ShareAlike 4.0 International)
66
metadata:
77
version: "0.1"
@@ -31,10 +31,12 @@ User has a CUP
3131
└─ ANAC BDNCP bulk (authoritative CUP↔CIG join)
3232
3333
User has a CIG
34+
├─ Full detail (participants, awards, subcontracts, financials) — any CIG type?
35+
│ └─ ANAC dettaglio-cig → scripts/cig-fetch.sh (requires agent-browser)
3436
├─ Starts with Z (e.g. Z063947806)?
35-
│ └─ Below-threshold direct award → ANAC BDNCP bulk only
36-
└─ Alphanumeric (e.g. 8874674CA7)?
37-
└─ SCP-MIT API (published tenders)
37+
│ └─ Below-threshold direct award → ANAC BDNCP bulk (no live API)
38+
└─ Alphanumeric (e.g. 8874674CA7) — basic tender list / outcomes?
39+
└─ SCP-MIT API
3840
```
3941

4042
---
@@ -164,6 +166,41 @@ duckdb -c "SELECT * FROM read_csv_auto('cup.csv', HEADER=TRUE) WHERE cig = 'Z063
164166

165167
---
166168

169+
## Source 5 — ANAC dettaglio-cig: Full CIG Detail
170+
171+
**Portal:** `https://dettaglio-cig.anticorruzione.it/cig/{CIG}`
172+
**Method:** Browser automation via `scripts/cig-fetch.sh`
173+
**Requires:** `agent-browser`, `jq`
174+
**Coverage:** All CIG types (Z-prefix and alphanumeric). Returns 20+ sections.
175+
176+
The payload is far richer than SCP-MIT: includes `bando`, `stazioneAppaltante`,
177+
`partecipanti`, `incaricati`, `aggiudicazione`, `quadroEconomico`, `subappalti`,
178+
`varianti`, `categorieOpera`, `pubblicazioni`, and more.
179+
180+
### Fetch full detail for a single CIG
181+
182+
```bash
183+
bash scripts/cig-fetch.sh 8874674CA7
184+
# → ./8874674CA7.json
185+
```
186+
187+
```bash
188+
bash scripts/cig-fetch.sh 8874674CA7 /tmp
189+
# → /tmp/8874674CA7.json
190+
```
191+
192+
### Troubleshooting
193+
194+
If the script fails with `Invalid response: EOF`:
195+
196+
```bash
197+
agent-browser close
198+
```
199+
200+
Then rerun the script.
201+
202+
---
203+
167204
## Definition of Done
168205

169206
A task is complete when:
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env bash
2+
# Scarica il JSON completo di un CIG dal portale ANAC dettaglio-cig.
3+
# Gist: https://gist.github.com/aborruso/183e93419b5d3528af69a6617e97a3f1
4+
# Richiede: agent-browser (https://github.com/vercel-labs/agent-browser), jq, timeout (GNU coreutils)
5+
set -euo pipefail
6+
7+
if ! command -v agent-browser >/dev/null 2>&1; then
8+
echo "Errore: 'agent-browser' non installato." >&2
9+
echo "Info e installazione: https://github.com/vercel-labs/agent-browser" >&2
10+
exit 127
11+
fi
12+
13+
if ! command -v jq >/dev/null 2>&1; then
14+
echo "Errore: 'jq' non installato." >&2
15+
exit 127
16+
fi
17+
18+
if ! command -v timeout >/dev/null 2>&1; then
19+
echo "Errore: 'timeout' non trovato (GNU coreutils richiesto; su macOS: brew install coreutils)." >&2
20+
exit 127
21+
fi
22+
23+
if [ $# -lt 1 ]; then
24+
echo "Uso: $0 <CIG> [output-dir]" >&2
25+
exit 1
26+
fi
27+
28+
CIG="$1"
29+
OUTDIR="${2:-.}"
30+
URL="https://dettaglio-cig.anticorruzione.it/cig/${CIG}"
31+
OUT="${OUTDIR%/}/${CIG}.json"
32+
33+
mkdir -p "$OUTDIR"
34+
35+
# Sessione univoca per evitare race condition tra run concorrenti
36+
export AGENT_BROWSER_SESSION="cig-fetch-${CIG}-$$"
37+
38+
# Chiudi eventuali sessioni rimaste appese da run precedenti con lo stesso nome
39+
agent-browser close >/dev/null 2>&1 || true
40+
sleep 1
41+
42+
# Timeout globale: oltre HARD_TIMEOUT secondi si considera fallito
43+
HARD_TIMEOUT="${CIG_FETCH_TIMEOUT:-15}"
44+
45+
run() {
46+
# Esegue un comando agent-browser con timeout per step; fallisce se scade.
47+
local step_timeout="$1"; shift
48+
if ! timeout "$step_timeout" "$@" >/dev/null 2>&1; then
49+
echo "Errore: step scaduto (${step_timeout}s): $*" >&2
50+
agent-browser close >/dev/null 2>&1 || true
51+
exit 4
52+
fi
53+
}
54+
55+
# Avvia il timer complessivo come watchdog in background
56+
(
57+
sleep "$HARD_TIMEOUT"
58+
kill -TERM $$ 2>/dev/null
59+
) &
60+
WATCHDOG=$!
61+
trap 'kill $WATCHDOG 2>/dev/null || true; agent-browser close >/dev/null 2>&1 || true' EXIT
62+
trap 'echo "Errore: timeout globale ${HARD_TIMEOUT}s superato" >&2; exit 5' TERM
63+
64+
run 8 agent-browser open "$URL"
65+
run 6 agent-browser wait --text "I accept that the form entries"
66+
67+
run 4 agent-browser eval "(async () => {
68+
for (let i = 0; i < 20; i++) {
69+
const cb = document.querySelector('input[type=checkbox]');
70+
if (cb) { cb.click(); return 'clicked'; }
71+
await new Promise(r => setTimeout(r, 250));
72+
}
73+
return 'not-found';
74+
})()"
75+
76+
run 3 agent-browser eval "(() => {
77+
const orig = URL.createObjectURL;
78+
URL.createObjectURL = function(blob) {
79+
if (blob instanceof Blob) blob.text().then(t => window.__capturedJSON = t);
80+
return orig.call(this, blob);
81+
};
82+
return 'hooked';
83+
})()"
84+
85+
# Click "Cerca" via JS (il finder role-button si blocca su questa SPA)
86+
run 3 agent-browser eval "(() => {
87+
const b = Array.from(document.querySelectorAll('button')).find(x => x.innerText.trim() === 'Cerca');
88+
if (!b) return 'no-cerca';
89+
b.click();
90+
return 'clicked';
91+
})()"
92+
93+
run 8 agent-browser wait --text "Informazioni Gara"
94+
95+
# Click "Esporta in JSON" via JS (match parziale su "JSON")
96+
run 3 agent-browser eval "(() => {
97+
const b = Array.from(document.querySelectorAll('button')).find(x => /JSON/i.test(x.innerText));
98+
if (!b) return 'no-json';
99+
b.click();
100+
return 'clicked';
101+
})()"
102+
103+
# Attendi che il blob JSON venga catturato (entro il timeout residuo)
104+
RAW='""'
105+
for i in $(seq 1 10); do
106+
sleep 1
107+
RAW=$(agent-browser eval "window.__capturedJSON || ''" --json 2>/dev/null || echo '""')
108+
if [ "$(printf '%s' "$RAW" | wc -c)" -gt 20 ]; then
109+
break
110+
fi
111+
done
112+
113+
kill $WATCHDOG 2>/dev/null || true
114+
agent-browser close >/dev/null 2>&1 || true
115+
116+
if [ -z "${RAW:-}" ] || [ "$RAW" = '""' ]; then
117+
echo "Errore: JSON non catturato per $CIG" >&2
118+
exit 2
119+
fi
120+
121+
printf '%s' "$RAW" | jq -r '.data.result' > "$OUT"
122+
123+
if ! jq 'if type == "object" or type == "array" then true else error end' "$OUT" >/dev/null 2>&1; then
124+
echo "Errore: output non è un oggetto/array JSON valido ($OUT)" >&2
125+
exit 3
126+
fi
127+
128+
echo "OK: $OUT ($(wc -c <"$OUT") byte)"

0 commit comments

Comments
 (0)