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.
Files changed (60) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +11 -1
  2. package/bundled-skills/agent-qa-authoring/SKILL.md +3 -1
  3. package/bundled-skills/agy-delegate/SKILL.md +1 -1
  4. package/bundled-skills/aider-delegate/SKILL.md +1 -1
  5. package/bundled-skills/api-rate-limit-handler/SKILL.md +1 -1
  6. package/bundled-skills/atlas-cloud-media/SKILL.md +52 -18
  7. package/bundled-skills/babysit-pr/SKILL.md +1 -1
  8. package/bundled-skills/boost-asio-pro/references/pre-cpp20.md +8 -1
  9. package/bundled-skills/check-identity-pack/SKILL.md +88 -0
  10. package/bundled-skills/claude-delegate/SKILL.md +1 -1
  11. package/bundled-skills/cline-delegate/SKILL.md +1 -1
  12. package/bundled-skills/codex-delegate/SKILL.md +1 -1
  13. package/bundled-skills/commandcode-delegate/SKILL.md +1 -1
  14. package/bundled-skills/copilot-delegate/SKILL.md +1 -1
  15. package/bundled-skills/cursor-delegate/SKILL.md +1 -1
  16. package/bundled-skills/detect-ai-text/SKILL.md +91 -0
  17. package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
  18. package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
  19. package/bundled-skills/docs/maintainers/repo-growth-seo.md +1 -1
  20. package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
  21. package/bundled-skills/docs/users/aas-core.md +1 -1
  22. package/bundled-skills/docs/users/bundles.md +1 -1
  23. package/bundled-skills/docs/users/claude-code-skills.md +1 -1
  24. package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
  25. package/bundled-skills/docs/users/kiro-integration.md +1 -1
  26. package/bundled-skills/docs/users/usage.md +3 -3
  27. package/bundled-skills/docs/users/visual-guide.md +4 -4
  28. package/bundled-skills/extract-document-data/SKILL.md +99 -0
  29. package/bundled-skills/find-matching-tenders/SKILL.md +100 -0
  30. package/bundled-skills/grok-delegate/SKILL.md +1 -1
  31. package/bundled-skills/inngest/SKILL.md +2 -0
  32. package/bundled-skills/kimi-delegate/SKILL.md +1 -1
  33. package/bundled-skills/liuguang-banlan-ui/SKILL.md +6 -4
  34. package/bundled-skills/liuguang-banlan-ui/scripts/manifest_parser.py +178 -0
  35. package/bundled-skills/liuguang-banlan-ui/scripts/measure_preview.py +14 -17
  36. package/bundled-skills/liuguang-banlan-ui/scripts/validate_manifest.py +12 -16
  37. package/bundled-skills/lovable-cleanup/SKILL.md +32 -6
  38. package/bundled-skills/lovable-cleanup/references/favicon-vercel-cleanup.md +106 -0
  39. package/bundled-skills/lovable-cleanup/scripts/write-transparent-favicon.js +88 -0
  40. package/bundled-skills/multi-source-search/scripts/validate_report.py +5 -0
  41. package/bundled-skills/omp-delegate/SKILL.md +1 -1
  42. package/bundled-skills/opencode-delegate/SKILL.md +1 -1
  43. package/bundled-skills/pentest-tools/references/pentest-ai-agents-matrix.md +8 -5
  44. package/bundled-skills/pi-delegate/SKILL.md +1 -1
  45. package/bundled-skills/prompt-caching/SKILL.md +3 -1
  46. package/bundled-skills/qoder-delegate/SKILL.md +1 -1
  47. package/bundled-skills/screen-adverse-media/SKILL.md +100 -0
  48. package/bundled-skills/slideops/SKILL.md +201 -0
  49. package/bundled-skills/trigger-dev/SKILL.md +2 -0
  50. package/bundled-skills/unsloth-finetuning/SKILL.md +45 -2
  51. package/bundled-skills/upstash-qstash/SKILL.md +1 -1
  52. package/bundled-skills/upstash-ratelimit/SKILL.md +186 -0
  53. package/bundled-skills/upstash-redis/SKILL.md +156 -0
  54. package/bundled-skills/verify-citations/SKILL.md +88 -0
  55. package/bundled-skills/verify-document/SKILL.md +94 -0
  56. package/bundled-skills/vibe-delegate/SKILL.md +1 -1
  57. package/bundled-skills/warp-delegate/SKILL.md +1 -1
  58. package/bundled-skills/zcode-delegate/SKILL.md +1 -1
  59. package/package.json +1 -1
  60. 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,097+ skill files** to your computer (default: `~/.agents/skills/`; or a custom path like `~/.agent/skills/` if you used `--path`)
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,097+ skills at once. Here's a sensible approach:
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,097+ skills installed locally, you should **not** concatenate every `SKILL.md` into a single system prompt or context block.
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,097+ skills live here
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,097+ total)
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,097+ SKILLS │
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,097+ total) │
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: safe
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: safe
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
- - Treat JavaScript manifests as executable code: inspect them first and run the
94
- bundled helpers only on reviewed, locally authored configuration. Never pass
95
- an untrusted or freshly downloaded manifest to either Python helper.
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
- - `validate_manifest.py`: parse a JavaScript manifest through Node and validate required fields and ranges.
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
- config = load_config(args.config.resolve())
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
- config = load_manifest(args.config.resolve())
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))