claude-math 0.1.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.
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "vladimirrott",
3
+ "owner": { "name": "Vladimir Rotariu" },
4
+ "metadata": { "description": "Vladimir's Claude Code plugins" },
5
+ "plugins": [
6
+ {
7
+ "name": "claude-math",
8
+ "source": "./",
9
+ "description": "Renders math as Unicode glyphs in the terminal instead of unrenderable LaTeX",
10
+ "version": "0.1.0"
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "claude-math",
3
+ "version": "0.1.0",
4
+ "description": "Render math legibly in Claude Code. Emits Unicode glyphs instead of unrenderable LaTeX in the terminal (works in every terminal). Graphical sixel/kitty rendering is on the roadmap.",
5
+ "author": { "name": "Vladimir Rotariu", "email": "vladimirrott@users.noreply.github.com" },
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/vladimirrott/claude-math",
8
+ "keywords": ["math", "latex", "katex", "unicode", "rendering", "skill"]
9
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ All notable changes to `claude-math` documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [SemVer](https://semver.org/).
4
+
5
+ ## [0.1.0] — 2026-05-20
6
+
7
+ ### Added
8
+ - `math-unicode` skill — auto-triggers on math; instructs Claude to emit math as Unicode glyphs (Greek, operators, relations, logic, sub/super) instead of raw LaTeX dollar-sign syntax.
9
+ - LaTeX → Unicode cheatsheet, style rules, common-forms reference, anti-patterns.
10
+ - `claude-math` CLI (`bin/claude-math.js`) — `install` / `uninstall` / `status` / `prepack` commands. Symlinks plugin into `~/.claude/plugins/local/` and patches `installed_plugins.json` + `settings.json`.
11
+ - **Install safety**: atomic JSON writes (`.tmp` + rename); `.claude-math.bak` backup of `settings.json` and `installed_plugins.json` on first touch; refuses to overwrite a foreign symlink or non-plugin directory without `--force`.
12
+ - **Install modes**: auto-copy when invoked from an npx cache path (ephemeral location); junction on Windows; symlink elsewhere. `--copy` flag forces copy.
13
+ - **Tests**: `node --test` suite covering install/uninstall round-trip, idempotency, atomicity, backups, copy mode, foreign-symlink refusal, and prepack version sync.
14
+ - **CI**: `.github/workflows/ci.yml` (matrix: ubuntu/macos × node 18/20/22) and `.github/workflows/publish.yml` (tag-triggered `npm publish --provenance`).
15
+ - `prepack` script auto-syncs `.claude-plugin/plugin.json` version from `package.json` so the two never drift.
16
+ - Roadmap notes for opt-in graphical rendering (sixel / kitty graphics via a `claude-math-render` MCP server).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vladimir Rotariu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # claude-math
2
+
3
+ Make math in Claude Code legible.
4
+
5
+ <p align="center">
6
+ <img src="assets/demo.svg" alt="Without claude-math, LaTeX prints as raw dollar-sign noise; with it, the same answer renders as clean Unicode math." width="760">
7
+ </p>
8
+
9
+ Claude Code's terminal does not render LaTeX. Without help, a formula like
10
+ `$f(x) = \sum_{i=1}^n x_i$` appears as raw dollar signs and backslashes,
11
+ exactly the noise you wanted formatting to remove. This plugin ships a single
12
+ skill (`math-unicode`) that instructs Claude to emit math as Unicode glyphs
13
+ inline, which every terminal already renders.
14
+
15
+ ## Before / after
16
+
17
+ ```
18
+ Before: The qualifying cohort is $Q = \{ (s,r) \in T : n_{s,r} \geq 18 \wedge p^0_{s,r} < 0.9 \}$,
19
+ with $|Q| / |T| \approx 17.3\%$.
20
+
21
+ After: The qualifying cohort is Q = { (s,r) ∈ T : n_{s,r} ≥ 18 ∧ p⁰_{s,r} < 0.9 },
22
+ with |Q| / |T| ≈ 17.3 %.
23
+ ```
24
+
25
+ ## Install
26
+
27
+ The quickest install is through the plugin marketplace (see below). The npm and manual paths also work.
28
+
29
+ ### via npm
30
+
31
+ ```bash
32
+ npm install -g claude-math
33
+ claude-math install
34
+ ```
35
+
36
+ Symlinks the package into `~/.claude/plugins/local/claude-math`, registers it
37
+ in `installed_plugins.json`, and enables it in `settings.json` (atomically;
38
+ both files get a `.claude-math.bak` backup on first touch). Restart Claude
39
+ Code and the skill loads.
40
+
41
+ `npx claude-math install` also works: the CLI auto-detects an npx-cache
42
+ install path and **copies** rather than symlinks (since the cache directory
43
+ is ephemeral). Prefer the global install if you want updates via
44
+ `npm update -g claude-math` to propagate automatically.
45
+
46
+ Other commands:
47
+
48
+ ```bash
49
+ claude-math status # report target, validity, settings state, next-install mode
50
+ claude-math uninstall # remove symlink/dir, deregister, disable
51
+ claude-math --help # full flag list, env overrides
52
+ ```
53
+
54
+ Flags: `--force` overrides safety checks (foreign symlinks, non-plugin
55
+ directories at the target). `--copy` forces a real copy instead of symlink.
56
+
57
+ ### Manual install
58
+
59
+ ```bash
60
+ git clone https://github.com/vladimirrott/claude-math \
61
+ ~/.claude/plugins/local/claude-math
62
+ ```
63
+
64
+ Then add `"claude-math@local": true` under `enabledPlugins` in
65
+ `~/.claude/settings.json` and a matching entry in
66
+ `~/.claude/plugins/installed_plugins.json`. The CLI does both; running it
67
+ once is the easiest path.
68
+
69
+ ### Via Claude Code's plugin marketplace (recommended)
70
+
71
+ ```
72
+ /plugin marketplace add vladimirrott/claude-math
73
+ /plugin install claude-math@vladimirrott
74
+ ```
75
+
76
+ ### Codex CLI
77
+
78
+ The `math-unicode` skill works in [OpenAI Codex CLI](https://developers.openai.com/codex/) too: Codex reads skills from `$CODEX_HOME/skills/<name>/SKILL.md` using the same `name` + `description` frontmatter format, so the exact same skill drops straight in.
79
+
80
+ ```bash
81
+ npm install -g claude-math
82
+ claude-math install --codex
83
+ ```
84
+
85
+ This copies the skill into `$CODEX_HOME/skills/math-unicode/` (default `~/.codex/skills/`). Codex auto-detects new skills (restart Codex if it does not appear); invoke it with `/skills` or by mentioning `$math-unicode`. Use `claude-math status --codex` to check and `claude-math uninstall --codex` to remove.
86
+
87
+ ### Hacking on this repo
88
+
89
+ Working from a clone (before or after publish):
90
+
91
+ ```bash
92
+ git clone https://github.com/vladimirrott/claude-math && cd claude-math
93
+ node bin/claude-math.js install # uses the cloned directory directly
94
+ node --test test/ # run the test suite
95
+ ```
96
+
97
+ `CLAUDE_CONFIG_DIR=/tmp/somewhere claude-math install` lets you test against
98
+ a sandbox without touching your real `~/.claude` state, useful for
99
+ contributing.
100
+
101
+ The `math-unicode` skill auto-triggers any time Claude writes or explains math.
102
+ No configuration required.
103
+
104
+ ## Graphical rendering (sixel / kitty): not inside the chat
105
+
106
+ Rendering math as an actual image (sixel or kitty graphics) is **not possible
107
+ inside the Claude Code chat**. The TUI repaints its own screen buffer on every
108
+ update and overwrites any graphics escape sequences a plugin emits, and its
109
+ line accounting does not know an image's height. So in-chat output stays
110
+ Unicode, which is the point of this skill.
111
+
112
+ Roadmap (not built yet): a standalone `claude-math render "<latex>"` command
113
+ that converts LaTeX to an image and prints it via the terminal's graphics
114
+ protocol in your own graphics-capable terminal (kitty, wezterm, ghostty,
115
+ foot), outside the Claude Code TUI. It would be a convenience for viewing an
116
+ equation on demand, not in-chat rendering.
117
+
118
+ ## Why Unicode by default?
119
+
120
+ | Path | Works in plain terminal | SSH / tmux | CI logs | Copy-paste | Install cost |
121
+ |---|---|---|---|---|---|
122
+ | Unicode (this skill) | ✓ | ✓ | ✓ | ✓ | 1 file |
123
+ | Sixel / kitty graphics | ✗ (not in chat) | partial | ✗ | ✗ images | separate render CLI (roadmap) |
124
+ | Pipe through external viewer | ✗ TUI breaks | n/a | ✓ if `--print` | ✓ | shell wrapper |
125
+
126
+ Unicode is the only path that survives every distribution channel a Claude
127
+ Code session ends up in. The graphical path is roadmap-only and, even then,
128
+ would run as a separate terminal command outside the chat.
129
+
130
+ ## Related issues & prior art
131
+
132
+ - [anthropics/claude-code#44479](https://github.com/anthropics/claude-code/issues/44479): native LaTeX in terminal output (open)
133
+ - [openai/codex#15865](https://github.com/openai/codex/issues/15865): LaTeX in Codex CLI output (claude-math covers this via `claude-math install --codex`)
134
+ - [warpdotdev/warp#9677](https://github.com/warpdotdev/warp/issues/9677): same gap on Warp
135
+ - [`markless`](https://github.com/jvanderberg/markless): terminal Markdown viewer with Typst math and kitty/sixel images
136
+ - [`mdviewer`](https://github.com/aquele-dinho/mdviewer), [`glowm`](https://github.com/atani/glowm), [`mdterm`](https://www.toolhunter.cc/tools/mdterm): adjacent viewers
137
+
138
+ ## License
139
+
140
+ MIT.
@@ -0,0 +1,318 @@
1
+ #!/usr/bin/env node
2
+ // claude-math — install/uninstall the plugin into Claude Code's local plugin dir.
3
+
4
+ import {
5
+ copyFileSync,
6
+ cpSync,
7
+ existsSync,
8
+ lstatSync,
9
+ mkdirSync,
10
+ readFileSync,
11
+ readlinkSync,
12
+ renameSync,
13
+ rmSync,
14
+ symlinkSync,
15
+ unlinkSync,
16
+ writeFileSync,
17
+ } from "node:fs";
18
+ import { dirname, join, relative, resolve } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { homedir } from "node:os";
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = dirname(__filename);
24
+ const PLUGIN_ROOT = resolve(__dirname, "..");
25
+ const PLUGIN_NAME = "claude-math";
26
+ const PLUGIN_ID = `${PLUGIN_NAME}@local`;
27
+
28
+ const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
29
+ const PLUGINS_DIR = join(CLAUDE_DIR, "plugins");
30
+ const LOCAL_DIR = join(PLUGINS_DIR, "local");
31
+ const TARGET = join(LOCAL_DIR, PLUGIN_NAME);
32
+ const SETTINGS = join(CLAUDE_DIR, "settings.json");
33
+ const INSTALLED = join(PLUGINS_DIR, "installed_plugins.json");
34
+
35
+ // Codex target: Codex reads skills from $CODEX_HOME/skills/<name>/SKILL.md.
36
+ // The SKILL.md format (name + description frontmatter) is identical to Claude Code's,
37
+ // so the same skill directory drops straight in.
38
+ const SKILL_NAME = "math-unicode";
39
+ const SKILL_SRC = join(PLUGIN_ROOT, "skills", SKILL_NAME);
40
+ const CODEX_HOME = process.env.CODEX_HOME || join(homedir(), ".codex");
41
+ const CODEX_SKILLS_DIR = join(CODEX_HOME, "skills");
42
+ const CODEX_TARGET = join(CODEX_SKILLS_DIR, SKILL_NAME);
43
+
44
+ const pkg = JSON.parse(readFileSync(join(PLUGIN_ROOT, "package.json"), "utf8"));
45
+
46
+ const args = process.argv.slice(2);
47
+ const flags = new Set(args.filter((a) => a.startsWith("-")));
48
+ const cmd = args.find((a) => !a.startsWith("-"));
49
+ const FORCE = flags.has("--force") || flags.has("-f");
50
+ const FORCE_COPY = flags.has("--copy");
51
+ const CODEX = flags.has("--codex");
52
+
53
+ function log(msg) { console.log(`[claude-math] ${msg}`); }
54
+ function die(msg) { console.error(`[claude-math] ${msg}`); process.exit(1); }
55
+
56
+ function lstatSafe(p) { try { return lstatSync(p); } catch { return null; } }
57
+
58
+ function readJson(path, fallback) {
59
+ if (!existsSync(path)) return fallback;
60
+ try { return JSON.parse(readFileSync(path, "utf8")); }
61
+ catch (e) { die(`could not parse ${path}: ${e.message}`); }
62
+ }
63
+
64
+ function writeJsonAtomic(path, obj) {
65
+ mkdirSync(dirname(path), { recursive: true });
66
+ const tmp = `${path}.tmp-${process.pid}`;
67
+ writeFileSync(tmp, JSON.stringify(obj, null, 2) + "\n");
68
+ renameSync(tmp, path);
69
+ }
70
+
71
+ function backupOnce(path) {
72
+ const bak = `${path}.claude-math.bak`;
73
+ if (existsSync(path) && !existsSync(bak)) {
74
+ copyFileSync(path, bak);
75
+ log(`backed up ${path} → ${bak}`);
76
+ }
77
+ }
78
+
79
+ function isEphemeralPath(p) {
80
+ return /[\\/](?:_npx|_npm-cache)[\\/]/.test(p);
81
+ }
82
+
83
+ function looksLikeOurPlugin(path) {
84
+ return existsSync(join(path, ".claude-plugin", "plugin.json"))
85
+ && existsSync(join(path, "skills", "math-unicode", "SKILL.md"));
86
+ }
87
+
88
+ function chooseMode() {
89
+ if (FORCE_COPY) return "copy";
90
+ if (isEphemeralPath(PLUGIN_ROOT)) return "copy";
91
+ if (process.platform === "win32") return "junction";
92
+ return "symlink";
93
+ }
94
+
95
+ function clearTarget() {
96
+ const stat = lstatSafe(TARGET);
97
+ if (!stat) return;
98
+
99
+ if (stat.isSymbolicLink()) {
100
+ let current = "";
101
+ try { current = readlinkSync(TARGET); } catch {}
102
+ if (current === PLUGIN_ROOT || FORCE) {
103
+ unlinkSync(TARGET);
104
+ return;
105
+ }
106
+ die(`${TARGET} is a symlink to ${current}, not ${PLUGIN_ROOT}. Re-run with --force to overwrite.`);
107
+ }
108
+
109
+ if (stat.isDirectory()) {
110
+ if (!looksLikeOurPlugin(TARGET) && !FORCE) {
111
+ die(`${TARGET} is a directory but does not look like a claude-math install. Inspect manually, then re-run with --force.`);
112
+ }
113
+ rmSync(TARGET, { recursive: true, force: true });
114
+ return;
115
+ }
116
+
117
+ die(`${TARGET} exists and is neither symlink nor directory. Refusing to touch.`);
118
+ }
119
+
120
+ function placePlugin(mode) {
121
+ mkdirSync(LOCAL_DIR, { recursive: true });
122
+ if (mode === "copy") {
123
+ cpSync(PLUGIN_ROOT, TARGET, {
124
+ recursive: true,
125
+ filter: (src) => {
126
+ const rel = relative(PLUGIN_ROOT, src);
127
+ if (!rel) return true;
128
+ const top = rel.split(/[\\/]/, 1)[0];
129
+ return !["node_modules", ".git", "test", "tests"].includes(top);
130
+ },
131
+ });
132
+ log(`copied ${PLUGIN_ROOT} → ${TARGET}`);
133
+ return;
134
+ }
135
+ const linkType = mode === "junction" ? "junction" : "dir";
136
+ symlinkSync(PLUGIN_ROOT, TARGET, linkType);
137
+ log(`linked ${PLUGIN_ROOT} → ${TARGET} (${mode})`);
138
+ }
139
+
140
+ function registerInstalled() {
141
+ backupOnce(INSTALLED);
142
+ const data = readJson(INSTALLED, { version: 2, plugins: {} });
143
+ if (!data.plugins) data.plugins = {};
144
+ const now = new Date().toISOString();
145
+ const prev = data.plugins[PLUGIN_ID]?.[0];
146
+ data.plugins[PLUGIN_ID] = [{
147
+ scope: "user",
148
+ installPath: TARGET,
149
+ version: pkg.version,
150
+ installedAt: prev?.installedAt ?? now,
151
+ lastUpdated: now,
152
+ }];
153
+ writeJsonAtomic(INSTALLED, data);
154
+ log(`registered ${PLUGIN_ID}`);
155
+ }
156
+
157
+ function enableInSettings() {
158
+ backupOnce(SETTINGS);
159
+ const data = readJson(SETTINGS, {});
160
+ if (!data.enabledPlugins) data.enabledPlugins = {};
161
+ data.enabledPlugins[PLUGIN_ID] = true;
162
+ writeJsonAtomic(SETTINGS, data);
163
+ log(`enabled ${PLUGIN_ID}`);
164
+ }
165
+
166
+ function removeTarget() {
167
+ const stat = lstatSafe(TARGET);
168
+ if (!stat) return;
169
+ if (stat.isSymbolicLink()) {
170
+ unlinkSync(TARGET);
171
+ log(`removed symlink ${TARGET}`);
172
+ return;
173
+ }
174
+ if (stat.isDirectory() && (looksLikeOurPlugin(TARGET) || FORCE)) {
175
+ rmSync(TARGET, { recursive: true, force: true });
176
+ log(`removed directory ${TARGET}`);
177
+ return;
178
+ }
179
+ log(`${TARGET} exists but is not our plugin — leaving alone (use --force to remove anyway)`);
180
+ }
181
+
182
+ function unregister() {
183
+ const installed = readJson(INSTALLED, null);
184
+ if (installed?.plugins?.[PLUGIN_ID]) {
185
+ delete installed.plugins[PLUGIN_ID];
186
+ writeJsonAtomic(INSTALLED, installed);
187
+ log(`unregistered ${PLUGIN_ID}`);
188
+ }
189
+ const settings = readJson(SETTINGS, null);
190
+ if (settings?.enabledPlugins?.[PLUGIN_ID] !== undefined) {
191
+ delete settings.enabledPlugins[PLUGIN_ID];
192
+ writeJsonAtomic(SETTINGS, settings);
193
+ log(`disabled ${PLUGIN_ID}`);
194
+ }
195
+ }
196
+
197
+ function installCodex() {
198
+ mkdirSync(CODEX_SKILLS_DIR, { recursive: true });
199
+ const stat = lstatSafe(CODEX_TARGET);
200
+ if (stat) {
201
+ const ours = existsSync(join(CODEX_TARGET, "SKILL.md"));
202
+ if (!ours && !FORCE) {
203
+ die(`${CODEX_TARGET} exists and is not a math-unicode skill. Inspect it, then re-run with --force.`);
204
+ }
205
+ rmSync(CODEX_TARGET, { recursive: true, force: true });
206
+ }
207
+ cpSync(SKILL_SRC, CODEX_TARGET, { recursive: true });
208
+ log(`installed skill → ${CODEX_TARGET}`);
209
+ log("Codex auto-detects new skills; restart Codex if it doesn't appear. Invoke with /skills or $math-unicode.");
210
+ }
211
+
212
+ function uninstallCodex() {
213
+ const stat = lstatSafe(CODEX_TARGET);
214
+ if (!stat) { log(`nothing to remove at ${CODEX_TARGET}`); return; }
215
+ if (existsSync(join(CODEX_TARGET, "SKILL.md")) || FORCE) {
216
+ rmSync(CODEX_TARGET, { recursive: true, force: true });
217
+ log(`removed skill ${CODEX_TARGET}`);
218
+ return;
219
+ }
220
+ log(`${CODEX_TARGET} exists but is not our skill — leaving alone (use --force to remove anyway)`);
221
+ }
222
+
223
+ function statusCodex() {
224
+ const present = existsSync(join(CODEX_TARGET, "SKILL.md"));
225
+ console.log(`codex skill: ${present ? "✓" : "✗"} ${CODEX_TARGET}`);
226
+ console.log(`codex home: ${CODEX_HOME}`);
227
+ }
228
+
229
+ function install() {
230
+ if (CODEX) return installCodex();
231
+ const mode = chooseMode();
232
+ clearTarget();
233
+ placePlugin(mode);
234
+ registerInstalled();
235
+ enableInSettings();
236
+ log("done. Restart Claude Code to load the skill.");
237
+ }
238
+
239
+ function uninstall() {
240
+ if (CODEX) return uninstallCodex();
241
+ removeTarget();
242
+ unregister();
243
+ log("done. Restart Claude Code to drop the skill.");
244
+ }
245
+
246
+ function status() {
247
+ if (CODEX) return statusCodex();
248
+ const stat = lstatSafe(TARGET);
249
+ const kind = !stat ? "missing"
250
+ : stat.isSymbolicLink() ? "symlink"
251
+ : stat.isDirectory() ? "directory"
252
+ : "other";
253
+ const installed = !!readJson(INSTALLED, { plugins: {} }).plugins?.[PLUGIN_ID];
254
+ const enabled = readJson(SETTINGS, {}).enabledPlugins?.[PLUGIN_ID] === true;
255
+ const valid = stat ? looksLikeOurPlugin(TARGET) : false;
256
+ console.log(`target: ${kind === "missing" ? "✗" : "✓"} ${TARGET} (${kind})`);
257
+ console.log(`valid: ${valid ? "✓" : "✗"} plugin files present`);
258
+ console.log(`installed: ${installed ? "✓" : "✗"} ${INSTALLED}`);
259
+ console.log(`enabled: ${enabled ? "✓" : "✗"} ${SETTINGS}`);
260
+ console.log(`mode: ${chooseMode()} (would-be on next install)`);
261
+ }
262
+
263
+ function prepack() {
264
+ const pluginJsonPath = join(PLUGIN_ROOT, ".claude-plugin", "plugin.json");
265
+ const pj = JSON.parse(readFileSync(pluginJsonPath, "utf8"));
266
+ if (pj.version !== pkg.version) {
267
+ pj.version = pkg.version;
268
+ writeFileSync(pluginJsonPath, JSON.stringify(pj, null, 2) + "\n");
269
+ log(`synced plugin.json version → ${pkg.version}`);
270
+ } else {
271
+ log(`plugin.json already at ${pkg.version}`);
272
+ }
273
+ }
274
+
275
+ function help() {
276
+ console.log(`claude-math v${pkg.version}
277
+
278
+ Usage:
279
+ claude-math install [--force] [--copy] [--codex]
280
+ Symlink (or copy) the plugin into ~/.claude/plugins/local/ and enable it.
281
+ Auto-copies if invoked via npx; auto-junctions on Windows.
282
+ --force Overwrite an existing install at the target path.
283
+ --copy Force copy mode even on supported platforms.
284
+ --codex Install the math-unicode skill for Codex CLI instead, into
285
+ $CODEX_HOME/skills/math-unicode/ (default ~/.codex/skills/).
286
+
287
+ claude-math uninstall [--force] [--codex]
288
+ Remove the plugin and disable it. --force lets it remove a directory
289
+ that does not look like a claude-math install. --codex removes the
290
+ Codex skill instead.
291
+
292
+ claude-math status [--codex]
293
+ Show install state, target kind, and which mode would be used.
294
+ --codex shows the Codex skill install state instead.
295
+
296
+ claude-math prepack
297
+ Sync .claude-plugin/plugin.json version from package.json. Runs as an
298
+ npm \`prepack\` hook; safe to run manually.
299
+
300
+ claude-math --version
301
+ claude-math --help
302
+
303
+ Env:
304
+ CLAUDE_CONFIG_DIR Override the Claude config directory (default ~/.claude).
305
+ `);
306
+ }
307
+
308
+ switch (cmd) {
309
+ case "install": install(); break;
310
+ case "uninstall":
311
+ case "remove": uninstall(); break;
312
+ case "status": status(); break;
313
+ case "prepack": prepack(); break;
314
+ case undefined:
315
+ if (flags.has("--version") || flags.has("-v")) { console.log(pkg.version); break; }
316
+ help(); break;
317
+ default: die(`unknown command: ${cmd}. Run 'claude-math --help'.`);
318
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "claude-math",
3
+ "version": "0.1.0",
4
+ "description": "Render math legibly in Claude Code. Emits Unicode glyphs instead of unrenderable LaTeX in the terminal. (Graphical sixel/kitty rendering is on the roadmap.)",
5
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "claude-code-plugin",
9
+ "skill",
10
+ "math",
11
+ "latex",
12
+ "katex",
13
+ "unicode",
14
+ "terminal"
15
+ ],
16
+ "homepage": "https://github.com/vladimirrott/claude-math",
17
+ "bugs": {
18
+ "url": "https://github.com/vladimirrott/claude-math/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/vladimirrott/claude-math.git"
23
+ },
24
+ "license": "MIT",
25
+ "author": {
26
+ "name": "Vladimir Rotariu",
27
+ "email": "vladimirrott@users.noreply.github.com"
28
+ },
29
+ "type": "module",
30
+ "bin": {
31
+ "claude-math": "bin/claude-math.js"
32
+ },
33
+ "files": [
34
+ ".claude-plugin/",
35
+ "skills/",
36
+ "bin/",
37
+ "README.md",
38
+ "LICENSE",
39
+ "CHANGELOG.md"
40
+ ],
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "scripts": {
45
+ "test": "node --test test/*.test.js",
46
+ "prepack": "node bin/claude-math.js prepack",
47
+ "prepublishOnly": "npm test && npm pack --dry-run"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }
@@ -0,0 +1,211 @@
1
+ ---
2
+ name: math-unicode
3
+ description: Use whenever you need to express mathematical notation — equations, filters, set-builder, statistics, calculus, logic, ratios, drops, counts. Emit Unicode math glyphs INLINE; never wrap in `$…$`, `\(...\)`, or `$$...$$`. Claude Code terminal does not render LaTeX, so raw delimiters appear as noise. This skill activates automatically when math is involved.
4
+ ---
5
+
6
+ # math-unicode
7
+
8
+ When emitting mathematical notation in Claude Code, **always use Unicode glyphs inline** — never wrap math in `$…$`, `\(...\)`, or `$$...$$`. Claude Code's terminal does not render LaTeX; raw delimiters appear as plain dollar signs and reduce readability.
9
+
10
+ ## When this skill applies
11
+
12
+ Triggers (use Unicode math):
13
+ - Equations, formulas, derivations
14
+ - Filter conditions, set-builder notation
15
+ - Statistics: probabilities, expectations, distributions
16
+ - Calculus, linear algebra, logic
17
+ - Counts, ratios, fractions, drops where precision matters
18
+
19
+ Skip (do not transform):
20
+ - The user explicitly asks for LaTeX or a `.tex` file
21
+ - Math inside fenced code blocks (preserve source syntax)
22
+ - Strings being passed to a system that consumes LaTeX (KaTeX MCP, etc.)
23
+
24
+ ## Glyph cheatsheet
25
+
26
+ ### Greek
27
+
28
+ ```
29
+ lowercase α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ σ τ υ φ χ ψ ω
30
+ uppercase Α Β Γ Δ Ε Ζ Η Θ Ι Κ Λ Μ Ν Ξ Ο Π Ρ Σ Τ Υ Φ Χ Ψ Ω
31
+ variants ϵ ϑ ϕ ϖ ϱ ς
32
+ ```
33
+
34
+ ### Operators
35
+
36
+ ```
37
+ arithmetic + − × ÷ ± ∓ · ∗ ⋅ ∘ ⊕ ⊖ ⊗ ⊘ ⊙
38
+ big ∑ ∏ ∐ ∫ ∮ ∬ ∭ ⨁ ⨂ ⨅ ⨆
39
+ roots √ ∛ ∜
40
+ calculus ∂ ∇ Δ ∆ ⅆ ⅇ
41
+ constants ∞ ∅ ℵ ℶ
42
+ ```
43
+
44
+ ### Relations
45
+
46
+ ```
47
+ equality = ≠ ≈ ≅ ≡ ≜ ≝ ≐ ∝ ∼ ≃ ≢
48
+ order < > ≤ ≥ ≪ ≫ ⋘ ⋙ ⊴ ⊵
49
+ set ∈ ∉ ∋ ∌ ⊂ ⊃ ⊆ ⊇ ⊊ ⊋ ⊏ ⊐ ⊑ ⊒
50
+ set ops ∪ ∩ ⊎ ⊔ ⊓ ∖
51
+ ```
52
+
53
+ ### Logic & arrows
54
+
55
+ ```
56
+ logic ∧ ∨ ¬ ⊕ ⊻ ⊼ ⊽ ⊢ ⊨ ⊥ ⊤
57
+ quantifiers ∀ ∃ ∄ ∴ ∵
58
+ arrows → ← ↔ ⇒ ⇐ ⇔ ↦ ↪ ↩ ↑ ↓ ⇑ ⇓ ⟶ ⟵ ⟷ ⟹ ⟸ ⟺ ⊸
59
+ ```
60
+
61
+ ### Number sets & brackets
62
+
63
+ ```
64
+ sets ℕ ℤ ℚ ℝ ℂ ℙ ℍ 𝔽
65
+ brackets ⟨ ⟩ ⌈ ⌉ ⌊ ⌋ ‖ ‖ 〈 〉
66
+ ```
67
+
68
+ ### Sub/superscript glyph blocks
69
+
70
+ ```
71
+ superscript ⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁺ ⁻ ⁼ ⁽ ⁾ ⁱ ⁿ ᵃ ᵇ ᶜ ᵈ ᵉ ᶠ ᵍ ʰ ʲ ᵏ ˡ ᵐ ᵒ ᵖ ʳ ˢ ᵗ ᵘ ᵛ ʷ ˣ ʸ ᶻ
72
+ subscript ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₊ ₋ ₌ ₍ ₎ ₐ ₑ ₕ ᵢ ⱼ ₖ ₗ ₘ ₙ ₒ ₚ ᵣ ₛ ₜ ᵤ ᵥ ₓ
73
+ ```
74
+
75
+ ### Common LaTeX → Unicode
76
+
77
+ | LaTeX | Unicode | LaTeX | Unicode | LaTeX | Unicode |
78
+ |---|---|---|---|---|---|
79
+ | `\alpha` | α | `\sum` | ∑ | `\in` | ∈ |
80
+ | `\beta` | β | `\prod` | ∏ | `\notin` | ∉ |
81
+ | `\gamma` | γ | `\int` | ∫ | `\subset` | ⊂ |
82
+ | `\delta` | δ | `\partial` | ∂ | `\subseteq` | ⊆ |
83
+ | `\epsilon` | ε | `\nabla` | ∇ | `\cup` | ∪ |
84
+ | `\theta` | θ | `\infty` | ∞ | `\cap` | ∩ |
85
+ | `\lambda` | λ | `\emptyset` | ∅ | `\setminus` | ∖ |
86
+ | `\mu` | μ | `\leq` | ≤ | `\wedge` | ∧ |
87
+ | `\pi` | π | `\geq` | ≥ | `\vee` | ∨ |
88
+ | `\sigma` | σ | `\neq` | ≠ | `\neg` | ¬ |
89
+ | `\phi` | φ | `\approx` | ≈ | `\Rightarrow` | ⇒ |
90
+ | `\omega` | ω | `\equiv` | ≡ | `\Leftrightarrow` | ⇔ |
91
+ | `\sqrt` | √ | `\propto` | ∝ | `\forall` | ∀ |
92
+ | `\pm` | ± | `\cdot` | · | `\exists` | ∃ |
93
+ | `\times` | × | `\to` | → | `\mathbb{R}` | ℝ |
94
+
95
+ ## Style rules
96
+
97
+ ### Rule 1 — Inline math: Unicode, no delimiters
98
+
99
+ Bad: `The filter $f(T; m) = \{(s,r) : n_{s,r} \geq m\}$ produces the cohort.`
100
+ Good: `The filter f(T; m) = { (s,r) : n_{s,r} ≥ m } produces the cohort.`
101
+
102
+ ### Rule 2 — Block math: own line(s), still no delimiters
103
+
104
+ Bad:
105
+
106
+ $$|Q| / |T| = 5238 / 31075 \approx 16.9\%$$
107
+
108
+ Good:
109
+
110
+ |Q| / |T| = 5 238 / 31 075 ≈ 16.9 %
111
+
112
+ ### Rule 3 — Subscripts
113
+
114
+ - Single Unicode-renderable index: prefer the glyph (x₁, x₂, xᵢ, xⱼ)
115
+ - Multi-character or non-mappable subscript: keep `_{...}` syntax for clarity
116
+ - `n_{s,r}` ← (s,r) has no Unicode subscript form
117
+ - `x_max`, `σ_obs` ← multi-letter
118
+ - Never mix: don't write `x_₁` or `x_{1}` when `x₁` works.
119
+
120
+ ### Rule 4 — Superscripts (powers)
121
+
122
+ - Integer powers, simple: x², x³, xⁿ, e^x → eˣ
123
+ - Multi-character / non-mappable: `x^{T}`, `x^{(i)}`, `A^{-1}` (or A⁻¹ if the exponent is simple)
124
+
125
+ ### Rule 5 — Big operators with indices
126
+
127
+ Use Unicode operator + ASCII bounds:
128
+
129
+ ```
130
+ ∑_{i=1}^{n} aᵢ
131
+ ∏_{k ∈ K} pₖ
132
+ ∫_a^b f(x) dx
133
+ ```
134
+
135
+ ### Rule 6 — Fractions
136
+
137
+ - Inline: `a/b`, `(a + b) / (c + d)`
138
+ - Block (only when it aids clarity):
139
+
140
+ ```
141
+ a + b
142
+ ─────────────
143
+ c² + d²
144
+ ```
145
+
146
+ ### Rule 7 — Matrices / vectors
147
+
148
+ ASCII art with corner glyphs:
149
+
150
+ ```
151
+ A = ⎡ a b ⎤ v = ( v₁ , v₂ , v₃ )ᵀ
152
+ ⎣ c d ⎦
153
+ ```
154
+
155
+ ### Rule 8 — Sets and conditions
156
+
157
+ Prefer set-builder with `|` or `:`:
158
+
159
+ ```
160
+ Q = { (s,r) ∈ T : n_{s,r} ≥ 18 ∧ p⁰_{s,r} < 0.9 }
161
+ ```
162
+
163
+ ### Rule 9 — Numbers
164
+
165
+ - Thousands: thin space (` `) — `5 238`, `34 601` — not commas (locale ambiguous).
166
+ - Decimal: dot — `16.9 %`.
167
+ - Percent: space before `%` — `16.9 %` (typographic convention; readable).
168
+ - Approximations: ≈, ∼. Order of magnitude: ~. Confidence: `x = 5.2 ± 0.3`.
169
+
170
+ ### Rule 10 — When Unicode hurts, fall back explicitly
171
+
172
+ If a glyph chain becomes denser than the LaTeX it replaces, switch to readable ASCII pseudo-LaTeX and annotate it. Example:
173
+
174
+ ```
175
+ H(p) = − Σ_{x ∈ X} p(x) · log p(x) (Σ = sum over the support X)
176
+ ```
177
+
178
+ The reader's comprehension is the only metric. Choose whichever form is clearest, then stay consistent within a passage.
179
+
180
+ ### Rule 11 — Plain letters for variables; never style with math-alphanumeric codepoints
181
+
182
+ Write variable names and identifiers with ordinary letters (x, A, Var, RSS). Do **not** reach into the Unicode *Mathematical Alphanumeric Symbols* block (𝐀 bold, 𝐴 italic, 𝓐 script, 𝔸 styled double-struck) to *style* ordinary letters. Those codepoints garble on copy/paste, terminal search, and screen readers — the same failure Claude Code hit in issue #61558.
183
+
184
+ Exception: the standard, semantically meaningful blackboard-bold sets and operators are correct notation, not styling — keep using ℕ ℤ ℚ ℝ ℂ ℙ 𝔽 (number sets) and 𝔼 (expectation). Use those; don't hand-style anything else.
185
+
186
+ ## Quick reference — common forms
187
+
188
+ ```
189
+ Mean / std μ ± σ x̄ ± s
190
+ Probability P(A | B) ℙ(A ∩ B) = ℙ(A) · ℙ(B | A)
191
+ Expectation 𝔼[X] = ∫ x · f(x) dx
192
+ Variance Var(X) = 𝔼[X²] − 𝔼[X]²
193
+ Gradient ∇f = ( ∂f/∂x₁ , ... , ∂f/∂xₙ )
194
+ Norm ‖x‖₂ = √(Σᵢ xᵢ²)
195
+ Big-O T(n) = O(n log n)
196
+ Limit lim_{n → ∞} aₙ = L
197
+ Sum bounds ∑_{i=1}^{n} i = n(n+1)/2
198
+ Quantile q_α = inf{ x : F(x) ≥ α }
199
+ ```
200
+
201
+ ## Anti-patterns — never emit these in Claude Code terminal
202
+
203
+ ```
204
+ ✗ $f(x) = \sum_{i=1}^{n} x_i$
205
+ ✗ \( a^2 + b^2 = c^2 \)
206
+ ✗ $$\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}$$
207
+ ✗ \[ |Q|/|T| \approx 16.9\% \]
208
+ ✗ Let 𝑉𝑎𝑟 = … or matrix 𝐀 = … (math-alphanumeric styling; garbles on copy/search — write Var, A)
209
+ ```
210
+
211
+ If asked to produce raw LaTeX (e.g. for a `.tex` file or a KaTeX-rendering tool downstream), do so — and call it out explicitly: *"Raw LaTeX as requested; this will not render in the terminal."*