@g3nomaly/g3n-mods 0.1.6 → 0.1.8
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/.claude-plugin/marketplace.json +8 -2
- package/README.md +13 -6
- package/mods/agent-adventure/.claude-plugin/plugin.json +23 -0
- package/mods/agent-adventure/hooks/content.ts +263 -0
- package/mods/agent-adventure/hooks/engine.ts +1125 -0
- package/mods/agent-adventure/hooks/hooks.json +1 -0
- package/mods/agent-adventure/hooks/register.tsx +465 -0
- package/mods/agent-adventure/hooks/sprite.ts +424 -0
- package/mods/agent-adventure/types/index.d.ts +248 -0
- package/mods/agent-adventure/view/panes.json +8 -0
- package/mods/agent-adventure/view/view.css +230 -0
- package/mods/agent-adventure/view/view.ts +308 -0
- package/mods/agents-panel/.claude-plugin/plugin.json +9 -1
- package/mods/agents-panel/hooks/register.tsx +18 -4
- package/mods/agents-panel/view/panes.json +8 -0
- package/mods/context-bar/.claude-plugin/plugin.json +9 -1
- package/mods/context-bar/hooks/register.tsx +4 -3
- package/mods/context-bar/view/panes.json +8 -0
- package/mods/custom-crab-agents/hooks/register.tsx +15 -1
- package/mods/delete-guard/.claude-plugin/plugin.json +9 -1
- package/mods/delete-guard/hooks/register.tsx +15 -5
- package/mods/delete-guard/view/panes.json +8 -0
- package/mods/focus-mode/.claude-plugin/plugin.json +6 -0
- package/mods/focus-mode/hooks/register.tsx +7 -4
- package/mods/focus-mode/view/panes.json +8 -0
- package/mods/mini-game/.claude-plugin/plugin.json +10 -2
- package/mods/mini-game/hooks/bubbles.ts +27 -0
- package/mods/mini-game/hooks/minesweeper.ts +108 -0
- package/mods/mini-game/hooks/mole.ts +91 -0
- package/mods/mini-game/hooks/register.tsx +206 -148
- package/mods/mini-game/types/index.d.ts +18 -1
- package/mods/mini-game/view/panes.json +8 -0
- package/mods/tool-timeline/.claude-plugin/plugin.json +9 -1
- package/mods/tool-timeline/hooks/register.tsx +18 -4
- package/mods/tool-timeline/view/panes.json +8 -0
- package/mods/you-should-know/.claude-plugin/plugin.json +3 -3
- package/mods/you-should-know/hooks/register.tsx +173 -45
- package/mods/you-should-know/types/index.d.ts +13 -4
- package/package.json +2 -1
|
@@ -2,84 +2,204 @@
|
|
|
2
2
|
import { atom, read, update } from 'claude-code'
|
|
3
3
|
import type { EngineInterface, Register } from 'claude-code'
|
|
4
4
|
|
|
5
|
-
import type { Card } from '../types'
|
|
5
|
+
import type { Card, Point, PointKind } from '../types'
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const notes = atom({ plugin: 'you-should-know', key: 'notes' } as const, [])
|
|
8
8
|
|
|
9
|
-
const DEFAULT_MIN_CHARS =
|
|
9
|
+
const DEFAULT_MIN_CHARS = 800
|
|
10
10
|
const DEFAULT_MODEL = 'haiku'
|
|
11
11
|
const TAIL = 200
|
|
12
12
|
const KEEP = 20
|
|
13
13
|
/** Most of a reply sent to the model; the end of a reply carries its conclusion. */
|
|
14
14
|
const MAX_PROMPT_CHARS = 40_000
|
|
15
|
+
/** Most questions a card lists, and the longest one kept whole. */
|
|
16
|
+
const MAX_QUESTIONS = 3
|
|
17
|
+
const MAX_QUESTION_CHARS = 220
|
|
18
|
+
/** Most changes, conclusions and facts the model adds to a card. */
|
|
19
|
+
const MAX_MODEL_POINTS = 3
|
|
20
|
+
/** Most of the user's request sent beside the reply. */
|
|
21
|
+
const MAX_REQUEST_CHARS = 4000
|
|
22
|
+
/** Kinds the model may tag a point with, in the order a card lists them; questions come from the reply itself. */
|
|
23
|
+
const MODEL_KINDS: readonly PointKind[] = ['change', 'conclusion', 'fact']
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Each tag's look: its name, its color, the dark tint of its round icon, and the icon's mark, drawn in a 40 by 40 box
|
|
27
|
+
* in `currentColor`, which `iconSvg` fills with the tag's color.
|
|
28
|
+
* `badge` is the mark's character for the terminal, which draws no SVG.
|
|
29
|
+
*/
|
|
30
|
+
const TAGS: Readonly<Record<PointKind, { badge: string; name: string; color: string; tint: string; mark: string }>> = {
|
|
31
|
+
question: {
|
|
32
|
+
badge: '?', name: 'Question', color: '#e5b567', tint: '#3d3424',
|
|
33
|
+
mark: '<path d="M15.5 15.5a4.5 4.5 0 1 1 6.3 4.1c-1.2.6-1.8 1.5-1.8 2.8v1" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"/><circle cx="20" cy="28.5" r="1.6" fill="currentColor"/>',
|
|
34
|
+
},
|
|
35
|
+
change: {
|
|
36
|
+
badge: '±', name: 'Change', color: '#b48ead', tint: '#36293a',
|
|
37
|
+
mark: '<path d="M13 27l1.6-5.2 9.2-9.2 3.6 3.6-9.2 9.2z" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linejoin="round"/>',
|
|
38
|
+
},
|
|
39
|
+
conclusion: {
|
|
40
|
+
badge: '✓', name: 'Conclusion', color: '#7cc47f', tint: '#26392a',
|
|
41
|
+
mark: '<path d="M13.5 20.5l4.5 4.5 8.5-9.5" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>',
|
|
42
|
+
},
|
|
43
|
+
fact: {
|
|
44
|
+
badge: 'i', name: 'Fact', color: '#5b8def', tint: '#232e45',
|
|
45
|
+
mark: '<circle cx="20" cy="13.5" r="1.8" fill="currentColor"/><path d="M20 18.5v9.5" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"/>',
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Text color on a badge, the card's own dark. */
|
|
50
|
+
const CARD = '#1b1f27'
|
|
51
|
+
|
|
52
|
+
/** Width of the longest tag name, so the points' text starts level. */
|
|
53
|
+
const NAME_CELLS = Math.max(...Object.values(TAGS).map(tag => tag.name.length))
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Spacing of the card per surface. The terminal counts whole cells; the Claude Code extension turns a number into CSS
|
|
57
|
+
* (`ch` across, `lh` down) and takes fractions, so there the card gets room inside its border and between its lines.
|
|
58
|
+
*/
|
|
59
|
+
const SPACING = {
|
|
60
|
+
terminal: { paddingX: 1, paddingY: 0, rowGap: 0 },
|
|
61
|
+
other: { paddingX: 1.5, paddingY: 0.6, rowGap: 0.35 },
|
|
62
|
+
} as const
|
|
63
|
+
|
|
64
|
+
/** Size of a tag's round icon, in pixels. */
|
|
65
|
+
const ICON_PX = 18
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Draws a tag's round icon: its tint filling the circle and its mark in the tag's color.
|
|
69
|
+
*
|
|
70
|
+
* @param kind - The tag, see `TAGS`.
|
|
71
|
+
* @returns The SVG markup as a string.
|
|
72
|
+
*/
|
|
73
|
+
function iconSvg(kind: PointKind): string {
|
|
74
|
+
const tag = TAGS[kind]
|
|
75
|
+
const mark = tag.mark.replaceAll('currentColor', tag.color)
|
|
76
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${ICON_PX}" height="${ICON_PX}" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="${tag.tint}"/>${mark}</svg>`
|
|
77
|
+
}
|
|
78
|
+
|
|
15
79
|
|
|
16
80
|
const SYSTEM = [
|
|
17
|
-
'You read
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
81
|
+
'You read the request a user gave an AI coding assistant and the reply it wrote, and pick at most three points that',
|
|
82
|
+
'tell the user what happened about their request.',
|
|
83
|
+
'Tag each one: "change" for something the reply did, such as a file, feature, element or setting it added, removed',
|
|
84
|
+
'or changed; "conclusion" for its direct answer to what the user asked; "fact" only for a cost, a breaking change',
|
|
85
|
+
'or a decision the user must make. Prefer changes when the reply did work, and list the biggest first.',
|
|
86
|
+
'Never pick caveats, limitations, what was not checked or tested, how to try it out, or anything the user did not ask about.',
|
|
87
|
+
'A line saying something was not verified, could not be checked or may need fixing later is a caveat: leave it out.',
|
|
88
|
+
'Fewer points beat a weak one; an empty list is fine.',
|
|
89
|
+
'Leave out the questions the reply asks; they are shown elsewhere.',
|
|
90
|
+
'Write a change as what was done, starting with the verb: "Removed the bulb icon", "Added a round icon per tag".',
|
|
91
|
+
'Answer with JSON only, no prose and no code fence: {"points": [{"kind": "change", "text": "..."}]}.',
|
|
92
|
+
'Each text is one plain line of at most 100 characters, in the reply\'s own facts and numbers.',
|
|
93
|
+
'If nothing in the reply answers the request, answer {"points": []}.',
|
|
22
94
|
].join(' ')
|
|
23
95
|
|
|
24
|
-
const BULB_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="#3d3424"/><path d="M20 11a6 6 0 0 0-3.6 10.8c.8.6 1.1 1.3 1.1 2.2v1h5v-1c0-.9.3-1.6 1.1-2.2A6 6 0 0 0 20 11z" fill="none" stroke="#e5b567" stroke-width="1.6"/><path d="M17.5 28h5M18.3 30.5h3.4" stroke="#e5b567" stroke-width="1.6" stroke-linecap="round"/></svg>'
|
|
25
|
-
|
|
26
96
|
/** Collapses every run of whitespace to one space and trims both ends. */
|
|
27
97
|
function normalize(text: string): string {
|
|
28
98
|
return text.replace(/\s+/g, ' ').trim()
|
|
29
99
|
}
|
|
30
100
|
|
|
31
101
|
/**
|
|
32
|
-
* @brief
|
|
102
|
+
* @brief Finds the questions a reply asks, in its own words: the sentences of its prose that end in a question mark.
|
|
103
|
+
*
|
|
104
|
+
* Code blocks and inline code holding a `?` are left out, and so is a `?` inside a word or URL, such as `?page=2`.
|
|
105
|
+
* List markers, heading marks and bold or italic marks are taken off.
|
|
106
|
+
*
|
|
107
|
+
* @param text - Full text of the reply.
|
|
108
|
+
* @returns Up to `MAX_QUESTIONS` questions, in order, each cut to `MAX_QUESTION_CHARS`.
|
|
109
|
+
*/
|
|
110
|
+
export function extractQuestions(text: string): string[] {
|
|
111
|
+
const prose = text
|
|
112
|
+
.replace(/```[\s\S]*?```/g, '\n')
|
|
113
|
+
.replace(/`([^`\n]*)`/g, (_, code: string) => (code.includes('?') ? ' ' : code))
|
|
114
|
+
const questions: string[] = []
|
|
115
|
+
for (const rawLine of prose.split('\n')) {
|
|
116
|
+
const line = rawLine.replace(/^\s*(?:[-*+]|\d+\.|#+|>)\s+/, '').replace(/\*\*|__/g, '')
|
|
117
|
+
for (const match of line.matchAll(/[^.!?]*\?(?=[\s)"'*_]|$)/g)) {
|
|
118
|
+
const question = normalize(match[0])
|
|
119
|
+
if (question.length >= 8 && !questions.includes(question)) questions.push(question.slice(0, MAX_QUESTION_CHARS))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return questions.slice(0, MAX_QUESTIONS)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @brief Reads the model's answer into tagged points.
|
|
33
127
|
*
|
|
34
128
|
* @param text - Raw answer from the model.
|
|
35
|
-
* @returns The
|
|
129
|
+
* @returns The changes, conclusions and facts, at most `MAX_MODEL_POINTS`; empty for a skip or anything malformed.
|
|
36
130
|
*/
|
|
37
|
-
export function
|
|
131
|
+
export function parseModelPoints(text: string): Point[] {
|
|
38
132
|
const json = /\{[\s\S]*\}/.exec(text)?.[0]
|
|
39
|
-
if (json === undefined) return
|
|
133
|
+
if (json === undefined) return []
|
|
40
134
|
let value: unknown
|
|
41
135
|
try {
|
|
42
136
|
value = JSON.parse(json)
|
|
43
137
|
} catch {
|
|
44
|
-
return
|
|
138
|
+
return []
|
|
139
|
+
}
|
|
140
|
+
if (typeof value !== 'object' || value === null || !('points' in value) || !Array.isArray(value.points)) return []
|
|
141
|
+
const points: Point[] = []
|
|
142
|
+
for (const item of value.points as unknown[]) {
|
|
143
|
+
if (typeof item !== 'object' || item === null || !('kind' in item) || !('text' in item)) continue
|
|
144
|
+
const { kind, text: line } = item
|
|
145
|
+
const tagged = MODEL_KINDS.find(one => one === kind)
|
|
146
|
+
if (tagged === undefined || typeof line !== 'string' || line.trim() === '') continue
|
|
147
|
+
points.push({ kind: tagged, text: line.trim().slice(0, 140) })
|
|
45
148
|
}
|
|
46
|
-
|
|
47
|
-
const { headline, detail } = value
|
|
48
|
-
if (typeof headline !== 'string' || typeof detail !== 'string' || headline.trim().length === 0) return undefined
|
|
49
|
-
return { headline: headline.trim().slice(0, 90), detail: detail.trim().slice(0, 160) }
|
|
149
|
+
return points.slice(0, MAX_MODEL_POINTS)
|
|
50
150
|
}
|
|
51
151
|
|
|
52
152
|
/**
|
|
53
|
-
* @brief
|
|
153
|
+
* @brief Builds a reply's card: its questions, then the model's changes, conclusions and facts when the reply is long
|
|
154
|
+
* enough.
|
|
54
155
|
*
|
|
55
|
-
* @param $ - Engine handle the model call and the
|
|
156
|
+
* @param $ - Engine handle the model call and the notes state go through.
|
|
157
|
+
* @param request - The user's text the reply answers; empty for a turn started without one.
|
|
56
158
|
* @param answer - Full text of the assistant's reply.
|
|
57
|
-
* @param model - Model that reads the reply.
|
|
159
|
+
* @param model - Model that reads the reply, or `undefined` to skip the call for a short reply.
|
|
58
160
|
*/
|
|
59
|
-
async function
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
161
|
+
async function buildCard($: EngineInterface, request: string, answer: string, model: string | undefined): Promise<void> {
|
|
162
|
+
const questions: Point[] = extractQuestions(answer).map(text => ({ kind: 'question', text }))
|
|
163
|
+
let found: Point[] = []
|
|
164
|
+
if (model !== undefined) {
|
|
165
|
+
const asked = request.trim() === '' ? '(no typed request; the reply continues earlier work)' : request.slice(0, MAX_REQUEST_CHARS)
|
|
166
|
+
const prompt = `The user asked:\n${asked}\n\nThe reply:\n${answer.slice(-MAX_PROMPT_CHARS)}`
|
|
167
|
+
const result = await $.model.complete({ model, system: SYSTEM, prompt, maxTokens: 400 })
|
|
168
|
+
if (result.isAnswered) found = parseModelPoints(result.text)
|
|
169
|
+
}
|
|
170
|
+
const points = [...questions, ...MODEL_KINDS.flatMap(kind => found.filter(point => point.kind === kind))]
|
|
171
|
+
if (points.length === 0) return
|
|
64
172
|
const tail = normalize(answer).slice(-TAIL)
|
|
65
|
-
await update($,
|
|
173
|
+
await update($, notes, list => [...list.filter(one => one.tail !== tail), { tail, points }].slice(-KEEP))
|
|
66
174
|
}
|
|
67
175
|
|
|
68
176
|
/**
|
|
69
177
|
* @brief Subscribes the card handlers to the engine's events and renders the card.
|
|
70
178
|
*
|
|
71
179
|
* @param on - Subscribes a handler to one engine event.
|
|
72
|
-
* @param options - Plugin options: `minChars` sets the shortest reply
|
|
180
|
+
* @param options - Plugin options: `minChars` sets the shortest reply the model reads for conclusions and facts,
|
|
181
|
+
* `model` the model that reads it. A reply that asks a question gets a card at any length.
|
|
73
182
|
*/
|
|
74
183
|
export const register: Register = (on, options) => {
|
|
75
184
|
const minChars = Number(options.minChars ?? DEFAULT_MIN_CHARS)
|
|
76
185
|
const model = typeof options.model === 'string' && options.model.length > 0 ? options.model : DEFAULT_MODEL
|
|
77
186
|
|
|
187
|
+
/** The user's text of each running turn, by turn id, so the model reads a reply beside the request it answers. */
|
|
188
|
+
const requests = new Map<string, string>()
|
|
189
|
+
|
|
190
|
+
on('turn.start', async ($, e, next) => {
|
|
191
|
+
requests.set(e.turnId, e.text)
|
|
192
|
+
return next(e)
|
|
193
|
+
})
|
|
194
|
+
|
|
78
195
|
on('turn.complete', async ($, e, next) => {
|
|
79
196
|
const result = await next(e)
|
|
80
|
-
|
|
197
|
+
const request = requests.get(e.turnId) ?? ''
|
|
198
|
+
requests.delete(e.turnId)
|
|
199
|
+
if (e.agentId === undefined && e.reason === 'answer') {
|
|
81
200
|
const answer = e.answer
|
|
82
|
-
|
|
201
|
+
const isLong = answer.length >= minChars
|
|
202
|
+
if (isLong || extractQuestions(answer).length > 0) $.clock.after(0, () => void buildCard($, request, answer, isLong ? model : undefined))
|
|
83
203
|
}
|
|
84
204
|
return result
|
|
85
205
|
}).catch(($, e, next) => next(e))
|
|
@@ -88,27 +208,35 @@ export const register: Register = (on, options) => {
|
|
|
88
208
|
if (e.props.isSummary === true) return next(e)
|
|
89
209
|
const text = normalize(e.props.text)
|
|
90
210
|
if (text.length < 20) return next(e)
|
|
91
|
-
const card = (await read($,
|
|
211
|
+
const card: Card | undefined = (await read($, notes)).find(one => one.tail.endsWith(text.slice(-TAIL)))
|
|
92
212
|
if (card === undefined) return next(e)
|
|
93
213
|
|
|
94
214
|
const { Box, Text } = $.ui.resolve(e)
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
215
|
+
const space = e.surface === 'terminal' ? SPACING.terminal : SPACING.other
|
|
216
|
+
/** A tag's round icon; the terminal, which draws no SVG, gets its mark's character on its color. */
|
|
217
|
+
const icon = (kind: PointKind) => {
|
|
218
|
+
if (e.surface === 'terminal') return <Text bold color={CARD} backgroundColor={TAGS[kind].color}>{` ${TAGS[kind].badge} `}</Text>
|
|
219
|
+
const { Svg } = $.ui.resolve(e)
|
|
220
|
+
return <Svg source={iconSvg(kind)} alt={TAGS[kind].name} width={ICON_PX} height={ICON_PX} />
|
|
221
|
+
}
|
|
101
222
|
|
|
223
|
+
// One line per point, each led by its tag's icon and name; only the tags the reply has show. Rows say `nowrap`: the
|
|
224
|
+
// Claude Code extension wraps a row by default, which would drop a long point under its icon. The point's text
|
|
225
|
+
// wraps inside its own column instead.
|
|
102
226
|
return (
|
|
103
227
|
<Box flexDirection="column" gap={1}>
|
|
104
228
|
{await next(e)}
|
|
105
|
-
<Box borderStyle="round" borderColor="#3a3f4b" paddingX={
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
229
|
+
<Box borderStyle="round" borderColor="#3a3f4b" paddingX={space.paddingX} paddingY={space.paddingY} flexDirection="column" rowGap={space.rowGap}>
|
|
230
|
+
{card.points.map(point => {
|
|
231
|
+
const tag = TAGS[point.kind]
|
|
232
|
+
return (
|
|
233
|
+
<Box flexDirection="row" flexWrap="nowrap" columnGap={1} alignItems="flex-start">
|
|
234
|
+
<Box flexShrink={0}>{icon(point.kind)}</Box>
|
|
235
|
+
<Box width={NAME_CELLS} flexShrink={0}><Text color={tag.color}>{tag.name}</Text></Box>
|
|
236
|
+
<Box flexGrow={1} flexShrink={1} minWidth={0}><Text>{point.text}</Text></Box>
|
|
237
|
+
</Box>
|
|
238
|
+
)
|
|
239
|
+
})}
|
|
112
240
|
</Box>
|
|
113
241
|
</Box>
|
|
114
242
|
)
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
// packages/g3n-mods/mods/you-should-know/types/index.d.ts
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/** What a point is: a question the reply asks the user, a change it made, its answer to the request, or a fact they must act on. */
|
|
4
|
+
export type PointKind = 'question' | 'change' | 'conclusion' | 'fact'
|
|
5
|
+
|
|
6
|
+
/** One tagged line of a card. */
|
|
7
|
+
export type Point = {
|
|
8
|
+
kind: PointKind
|
|
9
|
+
/** For a question, the reply's own words; else one plain line */
|
|
10
|
+
text: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The points of one reply, keyed by the reply's last characters so its message can find them. */
|
|
4
14
|
export type Card = {
|
|
5
15
|
/** The reply's last 200 characters, whitespace collapsed */
|
|
6
16
|
tail: string
|
|
7
|
-
|
|
8
|
-
detail: string
|
|
17
|
+
points: Point[]
|
|
9
18
|
}
|
|
10
19
|
|
|
11
20
|
declare module 'claude-code' {
|
|
12
21
|
interface PluginState {
|
|
13
|
-
'you-should-know': {
|
|
22
|
+
'you-should-know': { notes: Card[] }
|
|
14
23
|
}
|
|
15
24
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@g3nomaly/g3n-mods",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "The g3n Claude Code mods: panes, status lines, commands and hooks, installed with @g3nomaly/mods.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"build": "bun run build:marketplace",
|
|
12
12
|
"typecheck": "tsc --noEmit && tsc --noEmit --project mods/tsconfig.json && tsc --noEmit --project mods/tsconfig.view.json",
|
|
13
13
|
"validate": "bun scripts/validate.ts",
|
|
14
|
+
"test": "bun test tests",
|
|
14
15
|
"build:marketplace": "bun scripts/build-marketplace.ts",
|
|
15
16
|
"types": "bun scripts/sync-types.ts"
|
|
16
17
|
},
|