opencode-skills-collection 4.0.48 → 4.0.49
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.
- package/bundled-skills/.antigravity-install-manifest.json +11 -1
- package/bundled-skills/agent-qa-authoring/SKILL.md +3 -1
- package/bundled-skills/agy-delegate/SKILL.md +1 -1
- package/bundled-skills/aider-delegate/SKILL.md +1 -1
- package/bundled-skills/api-rate-limit-handler/SKILL.md +1 -1
- package/bundled-skills/atlas-cloud-media/SKILL.md +52 -18
- package/bundled-skills/babysit-pr/SKILL.md +1 -1
- package/bundled-skills/boost-asio-pro/references/pre-cpp20.md +8 -1
- package/bundled-skills/check-identity-pack/SKILL.md +88 -0
- package/bundled-skills/claude-delegate/SKILL.md +1 -1
- package/bundled-skills/cline-delegate/SKILL.md +1 -1
- package/bundled-skills/codex-delegate/SKILL.md +1 -1
- package/bundled-skills/commandcode-delegate/SKILL.md +1 -1
- package/bundled-skills/copilot-delegate/SKILL.md +1 -1
- package/bundled-skills/cursor-delegate/SKILL.md +1 -1
- package/bundled-skills/detect-ai-text/SKILL.md +91 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +1 -1
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/aas-core.md +1 -1
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +3 -3
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/extract-document-data/SKILL.md +99 -0
- package/bundled-skills/find-matching-tenders/SKILL.md +100 -0
- package/bundled-skills/grok-delegate/SKILL.md +1 -1
- package/bundled-skills/inngest/SKILL.md +2 -0
- package/bundled-skills/kimi-delegate/SKILL.md +1 -1
- package/bundled-skills/liuguang-banlan-ui/SKILL.md +6 -4
- package/bundled-skills/liuguang-banlan-ui/scripts/manifest_parser.py +178 -0
- package/bundled-skills/liuguang-banlan-ui/scripts/measure_preview.py +14 -17
- package/bundled-skills/liuguang-banlan-ui/scripts/validate_manifest.py +12 -16
- package/bundled-skills/lovable-cleanup/SKILL.md +32 -6
- package/bundled-skills/lovable-cleanup/references/favicon-vercel-cleanup.md +106 -0
- package/bundled-skills/lovable-cleanup/scripts/write-transparent-favicon.js +88 -0
- package/bundled-skills/multi-source-search/scripts/validate_report.py +5 -0
- package/bundled-skills/omp-delegate/SKILL.md +1 -1
- package/bundled-skills/opencode-delegate/SKILL.md +1 -1
- package/bundled-skills/pentest-tools/references/pentest-ai-agents-matrix.md +8 -5
- package/bundled-skills/pi-delegate/SKILL.md +1 -1
- package/bundled-skills/prompt-caching/SKILL.md +3 -1
- package/bundled-skills/qoder-delegate/SKILL.md +1 -1
- package/bundled-skills/screen-adverse-media/SKILL.md +100 -0
- package/bundled-skills/slideops/SKILL.md +201 -0
- package/bundled-skills/trigger-dev/SKILL.md +2 -0
- package/bundled-skills/unsloth-finetuning/SKILL.md +45 -2
- package/bundled-skills/upstash-qstash/SKILL.md +1 -1
- package/bundled-skills/upstash-ratelimit/SKILL.md +186 -0
- package/bundled-skills/upstash-redis/SKILL.md +156 -0
- package/bundled-skills/verify-citations/SKILL.md +88 -0
- package/bundled-skills/verify-document/SKILL.md +94 -0
- package/bundled-skills/vibe-delegate/SKILL.md +1 -1
- package/bundled-skills/warp-delegate/SKILL.md +1 -1
- package/bundled-skills/zcode-delegate/SKILL.md +1 -1
- package/package.json +1 -1
- package/skills_index.json +347 -21
|
@@ -39,7 +39,7 @@ If you came in through a **Claude Code** or **Codex** plugin instead of AAS Core
|
|
|
39
39
|
|
|
40
40
|
When you ran `npx agentic-awesome-skills` or cloned the repository, you:
|
|
41
41
|
|
|
42
|
-
✅ **Downloaded 2,
|
|
42
|
+
✅ **Downloaded 2,107+ skill files** to your computer (default: `~/.agents/skills/`; or a custom path like `~/.agent/skills/` if you used `--path`)
|
|
43
43
|
✅ **Made them available** to your AI assistant
|
|
44
44
|
❌ **Did NOT enable them all automatically** (they're just sitting there, waiting)
|
|
45
45
|
|
|
@@ -231,7 +231,7 @@ Let's actually use a skill right now. Follow these steps:
|
|
|
231
231
|
|
|
232
232
|
## Direct-install Step 5: Pick Skills Manually
|
|
233
233
|
|
|
234
|
-
Don't try to use all 2,
|
|
234
|
+
Don't try to use all 2,107+ skills at once. Here's a sensible approach:
|
|
235
235
|
|
|
236
236
|
If you want a tool-specific starting point before choosing skills, use:
|
|
237
237
|
|
|
@@ -362,7 +362,7 @@ Usually no, but if your AI doesn't recognize a skill:
|
|
|
362
362
|
|
|
363
363
|
### "Can I load all skills into the model at once?"
|
|
364
364
|
|
|
365
|
-
No. Even though you have 2,
|
|
365
|
+
No. Even though you have 2,107+ skills installed locally, you should **not** concatenate every `SKILL.md` into a single system prompt or context block.
|
|
366
366
|
|
|
367
367
|
The intended pattern is:
|
|
368
368
|
|
|
@@ -40,7 +40,7 @@ agentic-awesome-skills/
|
|
|
40
40
|
├── 📄 CONTRIBUTING.md ← Contributor workflow
|
|
41
41
|
├── 📄 CATALOG.md ← Full generated catalog
|
|
42
42
|
│
|
|
43
|
-
├── 📁 skills/ ← 2,
|
|
43
|
+
├── 📁 skills/ ← 2,107+ skills live here
|
|
44
44
|
│ │
|
|
45
45
|
│ ├── 📁 brainstorming/
|
|
46
46
|
│ │ └── 📄 SKILL.md ← Skill definition
|
|
@@ -53,7 +53,7 @@ agentic-awesome-skills/
|
|
|
53
53
|
│ │ └── 📁 2d-games/
|
|
54
54
|
│ │ └── 📄 SKILL.md ← Nested skills also supported
|
|
55
55
|
│ │
|
|
56
|
-
│ └── ... (2,
|
|
56
|
+
│ └── ... (2,107+ total)
|
|
57
57
|
│
|
|
58
58
|
├── 📁 apps/
|
|
59
59
|
│ └── 📁 web-app/ ← Interactive browser
|
|
@@ -106,7 +106,7 @@ agentic-awesome-skills/
|
|
|
106
106
|
|
|
107
107
|
```
|
|
108
108
|
┌─────────────────────────┐
|
|
109
|
-
│ 2,
|
|
109
|
+
│ 2,107+ SKILLS │
|
|
110
110
|
└────────────┬────────────┘
|
|
111
111
|
│
|
|
112
112
|
┌────────────────────────┼────────────────────────┐
|
|
@@ -207,7 +207,7 @@ If you want a workspace-style manual install instead, cloning into `.agent/skill
|
|
|
207
207
|
│ ├── 📁 brainstorming/ │
|
|
208
208
|
│ ├── 📁 stripe-integration/ │
|
|
209
209
|
│ ├── 📁 react-best-practices/ │
|
|
210
|
-
│ └── ... (2,
|
|
210
|
+
│ └── ... (2,107+ total) │
|
|
211
211
|
└─────────────────────────────────────────┘
|
|
212
212
|
```
|
|
213
213
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: extract-document-data
|
|
3
|
+
description: Extract structured, grounded fields from documents — values cite their page, missing values abstain instead of hallucinating. Use for parsing invoices, payslips, statements, contracts.
|
|
4
|
+
category: document-verification
|
|
5
|
+
risk: critical
|
|
6
|
+
source: community
|
|
7
|
+
source_repo: Sketchjar/stipple-agent-skills
|
|
8
|
+
source_type: community
|
|
9
|
+
date_added: "2026-08-31"
|
|
10
|
+
author: Sketchjar
|
|
11
|
+
tags: [document-verification, fact-checking, stipple, authenticity]
|
|
12
|
+
tools: [claude, cursor, gemini, codex]
|
|
13
|
+
license: "Apache-2.0"
|
|
14
|
+
license_source: "https://github.com/Sketchjar/stipple-agent-skills/blob/main/LICENSE"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Extract Document Data
|
|
18
|
+
|
|
19
|
+
Extract structured JSON from documents with per-value grounding: every extracted value cites where it came from (page number, confidence), and values that aren't clearly present are reported in `not_found` rather than hallucinated. Uses the Stipple API (free anonymous tier).
|
|
20
|
+
|
|
21
|
+
## When to use
|
|
22
|
+
|
|
23
|
+
- Parsing payslips, invoices, bank statements, receipts, or contracts
|
|
24
|
+
- Converting unstructured documents to JSON for downstream systems
|
|
25
|
+
- Any extraction where hallucinated values are worse than missing values (lending, accounting, compliance)
|
|
26
|
+
|
|
27
|
+
## Instructions
|
|
28
|
+
|
|
29
|
+
1. **Get the document.** URL or local file path (PDF, PNG, JPEG, DOCX).
|
|
30
|
+
|
|
31
|
+
2. **Choose the extraction mode:**
|
|
32
|
+
- **Ad-hoc fields** — tell the API exactly which fields you want:
|
|
33
|
+
```bash
|
|
34
|
+
curl -X POST https://www.stipple.sh/v1/extract \
|
|
35
|
+
-F "file=@payslip.pdf" \
|
|
36
|
+
-F 'fields=[{"name":"employer_name"},{"name":"net_pay"},{"name":"pay_date"}]' \
|
|
37
|
+
-H "Authorization: Bearer $STIPPLE_API_KEY"
|
|
38
|
+
```
|
|
39
|
+
- **Template** — use a built-in schema: `payslip`, `tax_invoice`, `bank_statement`, `receipt`, `contract`
|
|
40
|
+
- **Schema-free** — omit `fields` and let the model extract what it finds
|
|
41
|
+
|
|
42
|
+
3. **Interpret the response.**
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mode": "schema_free",
|
|
47
|
+
"document_type": "payslip",
|
|
48
|
+
"pages_read": 1,
|
|
49
|
+
"fields": {
|
|
50
|
+
"employer_name": {"value": "Acme Cleaning Pty Ltd", "confidence": 0.95, "page": 1},
|
|
51
|
+
"net_pay": {"value": "2845.10", "confidence": 0.97, "page": 1}
|
|
52
|
+
},
|
|
53
|
+
"not_found": ["ytd_tax"]
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- Every value carries `confidence` (the model's self-report) and `page` (grounding)
|
|
58
|
+
- `not_found[]` lists requested fields the model couldn't find — **absences are reported, never guessed**
|
|
59
|
+
- `pages_read` shows how many pages were processed (page limits apply per document)
|
|
60
|
+
|
|
61
|
+
4. **Report honestly.** This is *extraction, not verification* — values are what the document **shows**, not proof it's genuine:
|
|
62
|
+
- "Employer: Acme Cleaning Pty Ltd (confidence 0.95, page 1)"
|
|
63
|
+
- "ytd_tax: not found in document" — never "ytd_tax: 0" or a guess
|
|
64
|
+
- For "is this document genuine?", pair with the `verify-document` skill first
|
|
65
|
+
|
|
66
|
+
## Output format
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Payslip fields (grounded, not guessed):
|
|
70
|
+
|
|
71
|
+
Employer Acme Cleaning Pty Ltd (confidence 0.95, page 1)
|
|
72
|
+
Employee J. Citizen (confidence 0.98, page 1)
|
|
73
|
+
Net pay 2,845.10 (confidence 0.97, page 1)
|
|
74
|
+
Superannuation 268.20 (confidence 0.93, page 1)
|
|
75
|
+
|
|
76
|
+
not_found: ytd_tax
|
|
77
|
+
(absences are reported, never hallucinated)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Limitations and Safety
|
|
81
|
+
|
|
82
|
+
- Invoices, statements, payslips, and contracts often contain sensitive personal,
|
|
83
|
+
financial, or commercial data. Obtain explicit approval before uploading them to
|
|
84
|
+
a hosted third party, minimize the submitted content, and confirm current
|
|
85
|
+
retention, residency, access, and deletion terms.
|
|
86
|
+
- Confidence and page grounding do not prove that an extracted value is correct or
|
|
87
|
+
that the source document is authentic. Reconcile consequential values against the
|
|
88
|
+
original document and authoritative systems before payment, lending, accounting,
|
|
89
|
+
compliance, or legal action.
|
|
90
|
+
- Keep the original file and extraction response so a human reviewer can reproduce
|
|
91
|
+
and correct disputed fields.
|
|
92
|
+
|
|
93
|
+
## Notes
|
|
94
|
+
|
|
95
|
+
- Costs 1 credit per page read by the model (minimum 1); free weekly allowance applies
|
|
96
|
+
- Templates: `payslip`, `tax_invoice`, `bank_statement`, `receipt`, `contract` — pass as the `template` form field
|
|
97
|
+
- Tables are extracted with structure preserved; multi-page documents are processed page by page
|
|
98
|
+
- Pairs with `verify-document` (run first, for authenticity) — an extracted value from a tampered document is still wrong
|
|
99
|
+
- Free key at https://www.stipple.sh for metering beyond the anonymous allowance
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: find-matching-tenders
|
|
3
|
+
description: Find open AU/NZ government tenders matching what a company does, ranked by fit with why and gap analysis. Use when the user asks to find tenders, bid opportunities, government contracts, or RFPs for their business (or a client's).
|
|
4
|
+
category: document-verification
|
|
5
|
+
risk: critical
|
|
6
|
+
source: community
|
|
7
|
+
source_repo: Sketchjar/stipple-agent-skills
|
|
8
|
+
source_type: community
|
|
9
|
+
date_added: "2026-08-31"
|
|
10
|
+
author: Sketchjar
|
|
11
|
+
tags: [document-verification, fact-checking, stipple, authenticity]
|
|
12
|
+
tools: [claude, cursor, gemini, codex]
|
|
13
|
+
license: "Apache-2.0"
|
|
14
|
+
license_source: "https://github.com/Sketchjar/stipple-agent-skills/blob/main/LICENSE"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Find Matching Tenders
|
|
18
|
+
|
|
19
|
+
Search live AU/NZ government tenders, rank them against what a company actually does (read from its website), and explain why each opportunity is relevant — including the capability gaps to prepare evidence for. Tender search is free forever on the Stipple API; no API key or signup needed.
|
|
20
|
+
|
|
21
|
+
## When to use
|
|
22
|
+
|
|
23
|
+
- Business development: "find tenders we could bid on"
|
|
24
|
+
- Market research: "what government work is out there for cybersecurity firms in NSW?"
|
|
25
|
+
- Bid pipeline maintenance: "check for new construction tenders this week"
|
|
26
|
+
|
|
27
|
+
## Instructions
|
|
28
|
+
|
|
29
|
+
1. **Get the company profile.** Ask for the company's website URL. If the user provides a description instead, skip resolution and use their description directly as capability context.
|
|
30
|
+
|
|
31
|
+
2. **Resolve the company** (optional but improves matching):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
curl -X POST https://www.stipple.sh/v1/companies/resolve \
|
|
35
|
+
-H "Content-Type: application/json" \
|
|
36
|
+
-d '{"query": "https://your-company.com"}'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Returns the company's registered name, ABN, jurisdiction, and status.
|
|
40
|
+
|
|
41
|
+
3. **Search open tenders** (free, no key):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
curl "https://www.stipple.sh/v1/tenders?q=construction&jurisdiction=AU&limit=10"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Filters: `q` (keyword), `jurisdiction` (AU, AU-NSW, AU-VIC, NZ, ...), `limit`, `offset`.
|
|
48
|
+
|
|
49
|
+
4. **Rank against the company** (optional, uses free weekly allowance):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
curl -X POST https://www.stipple.sh/v1/tenders/match \
|
|
53
|
+
-H "Content-Type: application/json" \
|
|
54
|
+
-d '{"url": "https://your-company.com", "jurisdiction": "AU", "limit": 5}'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Returns ranked matches with `why[]` (why it fits) and `gaps[]` (capability evidence to prepare).
|
|
58
|
+
|
|
59
|
+
5. **Check data provenance** if results seem thin (free):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
curl "https://www.stipple.sh/v1/tenders/sources"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Shows which government feeds are indexed and their freshness — explains why a search can be empty.
|
|
66
|
+
|
|
67
|
+
6. **Report honestly.** Match scores are *fit signals*, not win probabilities. Present `gaps[]` as "prepare evidence for this", not disqualification.
|
|
68
|
+
|
|
69
|
+
## Output format
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
Found 403 open tenders matching "construction" (AU/NZ)
|
|
73
|
+
|
|
74
|
+
#1 [AU-NSW] Digital platform modernisation services
|
|
75
|
+
buyer: NSW Department of ...
|
|
76
|
+
why: matches 'cloud migration', 'API integration' from your services
|
|
77
|
+
gap: no evidence found for 'ICT security assessment'
|
|
78
|
+
closes: 2026-09-15
|
|
79
|
+
https://...
|
|
80
|
+
|
|
81
|
+
#2 [NZ] ...
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Limitations and Safety
|
|
85
|
+
|
|
86
|
+
- The hosted service receives the search terms and, for matching, the supplied
|
|
87
|
+
company URL or description. Obtain approval before sending private capability or
|
|
88
|
+
client information and do not include secrets or non-public bid strategy.
|
|
89
|
+
- Feed coverage and freshness can be incomplete. Confirm eligibility, scope,
|
|
90
|
+
amendments, deadlines, and submission instructions on the issuing authority's
|
|
91
|
+
primary tender page before investing time or submitting a bid.
|
|
92
|
+
- Match scores and gap analysis are triage signals, not procurement advice, an
|
|
93
|
+
eligibility ruling, or a probability of winning.
|
|
94
|
+
|
|
95
|
+
## Notes
|
|
96
|
+
|
|
97
|
+
- Tender search and source listing are free forever — no key, no credits
|
|
98
|
+
- Matching costs a small number of credits on the free weekly allowance
|
|
99
|
+
- For ongoing monitoring, pair with a cron job that re-runs step 3 daily and diffs results
|
|
100
|
+
- Data sources: NZ GETS, NSW eTendering, VendorPanel, and other government feeds
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: grok-delegate
|
|
3
3
|
description: Delegate coding tasks to the Grok Build CLI only when the user explicitly
|
|
4
4
|
requests it, while the orchestrator retains review and landing responsibility.
|
|
5
|
-
risk:
|
|
5
|
+
risk: critical
|
|
6
6
|
category: agent-orchestration
|
|
7
7
|
source: https://github.com/amElnagdy/delegate-skills
|
|
8
8
|
source_repo: amElnagdy/delegate-skills
|
|
@@ -37,6 +37,7 @@ and durable execution without managing queues or workers.
|
|
|
37
37
|
## Scope
|
|
38
38
|
|
|
39
39
|
- redis-queues -> bullmq-specialist
|
|
40
|
+
- serverless-queues -> upstash-qstash
|
|
40
41
|
- workflow-orchestration -> temporal-craftsman
|
|
41
42
|
- message-streaming -> event-architect
|
|
42
43
|
- infrastructure -> infra-architect
|
|
@@ -388,6 +389,7 @@ Fix action: Add idempotency: 'event.data.orderId' to function config
|
|
|
388
389
|
### Delegation Triggers
|
|
389
390
|
|
|
390
391
|
- redis|queue infrastructure|bullmq -> bullmq-specialist (Need Redis-based queue with existing infrastructure)
|
|
392
|
+
- serverless queue|http queue|scheduled http -> upstash-qstash (Need plain HTTP delivery and cron without an event framework)
|
|
391
393
|
- saga|compensation|rollback|long-running workflow -> temporal-craftsman (Need complex workflow orchestration with compensation)
|
|
392
394
|
- event sourcing|event store|cqrs -> event-architect (Need event sourcing patterns)
|
|
393
395
|
- vercel|deploy|production -> vercel-deployment (Need deployment configuration)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: kimi-delegate
|
|
3
3
|
description: Delegate coding tasks to the Kimi Code CLI (`kimi`) only when the user
|
|
4
4
|
explicitly requests it, while the orchestrator retains review and landing responsibility.
|
|
5
|
-
risk:
|
|
5
|
+
risk: critical
|
|
6
6
|
category: agent-orchestration
|
|
7
7
|
source: https://github.com/amElnagdy/delegate-skills
|
|
8
8
|
source_repo: amElnagdy/delegate-skills
|
|
@@ -90,9 +90,10 @@ Maintain a serializable manifest with these top-level fields:
|
|
|
90
90
|
### 7. Validate and report
|
|
91
91
|
|
|
92
92
|
- Scaffold a clean starter with `scripts/scaffold_template.py` when a neutral implementation is needed.
|
|
93
|
-
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
- The bundled helpers parse only the restricted data-literal assignment used by
|
|
94
|
+
the starter. They reject expressions, function calls, duplicate keys,
|
|
95
|
+
unsupported syntax, trailing statements, and oversized manifests without
|
|
96
|
+
executing JavaScript. Keep runtime theme configs data-only as well.
|
|
96
97
|
- Run `scripts/validate_manifest.py` on each theme config before rendering.
|
|
97
98
|
- Capture desktop and mobile screenshots with a real browser. Inspect them directly if visual capability is available.
|
|
98
99
|
- Run `scripts/measure_preview.py` on the pure field screenshot and retain measured chromatic ratio, luminance statistics, per-color coverage, and effective share.
|
|
@@ -122,7 +123,8 @@ Use the bundled starter under `assets/starter/` as a neutral base. Copy only the
|
|
|
122
123
|
### scripts/
|
|
123
124
|
|
|
124
125
|
- `scaffold_template.py`: copy the neutral starter for `opal`, `obsidian`, or both.
|
|
125
|
-
- `
|
|
126
|
+
- `manifest_parser.py`: statically parse the restricted data-only manifest grammar without executing JavaScript.
|
|
127
|
+
- `validate_manifest.py`: validate required manifest fields and ranges.
|
|
126
128
|
- `measure_preview.py`: measure a rendered pure-field PNG against the configured OKLCH palette.
|
|
127
129
|
|
|
128
130
|
### references/
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Parse the data-only subset used by Liuguang theme manifests."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import pathlib
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
MAX_MANIFEST_BYTES = 256 * 1024
|
|
13
|
+
MAX_NESTING_DEPTH = 64
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ManifestSyntaxError(ValueError):
|
|
17
|
+
"""Raised when a manifest contains code or unsupported JavaScript syntax."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Token:
|
|
22
|
+
kind: str
|
|
23
|
+
value: str
|
|
24
|
+
offset: int
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
TOKEN_RE = re.compile(
|
|
28
|
+
r"""
|
|
29
|
+
(?P<whitespace>\s+)
|
|
30
|
+
| (?P<line_comment>//[^\r\n]*)
|
|
31
|
+
| (?P<block_comment>/\*.*?\*/)
|
|
32
|
+
| (?P<string>"(?:\\["\\/bfnrt]|\\u[0-9A-Fa-f]{4}|[^"\\\x00-\x1f])*")
|
|
33
|
+
| (?P<number>-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)
|
|
34
|
+
| (?P<identifier>[A-Za-z_$][A-Za-z0-9_$]*)
|
|
35
|
+
| (?P<punctuation>[{}\[\]:,;=.])
|
|
36
|
+
""",
|
|
37
|
+
re.VERBOSE | re.DOTALL,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def tokenize(source: str) -> list[Token]:
|
|
42
|
+
tokens: list[Token] = []
|
|
43
|
+
offset = 0
|
|
44
|
+
while offset < len(source):
|
|
45
|
+
match = TOKEN_RE.match(source, offset)
|
|
46
|
+
if match is None:
|
|
47
|
+
raise ManifestSyntaxError(f"unsupported syntax at byte offset {offset}")
|
|
48
|
+
kind = match.lastgroup
|
|
49
|
+
if kind not in {"whitespace", "line_comment", "block_comment"}:
|
|
50
|
+
tokens.append(Token(kind or "unknown", match.group(0), offset))
|
|
51
|
+
offset = match.end()
|
|
52
|
+
tokens.append(Token("eof", "", len(source)))
|
|
53
|
+
return tokens
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Parser:
|
|
57
|
+
def __init__(self, source: str) -> None:
|
|
58
|
+
self.tokens = tokenize(source)
|
|
59
|
+
self.index = 0
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def current(self) -> Token:
|
|
63
|
+
return self.tokens[self.index]
|
|
64
|
+
|
|
65
|
+
def take(self, value: str | None = None, kind: str | None = None) -> Token:
|
|
66
|
+
token = self.current
|
|
67
|
+
if value is not None and token.value != value:
|
|
68
|
+
raise ManifestSyntaxError(
|
|
69
|
+
f"expected {value!r} at byte offset {token.offset}, found {token.value!r}"
|
|
70
|
+
)
|
|
71
|
+
if kind is not None and token.kind != kind:
|
|
72
|
+
raise ManifestSyntaxError(
|
|
73
|
+
f"expected {kind} at byte offset {token.offset}, found {token.value!r}"
|
|
74
|
+
)
|
|
75
|
+
self.index += 1
|
|
76
|
+
return token
|
|
77
|
+
|
|
78
|
+
def accept(self, value: str) -> bool:
|
|
79
|
+
if self.current.value != value:
|
|
80
|
+
return False
|
|
81
|
+
self.index += 1
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def parse(self) -> dict:
|
|
85
|
+
self.take("window")
|
|
86
|
+
self.take(".")
|
|
87
|
+
self.take("SPECTRAL_THEME")
|
|
88
|
+
self.take("=")
|
|
89
|
+
value = self.parse_value(0)
|
|
90
|
+
self.accept(";")
|
|
91
|
+
self.take(kind="eof")
|
|
92
|
+
if not isinstance(value, dict):
|
|
93
|
+
raise ManifestSyntaxError("SPECTRAL_THEME must be an object")
|
|
94
|
+
return value
|
|
95
|
+
|
|
96
|
+
def parse_value(self, depth: int):
|
|
97
|
+
if depth > MAX_NESTING_DEPTH:
|
|
98
|
+
raise ManifestSyntaxError("manifest nesting limit exceeded")
|
|
99
|
+
token = self.current
|
|
100
|
+
if token.value == "{":
|
|
101
|
+
return self.parse_object(depth + 1)
|
|
102
|
+
if token.value == "[":
|
|
103
|
+
return self.parse_array(depth + 1)
|
|
104
|
+
if token.kind == "string":
|
|
105
|
+
self.index += 1
|
|
106
|
+
return json.loads(token.value)
|
|
107
|
+
if token.kind == "number":
|
|
108
|
+
self.index += 1
|
|
109
|
+
try:
|
|
110
|
+
return (
|
|
111
|
+
float(token.value)
|
|
112
|
+
if any(mark in token.value for mark in ".eE")
|
|
113
|
+
else int(token.value)
|
|
114
|
+
)
|
|
115
|
+
except ValueError as error:
|
|
116
|
+
raise ManifestSyntaxError("numeric literal exceeds the supported range") from error
|
|
117
|
+
if token.kind == "identifier" and token.value in {"true", "false", "null"}:
|
|
118
|
+
self.index += 1
|
|
119
|
+
return {"true": True, "false": False, "null": None}[token.value]
|
|
120
|
+
raise ManifestSyntaxError(
|
|
121
|
+
f"only data literals are allowed at byte offset {token.offset}; found {token.value!r}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def parse_object(self, depth: int) -> dict:
|
|
125
|
+
result: dict = {}
|
|
126
|
+
self.take("{")
|
|
127
|
+
if self.accept("}"):
|
|
128
|
+
return result
|
|
129
|
+
while True:
|
|
130
|
+
token = self.current
|
|
131
|
+
if token.kind == "identifier":
|
|
132
|
+
key = self.take(kind="identifier").value
|
|
133
|
+
elif token.kind == "string":
|
|
134
|
+
key = json.loads(self.take(kind="string").value)
|
|
135
|
+
else:
|
|
136
|
+
raise ManifestSyntaxError(
|
|
137
|
+
f"object keys must be identifiers or strings at byte offset {token.offset}"
|
|
138
|
+
)
|
|
139
|
+
if key in result:
|
|
140
|
+
raise ManifestSyntaxError(f"duplicate object key: {key}")
|
|
141
|
+
self.take(":")
|
|
142
|
+
result[key] = self.parse_value(depth)
|
|
143
|
+
if self.accept("}"):
|
|
144
|
+
return result
|
|
145
|
+
self.take(",")
|
|
146
|
+
if self.accept("}"):
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
def parse_array(self, depth: int) -> list:
|
|
150
|
+
result: list = []
|
|
151
|
+
self.take("[")
|
|
152
|
+
if self.accept("]"):
|
|
153
|
+
return result
|
|
154
|
+
while True:
|
|
155
|
+
result.append(self.parse_value(depth))
|
|
156
|
+
if self.accept("]"):
|
|
157
|
+
return result
|
|
158
|
+
self.take(",")
|
|
159
|
+
if self.accept("]"):
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_manifest(source: str) -> dict:
|
|
164
|
+
return Parser(source).parse()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def load_manifest(path: pathlib.Path) -> dict:
|
|
168
|
+
with path.open("rb") as manifest:
|
|
169
|
+
raw = manifest.read(MAX_MANIFEST_BYTES + 1)
|
|
170
|
+
if len(raw) > MAX_MANIFEST_BYTES:
|
|
171
|
+
raise ManifestSyntaxError(
|
|
172
|
+
f"manifest exceeds the {MAX_MANIFEST_BYTES}-byte size limit"
|
|
173
|
+
)
|
|
174
|
+
try:
|
|
175
|
+
source = raw.decode("utf-8-sig")
|
|
176
|
+
except UnicodeDecodeError as error:
|
|
177
|
+
raise ManifestSyntaxError("manifest must be valid UTF-8") from error
|
|
178
|
+
return parse_manifest(source)
|
|
@@ -6,9 +6,10 @@ from __future__ import annotations
|
|
|
6
6
|
import argparse
|
|
7
7
|
import json
|
|
8
8
|
import pathlib
|
|
9
|
-
import subprocess
|
|
10
9
|
import sys
|
|
11
10
|
|
|
11
|
+
from manifest_parser import ManifestSyntaxError, load_manifest
|
|
12
|
+
|
|
12
13
|
try:
|
|
13
14
|
import numpy as np
|
|
14
15
|
from PIL import Image
|
|
@@ -29,21 +30,6 @@ except ModuleNotFoundError as exc:
|
|
|
29
30
|
raise SystemExit(2)
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
def load_config(path: pathlib.Path) -> dict:
|
|
33
|
-
source = (
|
|
34
|
-
"global.window={};require(require('path').resolve(process.argv[1]));"
|
|
35
|
-
"process.stdout.write(JSON.stringify(window.SPECTRAL_THEME));"
|
|
36
|
-
)
|
|
37
|
-
result = subprocess.run(
|
|
38
|
-
["node", "-e", source, str(path)],
|
|
39
|
-
check=True,
|
|
40
|
-
capture_output=True,
|
|
41
|
-
text=True,
|
|
42
|
-
encoding="utf-8",
|
|
43
|
-
)
|
|
44
|
-
return json.loads(result.stdout)
|
|
45
|
-
|
|
46
|
-
|
|
47
33
|
def srgb_to_oklab(rgb: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
48
34
|
linear = np.where(
|
|
49
35
|
rgb <= 0.04045,
|
|
@@ -129,7 +115,18 @@ def main() -> None:
|
|
|
129
115
|
parser.add_argument("--image", required=True, type=pathlib.Path)
|
|
130
116
|
parser.add_argument("--config", required=True, type=pathlib.Path)
|
|
131
117
|
args = parser.parse_args()
|
|
132
|
-
|
|
118
|
+
try:
|
|
119
|
+
config = load_manifest(args.config.resolve())
|
|
120
|
+
except (ManifestSyntaxError, OSError) as error:
|
|
121
|
+
print(
|
|
122
|
+
json.dumps(
|
|
123
|
+
{"status": "invalid", "errors": [str(error)]},
|
|
124
|
+
ensure_ascii=False,
|
|
125
|
+
indent=2,
|
|
126
|
+
),
|
|
127
|
+
file=sys.stderr,
|
|
128
|
+
)
|
|
129
|
+
raise SystemExit(1) from error
|
|
133
130
|
payload = {
|
|
134
131
|
"schemaVersion": "1.0",
|
|
135
132
|
"theme": config["label"],
|
|
@@ -8,23 +8,9 @@ import json
|
|
|
8
8
|
import math
|
|
9
9
|
import pathlib
|
|
10
10
|
import re
|
|
11
|
-
import subprocess
|
|
12
11
|
import sys
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
def load_manifest(path: pathlib.Path) -> dict:
|
|
16
|
-
program = (
|
|
17
|
-
"global.window={};require(require('path').resolve(process.argv[1]));"
|
|
18
|
-
"process.stdout.write(JSON.stringify(window.SPECTRAL_THEME));"
|
|
19
|
-
)
|
|
20
|
-
result = subprocess.run(
|
|
21
|
-
["node", "-e", program, str(path)],
|
|
22
|
-
check=True,
|
|
23
|
-
capture_output=True,
|
|
24
|
-
text=True,
|
|
25
|
-
encoding="utf-8",
|
|
26
|
-
)
|
|
27
|
-
return json.loads(result.stdout)
|
|
13
|
+
from manifest_parser import ManifestSyntaxError, load_manifest
|
|
28
14
|
|
|
29
15
|
|
|
30
16
|
def require(condition: bool, message: str, errors: list[str]) -> None:
|
|
@@ -161,7 +147,17 @@ def main() -> None:
|
|
|
161
147
|
parser = argparse.ArgumentParser()
|
|
162
148
|
parser.add_argument("config", type=pathlib.Path)
|
|
163
149
|
args = parser.parse_args()
|
|
164
|
-
|
|
150
|
+
try:
|
|
151
|
+
config = load_manifest(args.config.resolve())
|
|
152
|
+
except (ManifestSyntaxError, OSError) as error:
|
|
153
|
+
print(
|
|
154
|
+
json.dumps(
|
|
155
|
+
{"status": "invalid", "errors": [str(error)]},
|
|
156
|
+
ensure_ascii=False,
|
|
157
|
+
indent=2,
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
raise SystemExit(1) from error
|
|
165
161
|
errors = validate(config)
|
|
166
162
|
if errors:
|
|
167
163
|
print(json.dumps({"status": "invalid", "errors": errors}, ensure_ascii=False, indent=2))
|