claudeup 4.37.0 → 4.38.0
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/package.json +4 -4
- package/scripts/verify-community-registry.ts +272 -0
- package/src/__tests__/community-fetch.test.ts +545 -0
- package/src/__tests__/community-registry.test.ts +269 -0
- package/src/__tests__/community-staleness.test.ts +722 -0
- package/src/__tests__/open-file.test.ts +59 -0
- package/src/__tests__/style-wrap.test.ts +220 -0
- package/src/__tests__/styles-manager.test.ts +1124 -0
- package/src/__tests__/styles-origins.test.ts +416 -0
- package/src/__tests__/styles-screen-state.test.ts +460 -0
- package/src/__tests__/styles-status-line.test.ts +72 -0
- package/src/__tests__/styles-sync.test.ts +452 -0
- package/src/__tests__/tabbar-layout.test.ts +62 -0
- package/src/__tests__/terminology-filler.test.ts +214 -0
- package/src/data/community-styles.ts +521 -0
- package/src/main.tsx +15 -0
- package/src/services/catalog-cache-store.ts +101 -7
- package/src/services/community-fetcher.ts +90 -0
- package/src/services/community-styles.ts +1194 -0
- package/src/services/styles-manager.ts +1400 -0
- package/src/services/terminology-filler.ts +266 -0
- package/src/ui/App.tsx +15 -3
- package/src/ui/adapters/stylesAdapter.ts +403 -0
- package/src/ui/components/TabBar.tsx +43 -9
- package/src/ui/components/primitives/ActionHints.tsx +4 -1
- package/src/ui/components/primitives/ListCategoryRow.tsx +10 -1
- package/src/ui/registry.ts +6 -0
- package/src/ui/renderers/styleRenderers.tsx +809 -0
- package/src/ui/screens/StylesScreen.tsx +1089 -0
- package/src/ui/screens/index.ts +1 -0
- package/src/ui/state/reducer.ts +113 -1
- package/src/ui/state/types.ts +60 -2
- package/src/utils/open-file.ts +84 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* community-styles.ts — the curated registry of third-party output styles.
|
|
3
|
+
*
|
|
4
|
+
* COORDINATES ONLY. This file ships `(repo, path, ref, licence, attribution)`
|
|
5
|
+
* and never a byte of anyone else's style text. The text is fetched from the
|
|
6
|
+
* source repository on an explicit user action and cached outside
|
|
7
|
+
* `~/.claude/output-styles/`; nothing here redistributes it.
|
|
8
|
+
*
|
|
9
|
+
* ## What earns an entry
|
|
10
|
+
*
|
|
11
|
+
* A repo qualifies **iff** walking its git tree finds at least one `*.md` under
|
|
12
|
+
* a styles directory. `SKILL.md` count is irrelevant. That rule is not
|
|
13
|
+
* pedantry: the two highest-starred names in this space ship ZERO style files
|
|
14
|
+
* (see `DENIED_REPOS`), so a registry built from a popularity list would 404 on
|
|
15
|
+
* its most prominent entries.
|
|
16
|
+
*
|
|
17
|
+
* Paths are VERIFIED, never inferred. `scripts/verify-community-registry.ts`
|
|
18
|
+
* lists the real `.md` blobs in a repo (`--discover`) and re-fetches every
|
|
19
|
+
* committed entry (`--check`). A wrong path is a 404 the user sees, and the
|
|
20
|
+
* error message blames us for it, correctly.
|
|
21
|
+
*
|
|
22
|
+
* ## What this file must never become
|
|
23
|
+
*
|
|
24
|
+
* An arbitrary-URL fetcher. The set of fetchable things being a constant in a
|
|
25
|
+
* reviewed commit is the single strongest control on a feature whose output
|
|
26
|
+
* becomes system-prompt text.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { isValidGitHubRepo } from "../utils/string-utils.js";
|
|
30
|
+
|
|
31
|
+
/** SPDX id, or null when the repo grants no redistribution licence. */
|
|
32
|
+
export type StyleLicence = "MIT" | "AGPL-3.0" | "Apache-2.0" | null;
|
|
33
|
+
|
|
34
|
+
/** One upstream repository. Shared by every style it ships. */
|
|
35
|
+
export interface CommunityStyleSource {
|
|
36
|
+
/** Registry slug, and the first half of every id from this repo. Never changes. */
|
|
37
|
+
id: string;
|
|
38
|
+
/** `owner/name`, as GitHub spells it. */
|
|
39
|
+
repo: string;
|
|
40
|
+
/**
|
|
41
|
+
* Directory the styles live in. NOT a convention — four of these use
|
|
42
|
+
* `output-styles/` and one uses `.claude/output-styles/`, so nothing here may
|
|
43
|
+
* glob a fixed name.
|
|
44
|
+
*/
|
|
45
|
+
dir: string;
|
|
46
|
+
/** Git ref to fetch content from. `HEAD` means "whatever is current". */
|
|
47
|
+
ref: string;
|
|
48
|
+
licence: StyleLicence;
|
|
49
|
+
licenceUrl: string | null;
|
|
50
|
+
/** Display attribution. */
|
|
51
|
+
author: string;
|
|
52
|
+
homepage: string;
|
|
53
|
+
/** Curation-time snapshot of the last push, for context only. */
|
|
54
|
+
pushedAt: string;
|
|
55
|
+
/**
|
|
56
|
+
* Retired sources are no longer offered but still resolve, so a committed
|
|
57
|
+
* `.claude/style.json` naming one keeps working. Entries are never deleted.
|
|
58
|
+
*/
|
|
59
|
+
retired?: true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CommunityStyle {
|
|
63
|
+
/** `<sourceId>--<slug>`. The stable coordinate id — see the note below. */
|
|
64
|
+
id: string;
|
|
65
|
+
sourceId: string;
|
|
66
|
+
/** Path within the repo, INCLUDING the source's `dir`. Verified, never inferred. */
|
|
67
|
+
path: string;
|
|
68
|
+
/** What the list shows: upstream's own `name`, recorded at curation time. */
|
|
69
|
+
displayName: string;
|
|
70
|
+
/** One line, ours, kept short — upstream descriptions vary wildly in length. */
|
|
71
|
+
summary: string;
|
|
72
|
+
/** Retired styles still resolve for an old declaration; see `retired` above. */
|
|
73
|
+
retired?: true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Why ids are coordinates rather than the file's own `frontmatter.name`.
|
|
78
|
+
*
|
|
79
|
+
* Three reasons, the third decisive:
|
|
80
|
+
* 1. It survives upstream renaming the style.
|
|
81
|
+
* 2. Upstream names are arbitrary human strings ("Zen Master", "ADHD") and a
|
|
82
|
+
* comma in one corrupts the comma-separated `style-imports` round-trip.
|
|
83
|
+
* 3. A teammate who clones a repo whose `.claude/style.json` names
|
|
84
|
+
* `community:attention-span--spartan` has fetched nothing. To offer "press f
|
|
85
|
+
* to fetch this" rather than a bare "missing", the id must resolve against
|
|
86
|
+
* this registry BEFORE any file exists locally. An id derived from the
|
|
87
|
+
* file's own frontmatter is unknowable until after the fetch.
|
|
88
|
+
*/
|
|
89
|
+
export const COMMUNITY_ID_SEPARATOR = "--";
|
|
90
|
+
|
|
91
|
+
// ─── Sources ─────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
export const COMMUNITY_SOURCES: CommunityStyleSource[] = [
|
|
94
|
+
{
|
|
95
|
+
id: "attention-span",
|
|
96
|
+
repo: "alexgreensh/attention-span",
|
|
97
|
+
dir: "output-styles",
|
|
98
|
+
ref: "HEAD",
|
|
99
|
+
licence: "AGPL-3.0",
|
|
100
|
+
licenceUrl:
|
|
101
|
+
"https://github.com/alexgreensh/attention-span/blob/HEAD/LICENSE",
|
|
102
|
+
author: "alexgreensh",
|
|
103
|
+
homepage: "https://github.com/alexgreensh/attention-span",
|
|
104
|
+
pushedAt: "2026-08-15",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "claude-mods",
|
|
108
|
+
repo: "0xDarkMatter/claude-mods",
|
|
109
|
+
dir: "output-styles",
|
|
110
|
+
ref: "HEAD",
|
|
111
|
+
licence: "MIT",
|
|
112
|
+
licenceUrl: "https://github.com/0xDarkMatter/claude-mods/blob/HEAD/LICENSE",
|
|
113
|
+
author: "0xDarkMatter",
|
|
114
|
+
homepage: "https://github.com/0xDarkMatter/claude-mods",
|
|
115
|
+
pushedAt: "2026-08-15",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "lej-output-fixer",
|
|
119
|
+
repo: "justfinethanku/LEJ-output-fixer",
|
|
120
|
+
dir: "output-styles",
|
|
121
|
+
ref: "HEAD",
|
|
122
|
+
licence: "MIT",
|
|
123
|
+
licenceUrl:
|
|
124
|
+
"https://github.com/justfinethanku/LEJ-output-fixer/blob/HEAD/LICENSE",
|
|
125
|
+
author: "justfinethanku",
|
|
126
|
+
homepage: "https://github.com/justfinethanku/LEJ-output-fixer",
|
|
127
|
+
pushedAt: "2026-08-09",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
// The only source whose styles are NOT in `output-styles/`. Shipping it is
|
|
131
|
+
// what keeps the per-entry `path` exercised in production rather than
|
|
132
|
+
// merely designed for.
|
|
133
|
+
id: "hesreallyhim",
|
|
134
|
+
repo: "hesreallyhim/awesome-claude-code-output-styles-that-i-really-like",
|
|
135
|
+
dir: ".claude/output-styles",
|
|
136
|
+
ref: "HEAD",
|
|
137
|
+
licence: "MIT",
|
|
138
|
+
licenceUrl:
|
|
139
|
+
"https://github.com/hesreallyhim/awesome-claude-code-output-styles-that-i-really-like/blob/HEAD/LICENSE",
|
|
140
|
+
author: "hesreallyhim",
|
|
141
|
+
homepage:
|
|
142
|
+
"https://github.com/hesreallyhim/awesome-claude-code-output-styles-that-i-really-like",
|
|
143
|
+
pushedAt: "2025-11-21",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "nattergabriel",
|
|
147
|
+
repo: "nattergabriel/claude-code-output-styles",
|
|
148
|
+
dir: "output-styles",
|
|
149
|
+
ref: "HEAD",
|
|
150
|
+
licence: "MIT",
|
|
151
|
+
licenceUrl:
|
|
152
|
+
"https://github.com/nattergabriel/claude-code-output-styles/blob/HEAD/LICENSE",
|
|
153
|
+
author: "nattergabriel",
|
|
154
|
+
homepage: "https://github.com/nattergabriel/claude-code-output-styles",
|
|
155
|
+
pushedAt: "2026-04-03",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "simple-english",
|
|
159
|
+
repo: "AminBlg/SimpleEnglish",
|
|
160
|
+
dir: "output-styles",
|
|
161
|
+
ref: "HEAD",
|
|
162
|
+
licence: "MIT",
|
|
163
|
+
licenceUrl: "https://github.com/AminBlg/SimpleEnglish/blob/HEAD/LICENSE",
|
|
164
|
+
author: "AminBlg",
|
|
165
|
+
homepage: "https://github.com/AminBlg/SimpleEnglish",
|
|
166
|
+
pushedAt: "2026-08-17",
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
// ─── Styles ──────────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 22 entries across the 5 sources above.
|
|
174
|
+
*
|
|
175
|
+
* Every `path` and every `displayName` here was produced by
|
|
176
|
+
* `scripts/verify-community-registry.ts --discover`, which lists the repo's
|
|
177
|
+
* real `.md` blobs and reads each file's own `name`. None was typed from a
|
|
178
|
+
* README. `--check` was clean before this was committed.
|
|
179
|
+
*
|
|
180
|
+
* `displayName` is upstream's `name` verbatim, warts included — `claude-mods`
|
|
181
|
+
* writes lowercase slugs, `hesreallyhim` writes title case, and
|
|
182
|
+
* `existentialist-poet.md` calls itself "Existential Poet". Normalising any of
|
|
183
|
+
* that would put a name in the list that appears nowhere upstream.
|
|
184
|
+
*
|
|
185
|
+
* `summary` is OURS: one line, under 100 characters, in our register. Upstream
|
|
186
|
+
* descriptions run from 40 to 200+ characters and several are marketing.
|
|
187
|
+
*
|
|
188
|
+
* Not everything importable is listed. `claude-mods` and `nattergabriel` ship
|
|
189
|
+
* 13 each and 5 of each are here — the rest are unreviewed, and adding one is a
|
|
190
|
+
* one-line diff, so deferring costs nothing. Entries are never DELETED, only
|
|
191
|
+
* marked `retired`: a deleted id breaks a committed `.claude/style.json` on
|
|
192
|
+
* every teammate's machine.
|
|
193
|
+
*/
|
|
194
|
+
export const COMMUNITY_STYLES: CommunityStyle[] = [
|
|
195
|
+
// ── attention-span ───────────────────────────────────────────────────────
|
|
196
|
+
{
|
|
197
|
+
id: "attention-span--attention-kind",
|
|
198
|
+
sourceId: "attention-span",
|
|
199
|
+
path: "output-styles/attention-kind.md",
|
|
200
|
+
displayName: "Attention-kind",
|
|
201
|
+
summary:
|
|
202
|
+
"ADHD-friendly. Plain English, answers front-loaded, expands only where it matters.",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: "attention-span--rundown",
|
|
206
|
+
sourceId: "attention-span",
|
|
207
|
+
path: "output-styles/rundown.md",
|
|
208
|
+
displayName: "Rundown",
|
|
209
|
+
summary:
|
|
210
|
+
"Briefing format. TL;DR first, then state as checkboxes and tagged choices.",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: "attention-span--spartan",
|
|
214
|
+
sourceId: "attention-span",
|
|
215
|
+
path: "output-styles/spartan.md",
|
|
216
|
+
displayName: "Spartan",
|
|
217
|
+
summary: "Blunt and answer-first. Arrow points, no warmth, no filler.",
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
// ── claude-mods (5 of 13) ────────────────────────────────────────────────
|
|
221
|
+
{
|
|
222
|
+
id: "claude-mods--executive",
|
|
223
|
+
sourceId: "claude-mods",
|
|
224
|
+
path: "output-styles/executive.md",
|
|
225
|
+
displayName: "executive",
|
|
226
|
+
summary:
|
|
227
|
+
"Stakeholder brief. Decisions, impact and timelines, with the engineering left out.",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: "claude-mods--mentor",
|
|
231
|
+
sourceId: "claude-mods",
|
|
232
|
+
path: "output-styles/mentor.md",
|
|
233
|
+
displayName: "mentor",
|
|
234
|
+
summary:
|
|
235
|
+
"Teaching voice. Explains why before what, and builds understanding as it goes.",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: "claude-mods--noir",
|
|
239
|
+
sourceId: "claude-mods",
|
|
240
|
+
path: "output-styles/noir.md",
|
|
241
|
+
displayName: "noir",
|
|
242
|
+
summary:
|
|
243
|
+
"Hard-boiled detective narrating your codebase. Pure persona, no workflow changes.",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: "claude-mods--pair",
|
|
247
|
+
sourceId: "claude-mods",
|
|
248
|
+
path: "output-styles/pair.md",
|
|
249
|
+
displayName: "pair",
|
|
250
|
+
summary:
|
|
251
|
+
"Pairing voice. Thinks out loud and shares the driver's seat rather than dictating.",
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: "claude-mods--roast",
|
|
255
|
+
sourceId: "claude-mods",
|
|
256
|
+
path: "output-styles/roast.md",
|
|
257
|
+
displayName: "roast",
|
|
258
|
+
summary:
|
|
259
|
+
"Adversarial review. Brutally honest about your code, then helps you fix it.",
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
// ── lej-output-fixer (3 of 3) ────────────────────────────────────────────
|
|
263
|
+
{
|
|
264
|
+
id: "lej-output-fixer--bottom-line-first",
|
|
265
|
+
sourceId: "lej-output-fixer",
|
|
266
|
+
path: "output-styles/bottom-line-first.md",
|
|
267
|
+
displayName: "Bottom Line First",
|
|
268
|
+
summary:
|
|
269
|
+
"Verdict up top. Details appear only when they would change a decision.",
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: "lej-output-fixer--scannable",
|
|
273
|
+
sourceId: "lej-output-fixer",
|
|
274
|
+
path: "output-styles/scannable.md",
|
|
275
|
+
displayName: "Scannable",
|
|
276
|
+
summary:
|
|
277
|
+
"Short paragraphs, plain reading level, answer first. The antidote to a wall of text.",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: "lej-output-fixer--walk-me-through-it",
|
|
281
|
+
sourceId: "lej-output-fixer",
|
|
282
|
+
path: "output-styles/walk-me-through-it.md",
|
|
283
|
+
displayName: "Walk Me Through It",
|
|
284
|
+
summary:
|
|
285
|
+
"Explains as it goes and assumes no prior context. The inverse of a terse preset.",
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// ── hesreallyhim (6 of 6) ────────────────────────────────────────────────
|
|
289
|
+
{
|
|
290
|
+
id: "hesreallyhim--door-to-door-vim-salesman",
|
|
291
|
+
sourceId: "hesreallyhim",
|
|
292
|
+
path: ".claude/output-styles/door-to-door-vim-salesman.md",
|
|
293
|
+
displayName: "Door-to-Door Vim Salesman",
|
|
294
|
+
summary: "Redirects every topic toward the merits of Vim, relentlessly.",
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: "hesreallyhim--existentialist-poet",
|
|
298
|
+
sourceId: "hesreallyhim",
|
|
299
|
+
path: ".claude/output-styles/existentialist-poet.md",
|
|
300
|
+
// Upstream's own `name` is "Existential Poet" while the file is
|
|
301
|
+
// existentialist-poet.md. Recorded as upstream writes it.
|
|
302
|
+
displayName: "Existential Poet",
|
|
303
|
+
summary:
|
|
304
|
+
"Melancholic engineer-philosopher. Debugging as a metaphor for the human condition.",
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
id: "hesreallyhim--haiku-helper",
|
|
308
|
+
sourceId: "hesreallyhim",
|
|
309
|
+
path: ".claude/output-styles/haiku-helper.md",
|
|
310
|
+
displayName: "Haiku Helper",
|
|
311
|
+
summary:
|
|
312
|
+
"Replies only in 5-7-5 haiku. A hard constraint on length, played straight.",
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
id: "hesreallyhim--tabloid-journalist",
|
|
316
|
+
sourceId: "hesreallyhim",
|
|
317
|
+
path: ".claude/output-styles/tabloid-journalist.md",
|
|
318
|
+
displayName: "Tabloid Journalist",
|
|
319
|
+
summary:
|
|
320
|
+
"Covers your code as a scandal. Sensationalist persona, technically accurate.",
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
id: "hesreallyhim--technical-evangelist",
|
|
324
|
+
sourceId: "hesreallyhim",
|
|
325
|
+
path: ".claude/output-styles/technical-evangelist.md",
|
|
326
|
+
displayName: "Claude Code Technical Evangelist",
|
|
327
|
+
summary: "Enthusiastic Claude Code power user with deep product knowledge.",
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
id: "hesreallyhim--zen-master",
|
|
331
|
+
sourceId: "hesreallyhim",
|
|
332
|
+
path: ".claude/output-styles/zen-master.md",
|
|
333
|
+
displayName: "Zen Master",
|
|
334
|
+
summary: "Guides through koans and metaphor instead of answering directly.",
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
// ── nattergabriel (5 of 13) ──────────────────────────────────────────────
|
|
338
|
+
{
|
|
339
|
+
id: "nattergabriel--challenger",
|
|
340
|
+
sourceId: "nattergabriel",
|
|
341
|
+
path: "output-styles/challenger.md",
|
|
342
|
+
displayName: "Challenger",
|
|
343
|
+
summary:
|
|
344
|
+
"Challenges each decision and stress-tests a design before you commit to it.",
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: "nattergabriel--paranoid",
|
|
348
|
+
sourceId: "nattergabriel",
|
|
349
|
+
path: "output-styles/paranoid.md",
|
|
350
|
+
displayName: "Paranoid",
|
|
351
|
+
summary:
|
|
352
|
+
"Security-obsessed. Every input is hostile and every dependency is suspect.",
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
id: "nattergabriel--socratic",
|
|
356
|
+
sourceId: "nattergabriel",
|
|
357
|
+
path: "output-styles/socratic.md",
|
|
358
|
+
displayName: "Socratic",
|
|
359
|
+
summary: "Guides you to the answer with questions rather than giving it.",
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
id: "nattergabriel--tdd",
|
|
363
|
+
sourceId: "nattergabriel",
|
|
364
|
+
path: "output-styles/tdd.md",
|
|
365
|
+
displayName: "TDD",
|
|
366
|
+
summary:
|
|
367
|
+
"Test-driven. Writes the failing test before the implementation, every time.",
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
id: "nattergabriel--think-aloud",
|
|
371
|
+
sourceId: "nattergabriel",
|
|
372
|
+
path: "output-styles/think-aloud.md",
|
|
373
|
+
displayName: "Think Aloud",
|
|
374
|
+
summary:
|
|
375
|
+
"Reflects your reasoning back so you debug the problem by talking it through.",
|
|
376
|
+
},
|
|
377
|
+
// ── simple-english ───────────────────────────────────────────────────────
|
|
378
|
+
{
|
|
379
|
+
id: "simple-english--simple-english",
|
|
380
|
+
sourceId: "simple-english",
|
|
381
|
+
path: "output-styles/simple-english.md",
|
|
382
|
+
displayName: "Simple English",
|
|
383
|
+
summary:
|
|
384
|
+
"ASD-STE100 Simplified Technical English. One word one meaning, short sentences, no filler.",
|
|
385
|
+
},
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
// ─── Exclusions, recorded so they are not silently reversed ──────────────────
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Repos that ship ZERO output style files. An entry naming one would 404 on
|
|
392
|
+
* first fetch.
|
|
393
|
+
*
|
|
394
|
+
* They are here because two of them are the most-starred names in the field —
|
|
395
|
+
* `JuliusBrussee/caveman` (98,781 stars) and `blader/humanizer` (36,229) — and
|
|
396
|
+
* their absence reads as an oversight to anyone scanning a star column. They
|
|
397
|
+
* are METHODOLOGIES SHIPPED AS SKILLS. There is nothing to import. A test
|
|
398
|
+
* asserts no registry entry names any of them, so this decision cannot be
|
|
399
|
+
* quietly undone six months from now by someone sorting by popularity.
|
|
400
|
+
*/
|
|
401
|
+
export const DENIED_REPOS: readonly string[] = [
|
|
402
|
+
"JuliusBrussee/caveman",
|
|
403
|
+
"blader/humanizer",
|
|
404
|
+
"ayghri/i-have-adhd",
|
|
405
|
+
"hardikpandya/stop-slop",
|
|
406
|
+
"petergyang/no-ai-slop",
|
|
407
|
+
"conorbronsdon/avoid-ai-writing",
|
|
408
|
+
"mattpocock/skills",
|
|
409
|
+
];
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Repos that DO ship importable styles but grant no redistribution licence.
|
|
413
|
+
*
|
|
414
|
+
* Deferred, not denied. `smixs` is otherwise the highest-value repo in the
|
|
415
|
+
* field; it is excluded only because GitHub reports NOASSERTION, and
|
|
416
|
+
* `SimpleClaude` carries no licence file at all. Fetching a copy to the user's
|
|
417
|
+
* own machine is arguably fine, but claudeup would be curating, recommending
|
|
418
|
+
* and automating it, and the cost of being wrong in that direction is a legal
|
|
419
|
+
* problem on other people's machines. The cost of being wrong in this direction
|
|
420
|
+
* is a style a user can still install by hand.
|
|
421
|
+
*
|
|
422
|
+
* Revisit the moment a licence appears upstream. The mechanism for shipping one
|
|
423
|
+
* already exists: `licence: null` renders in warning colour.
|
|
424
|
+
*/
|
|
425
|
+
export const LICENCE_DEFERRED_REPOS: readonly string[] = [
|
|
426
|
+
"smixs/awesome-claude-output-styles",
|
|
427
|
+
"kylesnowschwartz/SimpleClaude",
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
// ─── Lookup ──────────────────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
/** The source a style belongs to, or null when the id names no known source. */
|
|
433
|
+
export function findCommunitySource(
|
|
434
|
+
sourceId: string,
|
|
435
|
+
): CommunityStyleSource | null {
|
|
436
|
+
return COMMUNITY_SOURCES.find((source) => source.id === sourceId) ?? null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** A registry entry by its coordinate id (`attention-span--spartan`). */
|
|
440
|
+
export function findCommunityStyle(id: string): CommunityStyle | null {
|
|
441
|
+
return COMMUNITY_STYLES.find((style) => style.id === id) ?? null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** An entry with its source resolved, or null when either half is unknown. */
|
|
445
|
+
export function resolveCommunityStyle(
|
|
446
|
+
id: string,
|
|
447
|
+
): { style: CommunityStyle; source: CommunityStyleSource } | null {
|
|
448
|
+
const style = findCommunityStyle(id);
|
|
449
|
+
if (!style) return null;
|
|
450
|
+
const source = findCommunitySource(style.sourceId);
|
|
451
|
+
if (!source) return null;
|
|
452
|
+
return { style, source };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Every style a source ships, in display order. */
|
|
456
|
+
export function stylesForSource(sourceId: string): CommunityStyle[] {
|
|
457
|
+
return COMMUNITY_STYLES.filter((style) => style.sourceId === sourceId).sort(
|
|
458
|
+
(a, b) => a.displayName.localeCompare(b.displayName),
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* The raw.githubusercontent.com URL for an entry.
|
|
464
|
+
*
|
|
465
|
+
* Kept here rather than in the fetcher so the one place that knows a
|
|
466
|
+
* coordinate's shape is the one place that knows the registry's shape.
|
|
467
|
+
*/
|
|
468
|
+
export function rawUrlFor(
|
|
469
|
+
style: CommunityStyle,
|
|
470
|
+
source: CommunityStyleSource,
|
|
471
|
+
): string {
|
|
472
|
+
return `https://raw.githubusercontent.com/${source.repo}/${source.ref}/${style.path}`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Why a coordinate is unusable, or null when it is fine.
|
|
477
|
+
*
|
|
478
|
+
* These are OUR data, so a failure is a bug in a commit rather than a user
|
|
479
|
+
* error — callers throw on it and the registry test catches it before it ships.
|
|
480
|
+
* Validating anyway, because a `..` that reached the URL builder would be both
|
|
481
|
+
* an SSRF and a path traversal in the destination filename.
|
|
482
|
+
*/
|
|
483
|
+
export function coordinateProblem(
|
|
484
|
+
style: CommunityStyle,
|
|
485
|
+
source: CommunityStyleSource,
|
|
486
|
+
): string | null {
|
|
487
|
+
if (!isValidGitHubRepo(source.repo)) {
|
|
488
|
+
return `not a valid GitHub repo: "${source.repo}"`;
|
|
489
|
+
}
|
|
490
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(source.ref)) {
|
|
491
|
+
return `not a valid git ref: "${source.ref}"`;
|
|
492
|
+
}
|
|
493
|
+
if (!/^[A-Za-z0-9._/-]+\.md$/.test(style.path)) {
|
|
494
|
+
return `not a valid style path: "${style.path}"`;
|
|
495
|
+
}
|
|
496
|
+
if (style.path.includes("..") || style.path.startsWith("/")) {
|
|
497
|
+
return `style path escapes the repo: "${style.path}"`;
|
|
498
|
+
}
|
|
499
|
+
if (!style.path.startsWith(`${source.dir}/`)) {
|
|
500
|
+
return `"${style.path}" is not under "${source.dir}/"`;
|
|
501
|
+
}
|
|
502
|
+
// The id is also the cache filename, so anything that is not a plain slug
|
|
503
|
+
// pair would escape the cache directory or collide with another entry.
|
|
504
|
+
if (
|
|
505
|
+
!new RegExp(`^[a-z0-9-]+${COMMUNITY_ID_SEPARATOR}[a-z0-9-]+$`).test(
|
|
506
|
+
style.id,
|
|
507
|
+
)
|
|
508
|
+
) {
|
|
509
|
+
return `id "${style.id}" is not a <source>${COMMUNITY_ID_SEPARATOR}<style> slug pair`;
|
|
510
|
+
}
|
|
511
|
+
if (style.id !== `${source.id}${COMMUNITY_ID_SEPARATOR}${slugOf(style.id)}`) {
|
|
512
|
+
return `id "${style.id}" does not start with "${source.id}${COMMUNITY_ID_SEPARATOR}"`;
|
|
513
|
+
}
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** The style half of a coordinate id. `attention-span--spartan` -> `spartan`. */
|
|
518
|
+
export function slugOf(id: string): string {
|
|
519
|
+
const at = id.indexOf(COMMUNITY_ID_SEPARATOR);
|
|
520
|
+
return at === -1 ? id : id.slice(at + COMMUNITY_ID_SEPARATOR.length);
|
|
521
|
+
}
|
package/src/main.tsx
CHANGED
|
@@ -33,6 +33,21 @@ async function main(): Promise<void> {
|
|
|
33
33
|
// element now names an adaptive colour explicitly — see src/ui/theme.ts.
|
|
34
34
|
const renderer = await createCliRenderer({
|
|
35
35
|
backgroundColor: RGBA.defaultBackground(),
|
|
36
|
+
// claudeup is keyboard-only — no component sets onMouseDown/Up/Move, and
|
|
37
|
+
// every action has a key. OpenTUI nevertheless defaults BOTH of these to
|
|
38
|
+
// true (`config.useMouse ?? true`, `config.enableMouseMovement ?? true`),
|
|
39
|
+
// which turns on button reporting and mode-1003 "report all motion".
|
|
40
|
+
//
|
|
41
|
+
// Inside tmux that is actively harmful: once a pane's application asks for
|
|
42
|
+
// mouse events, tmux hands them to that application instead of using them
|
|
43
|
+
// to select a pane or scroll history. Clicking another pane then does
|
|
44
|
+
// nothing, keyboard focus stays where it was, and what you type lands in
|
|
45
|
+
// the pane you thought you had just left.
|
|
46
|
+
//
|
|
47
|
+
// Turning it off costs nothing here and gives clicking and scrolling back
|
|
48
|
+
// to the terminal.
|
|
49
|
+
useMouse: false,
|
|
50
|
+
enableMouseMovement: false,
|
|
36
51
|
});
|
|
37
52
|
|
|
38
53
|
// Ask the terminal whether it is light or dark, once. Only the disabled-row
|
|
@@ -53,13 +53,48 @@ interface StoredCooldown {
|
|
|
53
53
|
strikes: number;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* What the last upstream check learned about one community-styles repository.
|
|
58
|
+
*
|
|
59
|
+
* Lives here rather than in a fourth cache file so it inherits this store's file
|
|
60
|
+
* lock, atomic write and CLAUDE_CONFIG_DIR test isolation — and, more to the
|
|
61
|
+
* point, so it sits beside the rate-limit cooldowns it is budgeted against.
|
|
62
|
+
*
|
|
63
|
+
* `dirHeadSha` is the pivot of the two-phase check: one API call per REPO
|
|
64
|
+
* answers "did anything in its styles directory move", and only a repo that
|
|
65
|
+
* moved is drilled into with free raw fetches. Per-style API calls would spend
|
|
66
|
+
* the machine's whole hourly budget on first use.
|
|
67
|
+
*/
|
|
68
|
+
export interface StoredCommunityCheck {
|
|
69
|
+
/** Wall-clock ms of the last successful phase A. Drives the 24h TTL. */
|
|
70
|
+
checkedAt: number;
|
|
71
|
+
/** Latest commit touching the repo's styles directory, as of that check. */
|
|
72
|
+
dirHeadSha: string;
|
|
73
|
+
/** Per coordinate id: the bytes we hold, and any update sitting in .pending/. */
|
|
74
|
+
styles: Record<
|
|
75
|
+
string,
|
|
76
|
+
{
|
|
77
|
+
sha256: string;
|
|
78
|
+
pendingSha256?: string;
|
|
79
|
+
/** Changed-line count of that pending update, so it survives a restart. */
|
|
80
|
+
pendingLines?: number;
|
|
81
|
+
}
|
|
82
|
+
>;
|
|
83
|
+
}
|
|
84
|
+
|
|
56
85
|
interface StoreShape {
|
|
57
86
|
version: 1;
|
|
58
87
|
catalogs: Record<string, StoredCatalog>;
|
|
59
88
|
cooldowns: Record<string, StoredCooldown>;
|
|
89
|
+
communityStyles: Record<string, StoredCommunityCheck>;
|
|
60
90
|
}
|
|
61
91
|
|
|
62
|
-
const EMPTY: StoreShape = {
|
|
92
|
+
const EMPTY: StoreShape = {
|
|
93
|
+
version: 1,
|
|
94
|
+
catalogs: {},
|
|
95
|
+
cooldowns: {},
|
|
96
|
+
communityStyles: {},
|
|
97
|
+
};
|
|
63
98
|
|
|
64
99
|
/**
|
|
65
100
|
* Resolved per call, honouring CLAUDE_CONFIG_DIR — same reasoning as
|
|
@@ -83,7 +118,8 @@ let memo: { path: string; data: StoreShape } | null = null;
|
|
|
83
118
|
|
|
84
119
|
async function load(): Promise<StoreShape> {
|
|
85
120
|
const file = storePath();
|
|
86
|
-
if (!file)
|
|
121
|
+
if (!file)
|
|
122
|
+
return { version: 1, catalogs: {}, cooldowns: {}, communityStyles: {} };
|
|
87
123
|
if (memo?.path === file) return memo.data;
|
|
88
124
|
try {
|
|
89
125
|
const parsed = JSON.parse(await fs.readFile(file, "utf-8")) as StoreShape;
|
|
@@ -92,11 +128,19 @@ async function load(): Promise<StoreShape> {
|
|
|
92
128
|
const data = parsed?.version === 1 ? parsed : { ...EMPTY };
|
|
93
129
|
data.catalogs ??= {};
|
|
94
130
|
data.cooldowns ??= {};
|
|
131
|
+
// Absent in every file written before community styles existed, so this
|
|
132
|
+
// default is the migration — an upgrade must not read as a corrupt cache.
|
|
133
|
+
data.communityStyles ??= {};
|
|
95
134
|
memo = { path: file, data };
|
|
96
135
|
return data;
|
|
97
136
|
} catch {
|
|
98
137
|
// Absent or corrupt. A cache must never be a failure mode — start empty.
|
|
99
|
-
const data: StoreShape = {
|
|
138
|
+
const data: StoreShape = {
|
|
139
|
+
...EMPTY,
|
|
140
|
+
catalogs: {},
|
|
141
|
+
cooldowns: {},
|
|
142
|
+
communityStyles: {},
|
|
143
|
+
};
|
|
100
144
|
memo = { path: file, data };
|
|
101
145
|
return data;
|
|
102
146
|
}
|
|
@@ -117,13 +161,27 @@ async function mutate(fn: (data: StoreShape) => void): Promise<void> {
|
|
|
117
161
|
await withFileLock(file, async () => {
|
|
118
162
|
let onDisk: StoreShape;
|
|
119
163
|
try {
|
|
120
|
-
const parsed = JSON.parse(
|
|
164
|
+
const parsed = JSON.parse(
|
|
165
|
+
await fs.readFile(file, "utf-8"),
|
|
166
|
+
) as StoreShape;
|
|
167
|
+
// Every section is named explicitly: a key omitted here is silently
|
|
168
|
+
// dropped on the next write of any OTHER section.
|
|
121
169
|
onDisk =
|
|
122
170
|
parsed?.version === 1
|
|
123
|
-
? {
|
|
124
|
-
|
|
171
|
+
? {
|
|
172
|
+
version: 1,
|
|
173
|
+
catalogs: parsed.catalogs ?? {},
|
|
174
|
+
cooldowns: parsed.cooldowns ?? {},
|
|
175
|
+
communityStyles: parsed.communityStyles ?? {},
|
|
176
|
+
}
|
|
177
|
+
: { version: 1, catalogs: {}, cooldowns: {}, communityStyles: {} };
|
|
125
178
|
} catch {
|
|
126
|
-
onDisk = {
|
|
179
|
+
onDisk = {
|
|
180
|
+
version: 1,
|
|
181
|
+
catalogs: {},
|
|
182
|
+
cooldowns: {},
|
|
183
|
+
communityStyles: {},
|
|
184
|
+
};
|
|
127
185
|
}
|
|
128
186
|
fn(onDisk);
|
|
129
187
|
// Temp + rename, so a crash mid-write cannot leave a half-written file
|
|
@@ -212,6 +270,42 @@ export async function clearStoredCooldown(host: string): Promise<void> {
|
|
|
212
270
|
});
|
|
213
271
|
}
|
|
214
272
|
|
|
273
|
+
// ─── Community style checks ──────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Every recorded upstream check, keyed by community source id.
|
|
277
|
+
*
|
|
278
|
+
* Deliberately NOT expired on read, unlike cooldowns. An expired cooldown means
|
|
279
|
+
* "you may call again"; an expired CHECK still carries the shas we compare
|
|
280
|
+
* against, and only its `checkedAt` has gone stale. Dropping it would throw away
|
|
281
|
+
* the one thing that makes the next check cheap, and would make a 25-hour-old
|
|
282
|
+
* result indistinguishable from never having looked. Freshness is judged by the
|
|
283
|
+
* reader, which is what lets an expired check present as `unknown` rather than
|
|
284
|
+
* as up to date.
|
|
285
|
+
*/
|
|
286
|
+
export async function readCommunityChecks(): Promise<
|
|
287
|
+
Record<string, StoredCommunityCheck>
|
|
288
|
+
> {
|
|
289
|
+
return { ...(await load()).communityStyles };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export async function writeCommunityCheck(
|
|
293
|
+
sourceId: string,
|
|
294
|
+
check: StoredCommunityCheck,
|
|
295
|
+
): Promise<void> {
|
|
296
|
+
await mutate((data) => {
|
|
297
|
+
data.communityStyles ??= {};
|
|
298
|
+
data.communityStyles[sourceId] = check;
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Drop every recorded check. Backs an explicit refresh. */
|
|
303
|
+
export async function clearCommunityChecks(): Promise<void> {
|
|
304
|
+
await mutate((data) => {
|
|
305
|
+
data.communityStyles = {};
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
215
309
|
/** Test seam: forget this process's memo so the next read hits disk. */
|
|
216
310
|
export function resetCatalogCacheMemo(): void {
|
|
217
311
|
memo = null;
|