@npow/oh-my-claude 0.2.0 → 0.3.1
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/README.md +36 -21
- package/bin/omc.js +284 -64
- package/docs/architecture.md +125 -43
- package/docs/{segment-contract.md → plugin-contract.md} +21 -21
- package/docs/theme-format.md +20 -20
- package/package.json +2 -2
- package/src/cache.js +78 -1
- package/src/color.js +64 -0
- package/src/compositor.js +51 -20
- package/src/config.js +6 -6
- package/src/{segments → plugins}/achievement.js +2 -2
- package/src/{segments → plugins}/api-timer.js +2 -2
- package/src/{segments → plugins}/battle-log.js +2 -2
- package/src/{segments → plugins}/cat.js +2 -2
- package/src/{segments → plugins}/coffee-cup.js +2 -2
- package/src/{segments → plugins}/commit-msg.js +2 -2
- package/src/{segments → plugins}/context-bar.js +2 -2
- package/src/{segments → plugins}/context-percent.js +2 -2
- package/src/{segments → plugins}/context-tokens.js +2 -2
- package/src/{segments → plugins}/cost-budget.js +2 -2
- package/src/{segments → plugins}/coworker.js +2 -2
- package/src/{segments → plugins}/custom-text.js +2 -2
- package/src/{segments → plugins}/directory.js +2 -2
- package/src/{segments → plugins}/emoji-story.js +2 -2
- package/src/{segments → plugins}/flex-space.js +4 -4
- package/src/{segments → plugins}/fortune-cookie.js +2 -2
- package/src/{segments → plugins}/garden.js +2 -2
- package/src/{segments → plugins}/git-branch.js +2 -2
- package/src/{segments → plugins}/git-status.js +2 -2
- package/src/{segments → plugins}/horoscope.js +2 -2
- package/src/{segments → plugins}/index.js +7 -7
- package/src/{segments → plugins}/lines-changed.js +2 -2
- package/src/{segments → plugins}/model-name.js +2 -2
- package/src/{segments → plugins}/narrator.js +2 -2
- package/src/{segments → plugins}/output-style.js +2 -2
- package/src/{segments → plugins}/rpg-stats.js +2 -2
- package/src/{segments → plugins}/separator-arrow.js +2 -2
- package/src/{segments → plugins}/separator-pipe.js +2 -2
- package/src/{segments → plugins}/separator-space.js +3 -3
- package/src/{segments → plugins}/session-cost.js +2 -2
- package/src/{segments → plugins}/session-timer.js +2 -2
- package/src/{segments → plugins}/smart-nudge.js +2 -2
- package/src/{segments → plugins}/soundtrack.js +3 -3
- package/src/{segments → plugins}/speedrun.js +2 -2
- package/src/{segments → plugins}/stock-ticker.js +2 -2
- package/src/{segments → plugins}/streak.js +2 -2
- package/src/{segments → plugins}/tamagotchi.js +2 -2
- package/src/{segments → plugins}/token-sparkline.js +2 -2
- package/src/{segments → plugins}/version.js +2 -2
- package/src/{segments → plugins}/vibe-check.js +3 -3
- package/src/{segments → plugins}/vim-mode.js +2 -2
- package/src/{segments → plugins}/weather-report.js +2 -2
- package/src/plugins.js +76 -13
- package/src/runner.js +22 -22
- package/themes/boss-battle.json +1 -1
- package/themes/coworker.json +1 -1
- package/themes/danger-zone.json +1 -1
- package/themes/default.json +1 -1
- package/themes/minimal.json +1 -1
- package/themes/narrator.json +1 -1
- package/themes/powerline.json +1 -1
- package/themes/rpg.json +1 -1
- package/themes/tamagotchi.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
# oh-my-claude
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Themes and plugins for [Claude Code's](https://docs.anthropic.com/en/docs/claude-code) status bar.**
|
|
4
|
+
|
|
5
|
+
[Claude Code](https://docs.anthropic.com/en/docs/claude-code) has a [statusline](https://docs.anthropic.com/en/docs/claude-code/settings#statusline) -- a bar pinned to the bottom of your terminal. It's blank by default. oh-my-claude gives it something to say.
|
|
6
|
+
|
|
7
|
+

|
|
4
8
|
|
|
5
9
|
[](https://www.npmjs.com/package/@npow/oh-my-claude)
|
|
6
10
|
[](https://github.com/npow/oh-my-claude/actions/workflows/ci.yml)
|
|
7
11
|
[](LICENSE)
|
|
8
12
|
[](package.json)
|
|
9
13
|
|
|
14
|
+
Claude Code streams your session data (model, context usage, cost, git info) into the statusline as JSON. oh-my-claude picks it up, runs it through a pipeline of **plugins** -- small functions that each render one piece of the bar -- and writes styled output back. A **theme** is just a layout: which plugins, in what order, left or right aligned.
|
|
15
|
+
|
|
16
|
+
Some plugins show data straight: `42%`, `$2.41`, `main`. Others get creative -- `battle-log` turns context usage into a dungeon crawl (`⚔️ Boss Battle (85%)`), `garden` grows an ASCII plant as you add lines of code, and `coffee-cup` drains over a 2-hour session. A few are just for fun (`fortune-cookie`, `cat`). Themes mix and match all of these.
|
|
17
|
+
|
|
18
|
+
| Your session | `default` | `boss-battle` | `tamagotchi` |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| 42% context | `██████░░░░ 42%` | `🗡️ Mid Dungeon (42%)` | `(^.^)` happy pet |
|
|
21
|
+
| 85% context | `████████████░░ 85%` | `⚔️ Boss Battle (85%)` | `(×_×)!!` panicking pet |
|
|
22
|
+
| 250 lines added | `+250` | `+250 gold` | `(🌿)` growing garden |
|
|
23
|
+
| 45min in | `45m 0s` | `45m 0s` | `[█░░░]` coffee half-empty |
|
|
24
|
+
|
|
10
25
|
## Install
|
|
11
26
|
|
|
12
27
|
```bash
|
|
@@ -14,7 +29,7 @@ npm install -g @npow/oh-my-claude
|
|
|
14
29
|
omc install
|
|
15
30
|
```
|
|
16
31
|
|
|
17
|
-
|
|
32
|
+
That's it -- takes effect immediately, no restart needed.
|
|
18
33
|
|
|
19
34
|
---
|
|
20
35
|
|
|
@@ -94,11 +109,11 @@ Run `npm run showcase` to see all themes live in your terminal.
|
|
|
94
109
|
|
|
95
110
|
---
|
|
96
111
|
|
|
97
|
-
## 41
|
|
112
|
+
## 41 plugins, mix and match
|
|
98
113
|
|
|
99
114
|
### Have fun while you wait
|
|
100
115
|
|
|
101
|
-
|
|
|
116
|
+
| Plugin | What it does | Example |
|
|
102
117
|
|---------|--------------|---------|
|
|
103
118
|
| `tamagotchi` | Virtual pet reacts to your session | `(^.^)` happy, `(x_x) RIP` at 95% context |
|
|
104
119
|
| `cat` | A cat doing cat things | `=^._.^= *sits on context window*` |
|
|
@@ -115,7 +130,7 @@ Run `npm run showcase` to see all themes live in your terminal.
|
|
|
115
130
|
|
|
116
131
|
### Gamify your session
|
|
117
132
|
|
|
118
|
-
|
|
|
133
|
+
| Plugin | What it does | Example |
|
|
119
134
|
|---------|--------------|---------|
|
|
120
135
|
| `achievement` | Unlockable badges | `Centurion` at 100 lines, `Whale` at $20 |
|
|
121
136
|
| `rpg-stats` | D&D character sheet | `Lv.9 STR:18 DEX:4 INT:11 WIS:18 CHA:0` |
|
|
@@ -127,7 +142,7 @@ Run `npm run showcase` to see all themes live in your terminal.
|
|
|
127
142
|
|
|
128
143
|
### Stay productive
|
|
129
144
|
|
|
130
|
-
|
|
|
145
|
+
| Plugin | What it shows | Example |
|
|
131
146
|
|---------|---------------|---------|
|
|
132
147
|
| `context-bar` | Visual context progress bar | `██████░░░░░░░░░ 38%` |
|
|
133
148
|
| `context-percent` | Context usage as number | `38%` |
|
|
@@ -148,9 +163,9 @@ Run `npm run showcase` to see all themes live in your terminal.
|
|
|
148
163
|
|
|
149
164
|
### Layout building blocks
|
|
150
165
|
|
|
151
|
-
|
|
|
166
|
+
| Plugin | Purpose |
|
|
152
167
|
|---------|---------|
|
|
153
|
-
| `separator-pipe` | Pipe `│` between
|
|
168
|
+
| `separator-pipe` | Pipe `│` between plugins |
|
|
154
169
|
| `separator-arrow` | Powerline arrow separator |
|
|
155
170
|
| `separator-space` | Whitespace |
|
|
156
171
|
| `flex-space` | Right-alignment marker |
|
|
@@ -207,20 +222,20 @@ See [screenshots above](#pick-your-vibe) for each theme in action.
|
|
|
207
222
|
|
|
208
223
|
---
|
|
209
224
|
|
|
210
|
-
##
|
|
225
|
+
## Write your own
|
|
211
226
|
|
|
212
|
-
No fork needed
|
|
227
|
+
No fork needed:
|
|
213
228
|
|
|
214
229
|
```bash
|
|
215
|
-
omc create my-
|
|
230
|
+
omc create my-plugin
|
|
216
231
|
```
|
|
217
232
|
|
|
218
|
-
Creates `~/.claude/oh-my-claude/plugins/my-
|
|
233
|
+
Creates `~/.claude/oh-my-claude/plugins/my-plugin/plugin.js`:
|
|
219
234
|
|
|
220
235
|
```js
|
|
221
236
|
export const meta = {
|
|
222
|
-
name: 'my-
|
|
223
|
-
description: 'My custom
|
|
237
|
+
name: 'my-plugin',
|
|
238
|
+
description: 'My custom plugin',
|
|
224
239
|
requires: [],
|
|
225
240
|
defaultConfig: {},
|
|
226
241
|
};
|
|
@@ -230,11 +245,11 @@ export function render(data, config) {
|
|
|
230
245
|
}
|
|
231
246
|
```
|
|
232
247
|
|
|
233
|
-
Add it to your theme
|
|
248
|
+
Add it to your theme and it takes effect immediately. Three rules: export `meta`, export `render`, return `{ text, style }` or `null`.
|
|
234
249
|
|
|
235
|
-
Full data field reference: [docs/
|
|
250
|
+
Full data field reference: [docs/plugin-contract.md](docs/plugin-contract.md)
|
|
236
251
|
|
|
237
|
-
**Share
|
|
252
|
+
**Share it:** PR it into `src/plugins/` or post your `plugin.js` anywhere -- others drop it in their plugins directory and go.
|
|
238
253
|
|
|
239
254
|
---
|
|
240
255
|
|
|
@@ -244,9 +259,9 @@ Full data field reference: [docs/segment-contract.md](docs/segment-contract.md)
|
|
|
244
259
|
omc install Interactive setup wizard
|
|
245
260
|
omc theme <name> Switch theme (e.g. omc theme tamagotchi)
|
|
246
261
|
omc themes List available themes
|
|
247
|
-
omc create <name> Scaffold a new plugin
|
|
248
|
-
omc list List all 41
|
|
249
|
-
omc validate Check
|
|
262
|
+
omc create <name> Scaffold a new plugin
|
|
263
|
+
omc list List all 41 built-in plugins
|
|
264
|
+
omc validate Check plugin contract compliance
|
|
250
265
|
omc uninstall Remove from Claude Code
|
|
251
266
|
```
|
|
252
267
|
|
|
@@ -259,7 +274,7 @@ omc uninstall Remove from Claude Code
|
|
|
259
274
|
|
|
260
275
|
## Contributing
|
|
261
276
|
|
|
262
|
-
PRs welcome. One file per
|
|
277
|
+
PRs welcome. One file per plugin, export `meta` + `render`, handle nulls, run `npm run validate`.
|
|
263
278
|
|
|
264
279
|
## License
|
|
265
280
|
|
package/bin/omc.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// bin/omc.js — oh-my-claude CLI
|
|
4
4
|
// Usage: npx oh-my-claude [command]
|
|
5
|
-
// Commands: install, theme, themes, uninstall, list, validate, create
|
|
5
|
+
// Commands: install, install <url>, theme, themes, uninstall, list, validate, create
|
|
6
6
|
|
|
7
|
-
import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, readdirSync } from 'node:fs';
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, readdirSync, chmodSync, accessSync, constants } from 'node:fs';
|
|
8
8
|
import { join, dirname } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { createInterface } from 'node:readline';
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
12
13
|
|
|
13
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
const PACKAGE_ROOT = join(__dirname, '..');
|
|
@@ -90,7 +91,7 @@ async function install() {
|
|
|
90
91
|
// 5. Write user config
|
|
91
92
|
const config = {
|
|
92
93
|
theme,
|
|
93
|
-
|
|
94
|
+
plugins: {
|
|
94
95
|
'cost-budget': { budget },
|
|
95
96
|
},
|
|
96
97
|
};
|
|
@@ -142,6 +143,93 @@ async function install() {
|
|
|
142
143
|
log(`Uninstall: omc uninstall${C.reset}\n`);
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
// ─── Install Plugin from URL/Path ────────────────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Extract a plugin name from a git URL or local path.
|
|
150
|
+
* Strips .git suffix, takes the last path component, lowercases it.
|
|
151
|
+
*/
|
|
152
|
+
function extractRepoName(urlOrPath) {
|
|
153
|
+
const cleaned = urlOrPath.replace(/\/+$/, '').replace(/\.git$/, '');
|
|
154
|
+
const parts = cleaned.split('/');
|
|
155
|
+
return parts[parts.length - 1].toLowerCase();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Install a plugin from a git URL or local path.
|
|
160
|
+
*/
|
|
161
|
+
function installPlugin(urlOrPath) {
|
|
162
|
+
if (!urlOrPath) {
|
|
163
|
+
warn('Usage: omc install <git-url-or-path>');
|
|
164
|
+
log(`\n${C.dim}Example: omc install https://github.com/user/omc-plugin-hello${C.reset}`);
|
|
165
|
+
log(`${C.dim} omc install /path/to/local/plugin${C.reset}\n`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const name = extractRepoName(urlOrPath);
|
|
170
|
+
const dest = join(PLUGINS_DIR, name);
|
|
171
|
+
|
|
172
|
+
if (existsSync(dest)) {
|
|
173
|
+
warn(`Plugin "${name}" already exists at:`);
|
|
174
|
+
log(` ${C.dim}${dest}${C.reset}`);
|
|
175
|
+
log(`\n${C.dim}To reinstall, remove it first: rm -rf ${dest}${C.reset}\n`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
180
|
+
|
|
181
|
+
log(`\n${C.bold}Installing plugin:${C.reset} ${name}\n`);
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
execSync(`git clone --depth 1 ${urlOrPath} ${dest}`, {
|
|
185
|
+
encoding: 'utf8',
|
|
186
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
187
|
+
timeout: 30000,
|
|
188
|
+
});
|
|
189
|
+
} catch (err) {
|
|
190
|
+
warn(`Failed to clone: ${err.message || 'unknown error'}`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check for valid plugin: plugin.js or executable plugin
|
|
195
|
+
const hasPluginJs = existsSync(join(dest, 'plugin.js'));
|
|
196
|
+
const pluginScript = join(dest, 'plugin');
|
|
197
|
+
let hasPluginScript = false;
|
|
198
|
+
|
|
199
|
+
if (existsSync(pluginScript)) {
|
|
200
|
+
// Ensure executable bit is set
|
|
201
|
+
try {
|
|
202
|
+
chmodSync(pluginScript, 0o755);
|
|
203
|
+
hasPluginScript = true;
|
|
204
|
+
} catch {
|
|
205
|
+
// chmod failed — try to use it anyway
|
|
206
|
+
hasPluginScript = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!hasPluginJs && !hasPluginScript) {
|
|
211
|
+
warn(`No plugin.js or executable plugin file found in cloned repo.`);
|
|
212
|
+
log(`${C.dim}Cleaning up ${dest}${C.reset}`);
|
|
213
|
+
try { execSync(`rm -rf ${dest}`, { stdio: 'pipe' }); } catch {}
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const pluginType = hasPluginJs ? 'JS (plugin.js)' : 'Script (plugin)';
|
|
218
|
+
|
|
219
|
+
success(`Installed ${C.bold}${name}${C.reset} → ${dest}`);
|
|
220
|
+
log(` ${C.dim}Type: ${pluginType}${C.reset}`);
|
|
221
|
+
|
|
222
|
+
// Show plugin.json info if present
|
|
223
|
+
try {
|
|
224
|
+
const manifest = JSON.parse(readFileSync(join(dest, 'plugin.json'), 'utf8'));
|
|
225
|
+
if (manifest.description) log(` ${C.dim}${manifest.description}${C.reset}`);
|
|
226
|
+
} catch {}
|
|
227
|
+
|
|
228
|
+
log(`\n${C.bold}To use it:${C.reset}`);
|
|
229
|
+
log(` Add ${C.cyan}"${name}"${C.reset} to a theme's lines array in your config:`);
|
|
230
|
+
log(` ${C.dim}${CONFIG_PATH}${C.reset}\n`);
|
|
231
|
+
}
|
|
232
|
+
|
|
145
233
|
// ─── Themes ──────────────────────────────────────
|
|
146
234
|
|
|
147
235
|
function listThemes() {
|
|
@@ -165,19 +253,19 @@ function listThemes() {
|
|
|
165
253
|
|
|
166
254
|
const marker = active ? `${C.green} ●${C.reset}` : ' ';
|
|
167
255
|
log(`${marker} ${C.bold}${name}${C.reset} — ${C.dim}${theme.description || ''}${C.reset}`);
|
|
168
|
-
log(` ${C.dim}Lines: ${theme.lines?.length || 0} |
|
|
256
|
+
log(` ${C.dim}Lines: ${theme.lines?.length || 0} | Plugins: ${[...(theme.lines || [])].flatMap(l => [...(l.left || []), ...(l.right || [])]).length}${C.reset}`);
|
|
169
257
|
} catch {}
|
|
170
258
|
}
|
|
171
259
|
log('');
|
|
172
260
|
}
|
|
173
261
|
|
|
174
|
-
// ─── List
|
|
262
|
+
// ─── List Plugins ────────────────────────────────
|
|
175
263
|
|
|
176
|
-
async function
|
|
177
|
-
log(`\n${C.bold}Built-in
|
|
178
|
-
const
|
|
179
|
-
? join(OMC_DIR, 'src', '
|
|
180
|
-
: join(PACKAGE_ROOT, 'src', '
|
|
264
|
+
async function listPlugins() {
|
|
265
|
+
log(`\n${C.bold}Built-in plugins:${C.reset}\n`);
|
|
266
|
+
const plugDir = existsSync(join(OMC_DIR, 'src', 'plugins'))
|
|
267
|
+
? join(OMC_DIR, 'src', 'plugins')
|
|
268
|
+
: join(PACKAGE_ROOT, 'src', 'plugins');
|
|
181
269
|
|
|
182
270
|
const MOCK_DATA = {
|
|
183
271
|
model: { id: 'claude-opus-4-6', display_name: 'Opus' },
|
|
@@ -187,11 +275,11 @@ async function listSegments() {
|
|
|
187
275
|
session_id: 'demo', version: '2.1.34',
|
|
188
276
|
};
|
|
189
277
|
|
|
190
|
-
const files = readdirSync(
|
|
278
|
+
const files = readdirSync(plugDir).filter(f => f.endsWith('.js') && f !== 'index.js').sort();
|
|
191
279
|
|
|
192
280
|
for (const file of files) {
|
|
193
281
|
try {
|
|
194
|
-
const mod = await import(join(
|
|
282
|
+
const mod = await import(join(plugDir, file));
|
|
195
283
|
const name = mod.meta?.name || file.replace('.js', '');
|
|
196
284
|
const desc = mod.meta?.description || '';
|
|
197
285
|
const requires = mod.meta?.requires?.length ? ` ${C.yellow}[${mod.meta.requires.join(', ')}]${C.reset}` : '';
|
|
@@ -205,7 +293,7 @@ async function listSegments() {
|
|
|
205
293
|
log(` ${C.cyan}${name}${C.reset} — ${desc}${requires}${preview}`);
|
|
206
294
|
} catch {}
|
|
207
295
|
}
|
|
208
|
-
log(`\n${C.dim}${files.length}
|
|
296
|
+
log(`\n${C.dim}${files.length} plugins available${C.reset}`);
|
|
209
297
|
|
|
210
298
|
// List plugins
|
|
211
299
|
let pluginCount = 0;
|
|
@@ -216,33 +304,70 @@ async function listSegments() {
|
|
|
216
304
|
|
|
217
305
|
for (const entry of pluginEntries) {
|
|
218
306
|
const entryPath = join(PLUGINS_DIR, entry);
|
|
219
|
-
const segmentPath = join(entryPath, 'segment.js');
|
|
220
307
|
try {
|
|
221
308
|
const { statSync: statSyncFs } = await import('node:fs');
|
|
222
309
|
const stat = statSyncFs(entryPath);
|
|
223
310
|
if (!stat.isDirectory()) continue;
|
|
224
|
-
if (!existsSync(segmentPath)) continue;
|
|
225
311
|
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
312
|
+
// Try plugin.js first
|
|
313
|
+
const pluginJsPath = join(entryPath, 'plugin.js');
|
|
314
|
+
if (existsSync(pluginJsPath)) {
|
|
315
|
+
try {
|
|
316
|
+
const { pathToFileURL } = await import('node:url');
|
|
317
|
+
const mod = await import(pathToFileURL(pluginJsPath).href);
|
|
318
|
+
if (!mod.meta || typeof mod.meta.name !== 'string' || typeof mod.render !== 'function') {
|
|
319
|
+
validPlugins.push({
|
|
320
|
+
name: entry,
|
|
321
|
+
desc: `${C.red}(invalid: missing meta.name or render)${C.reset}`,
|
|
322
|
+
path: pluginJsPath,
|
|
323
|
+
type: 'js',
|
|
324
|
+
});
|
|
325
|
+
} else {
|
|
326
|
+
validPlugins.push({
|
|
327
|
+
name: mod.meta.name,
|
|
328
|
+
desc: mod.meta.description || '',
|
|
329
|
+
path: pluginJsPath,
|
|
330
|
+
type: 'js',
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
} catch (err) {
|
|
334
|
+
validPlugins.push({
|
|
335
|
+
name: entry,
|
|
336
|
+
desc: `${C.red}(error: ${err.message})${C.reset}`,
|
|
337
|
+
path: pluginJsPath,
|
|
338
|
+
type: 'js',
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Fallback: check for executable plugin (script plugin)
|
|
345
|
+
const scriptPath = join(entryPath, 'plugin');
|
|
346
|
+
if (existsSync(scriptPath)) {
|
|
347
|
+
let isExecutable = false;
|
|
348
|
+
try { accessSync(scriptPath, constants.X_OK); isExecutable = true; } catch {}
|
|
349
|
+
|
|
350
|
+
let pluginName = entry;
|
|
351
|
+
let pluginDesc = '';
|
|
352
|
+
try {
|
|
353
|
+
const manifest = JSON.parse(readFileSync(join(entryPath, 'plugin.json'), 'utf8'));
|
|
354
|
+
if (manifest.name) pluginName = manifest.name;
|
|
355
|
+
if (manifest.description) pluginDesc = manifest.description;
|
|
356
|
+
} catch {}
|
|
357
|
+
|
|
235
358
|
validPlugins.push({
|
|
236
|
-
name:
|
|
237
|
-
desc:
|
|
238
|
-
path:
|
|
359
|
+
name: pluginName,
|
|
360
|
+
desc: pluginDesc + (isExecutable ? '' : ` ${C.red}(not executable)${C.reset}`),
|
|
361
|
+
path: scriptPath,
|
|
362
|
+
type: 'script',
|
|
239
363
|
});
|
|
240
364
|
}
|
|
241
365
|
} catch (err) {
|
|
242
366
|
validPlugins.push({
|
|
243
367
|
name: entry,
|
|
244
368
|
desc: `${C.red}(error: ${err.message})${C.reset}`,
|
|
245
|
-
path:
|
|
369
|
+
path: join(entryPath, 'plugin'),
|
|
370
|
+
type: 'unknown',
|
|
246
371
|
});
|
|
247
372
|
}
|
|
248
373
|
}
|
|
@@ -250,7 +375,8 @@ async function listSegments() {
|
|
|
250
375
|
if (validPlugins.length > 0) {
|
|
251
376
|
log(`\n${C.bold}Plugins:${C.reset}\n`);
|
|
252
377
|
for (const p of validPlugins) {
|
|
253
|
-
|
|
378
|
+
const typeLabel = p.type === 'script' ? `${C.yellow}[script]${C.reset} ` : '';
|
|
379
|
+
log(` ${C.magenta}${p.name}${C.reset} ${typeLabel}— ${p.desc}`);
|
|
254
380
|
log(` ${C.dim}${p.path}${C.reset}`);
|
|
255
381
|
}
|
|
256
382
|
pluginCount = validPlugins.length;
|
|
@@ -260,7 +386,7 @@ async function listSegments() {
|
|
|
260
386
|
}
|
|
261
387
|
|
|
262
388
|
if (pluginCount === 0) {
|
|
263
|
-
log(`\n${C.dim}No plugins installed. Run ${C.reset}${C.cyan}omc create <name>${C.reset}${C.dim} to create one.${C.reset}`);
|
|
389
|
+
log(`\n${C.dim}No plugins installed. Run ${C.reset}${C.cyan}omc create <name>${C.reset}${C.dim} to create one, or ${C.reset}${C.cyan}omc install <url>${C.reset}${C.dim} to install from git.${C.reset}`);
|
|
264
390
|
}
|
|
265
391
|
|
|
266
392
|
log('');
|
|
@@ -268,50 +394,131 @@ async function listSegments() {
|
|
|
268
394
|
|
|
269
395
|
// ─── Create Plugin ───────────────────────────────
|
|
270
396
|
|
|
271
|
-
function createPlugin(name) {
|
|
397
|
+
function createPlugin(name, args) {
|
|
272
398
|
if (!name || typeof name !== 'string') {
|
|
273
|
-
warn('Usage: omc create <
|
|
274
|
-
log(`\n${C.dim}Example: omc create my-
|
|
399
|
+
warn('Usage: omc create <plugin-name> [--script] [--lang=python|bash]');
|
|
400
|
+
log(`\n${C.dim}Example: omc create my-plugin`);
|
|
401
|
+
log(` omc create my-plugin --script --lang=python${C.reset}\n`);
|
|
275
402
|
process.exit(1);
|
|
276
403
|
}
|
|
277
404
|
|
|
278
|
-
// Validate
|
|
405
|
+
// Validate plugin name: lowercase letters, numbers, hyphens only
|
|
279
406
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
280
|
-
warn(`Invalid
|
|
407
|
+
warn(`Invalid plugin name: "${name}"`);
|
|
281
408
|
log(`\n${C.dim}Names must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.${C.reset}`);
|
|
282
|
-
log(`${C.dim}Example: my-
|
|
409
|
+
log(`${C.dim}Example: my-plugin, cpu-usage, weather-v2${C.reset}\n`);
|
|
283
410
|
process.exit(1);
|
|
284
411
|
}
|
|
285
412
|
|
|
413
|
+
// Parse flags
|
|
414
|
+
const isScript = args.includes('--script');
|
|
415
|
+
const langFlag = args.find(a => a.startsWith('--lang='));
|
|
416
|
+
const lang = langFlag ? langFlag.split('=')[1] : 'python';
|
|
417
|
+
|
|
286
418
|
const pluginDir = join(PLUGINS_DIR, name);
|
|
287
|
-
const segmentPath = join(pluginDir, 'segment.js');
|
|
288
419
|
|
|
289
420
|
// Check if plugin already exists
|
|
290
|
-
if (existsSync(
|
|
421
|
+
if (existsSync(pluginDir)) {
|
|
291
422
|
warn(`Plugin "${name}" already exists at:`);
|
|
292
|
-
log(` ${C.dim}${
|
|
423
|
+
log(` ${C.dim}${pluginDir}${C.reset}\n`);
|
|
293
424
|
process.exit(1);
|
|
294
425
|
}
|
|
295
426
|
|
|
296
|
-
// Create plugin directory and segment file
|
|
297
427
|
mkdirSync(pluginDir, { recursive: true });
|
|
298
428
|
|
|
299
|
-
|
|
300
|
-
//
|
|
429
|
+
if (isScript) {
|
|
430
|
+
// Script plugin: executable plugin + plugin.json
|
|
431
|
+
const scriptPath = join(pluginDir, 'plugin');
|
|
432
|
+
const manifestPath = join(pluginDir, 'plugin.json');
|
|
433
|
+
|
|
434
|
+
const manifest = {
|
|
435
|
+
name,
|
|
436
|
+
description: 'My custom script plugin',
|
|
437
|
+
cacheTtl: 5000,
|
|
438
|
+
defaultConfig: {},
|
|
439
|
+
};
|
|
440
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
441
|
+
|
|
442
|
+
let scriptContent;
|
|
443
|
+
if (lang === 'bash') {
|
|
444
|
+
scriptContent = `#!/usr/bin/env bash
|
|
445
|
+
# ${name} — script plugin for oh-my-claude
|
|
446
|
+
# Reads Claude Code JSON from stdin, outputs {text, style} JSON to stdout.
|
|
447
|
+
# Exit non-zero to hide the plugin.
|
|
448
|
+
|
|
449
|
+
set -euo pipefail
|
|
450
|
+
|
|
451
|
+
# Read stdin JSON
|
|
452
|
+
INPUT=$(cat)
|
|
453
|
+
|
|
454
|
+
# Extract a field (requires jq — or use other tools)
|
|
455
|
+
# MODEL=$(echo "$INPUT" | jq -r '.model.display_name // empty')
|
|
456
|
+
# CONFIG_VAL=$(echo "$INPUT" | jq -r '._config.myKey // empty')
|
|
457
|
+
|
|
458
|
+
# Output JSON
|
|
459
|
+
echo '{"text": "Hello from ${name}!", "style": "cyan"}'
|
|
460
|
+
`;
|
|
461
|
+
} else {
|
|
462
|
+
// Default: Python
|
|
463
|
+
scriptContent = `#!/usr/bin/env python3
|
|
464
|
+
"""${name} — script plugin for oh-my-claude.
|
|
465
|
+
|
|
466
|
+
Reads Claude Code JSON from stdin, outputs {text, style} JSON to stdout.
|
|
467
|
+
Exit non-zero to hide the plugin.
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
import json
|
|
471
|
+
import sys
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def main():
|
|
475
|
+
try:
|
|
476
|
+
data = json.load(sys.stdin)
|
|
477
|
+
except (json.JSONDecodeError, EOFError):
|
|
478
|
+
sys.exit(1)
|
|
479
|
+
|
|
480
|
+
# Access Claude Code data fields
|
|
481
|
+
# model_name = data.get("model", {}).get("display_name", "")
|
|
482
|
+
# cost = data.get("cost", {}).get("total_cost_usd", 0)
|
|
483
|
+
|
|
484
|
+
# Access per-plugin config (merged under _config key)
|
|
485
|
+
# config = data.get("_config", {})
|
|
486
|
+
|
|
487
|
+
# Output JSON to stdout
|
|
488
|
+
json.dump({"text": "Hello from ${name}!", "style": "cyan"}, sys.stdout)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
if __name__ == "__main__":
|
|
492
|
+
main()
|
|
493
|
+
`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
writeFileSync(scriptPath, scriptContent);
|
|
497
|
+
chmodSync(scriptPath, 0o755);
|
|
498
|
+
|
|
499
|
+
log(`\n${C.green}${C.bold}Script plugin created!${C.reset}\n`);
|
|
500
|
+
success(`Script: ${scriptPath}`);
|
|
501
|
+
success(`Manifest: ${manifestPath}`);
|
|
502
|
+
} else {
|
|
503
|
+
// JS plugin: plugin.js
|
|
504
|
+
const pluginPath = join(pluginDir, 'plugin.js');
|
|
505
|
+
|
|
506
|
+
const template = `// ${pluginPath}
|
|
507
|
+
// Custom plugin for oh-my-claude.
|
|
301
508
|
// Add "${name}" to your theme's lines array to use it.
|
|
302
509
|
|
|
303
510
|
export const meta = {
|
|
304
511
|
name: '${name}',
|
|
305
|
-
description: 'My custom
|
|
512
|
+
description: 'My custom plugin',
|
|
306
513
|
requires: [],
|
|
307
514
|
defaultConfig: {},
|
|
308
515
|
};
|
|
309
516
|
|
|
310
517
|
/**
|
|
311
|
-
* Render this
|
|
518
|
+
* Render this plugin.
|
|
312
519
|
*
|
|
313
520
|
* @param {object} data - JSON from Claude Code (use optional chaining: data?.model?.display_name)
|
|
314
|
-
* @param {object} config - Per-
|
|
521
|
+
* @param {object} config - Per-plugin config from your theme/config.json
|
|
315
522
|
* @returns {{ text: string, style: string } | null} Return { text, style } or null to hide
|
|
316
523
|
*/
|
|
317
524
|
export function render(data, config) {
|
|
@@ -325,20 +532,22 @@ export function render(data, config) {
|
|
|
325
532
|
// data?.session_id
|
|
326
533
|
// data?.version
|
|
327
534
|
//
|
|
328
|
-
// Return null to hide the
|
|
535
|
+
// Return null to hide the plugin when data is unavailable.
|
|
329
536
|
// Never throw — return null instead.
|
|
330
537
|
|
|
331
538
|
return { text: 'Hello from ${name}!', style: 'cyan' };
|
|
332
539
|
}
|
|
333
540
|
`;
|
|
334
541
|
|
|
335
|
-
|
|
542
|
+
writeFileSync(pluginPath, template);
|
|
543
|
+
|
|
544
|
+
log(`\n${C.green}${C.bold}Plugin created!${C.reset}\n`);
|
|
545
|
+
success(`File: ${pluginPath}`);
|
|
546
|
+
}
|
|
336
547
|
|
|
337
|
-
log(`\n${C.green}${C.bold}Plugin created!${C.reset}\n`);
|
|
338
|
-
success(`File: ${segmentPath}`);
|
|
339
548
|
log(`\n${C.bold}To use it:${C.reset}\n`);
|
|
340
|
-
log(` 1. Edit the
|
|
341
|
-
log(` ${C.dim}${
|
|
549
|
+
log(` 1. Edit the plugin file to customize it:`);
|
|
550
|
+
log(` ${C.dim}${pluginDir}${C.reset}\n`);
|
|
342
551
|
log(` 2. Add ${C.cyan}"${name}"${C.reset} to a theme's lines array in your config:`);
|
|
343
552
|
log(` ${C.dim}${CONFIG_PATH}${C.reset}\n`);
|
|
344
553
|
log(` Example config.json snippet:`);
|
|
@@ -347,7 +556,7 @@ export function render(data, config) {
|
|
|
347
556
|
log(` { "left": ["model-name", "${name}"], "right": ["session-cost"] }`);
|
|
348
557
|
log(` ]`);
|
|
349
558
|
log(` }${C.reset}\n`);
|
|
350
|
-
log(` 3. Restart Claude Code to see your
|
|
559
|
+
log(` 3. Restart Claude Code to see your plugin.\n`);
|
|
351
560
|
}
|
|
352
561
|
|
|
353
562
|
// ─── Set Theme ──────────────────────────────────
|
|
@@ -462,9 +671,16 @@ function uninstall() {
|
|
|
462
671
|
const command = process.argv[2] || 'install';
|
|
463
672
|
|
|
464
673
|
switch (command) {
|
|
465
|
-
case 'install':
|
|
466
|
-
|
|
674
|
+
case 'install': {
|
|
675
|
+
// Disambiguate: if arg looks like a URL or path, install a plugin
|
|
676
|
+
const installArg = process.argv[3];
|
|
677
|
+
if (installArg && (installArg.includes('/') || installArg.includes(':') || installArg.startsWith('.'))) {
|
|
678
|
+
installPlugin(installArg);
|
|
679
|
+
} else {
|
|
680
|
+
install().catch(err => { console.error(err); process.exit(1); });
|
|
681
|
+
}
|
|
467
682
|
break;
|
|
683
|
+
}
|
|
468
684
|
case 'themes':
|
|
469
685
|
listThemes();
|
|
470
686
|
break;
|
|
@@ -476,11 +692,11 @@ switch (command) {
|
|
|
476
692
|
}
|
|
477
693
|
break;
|
|
478
694
|
case 'list':
|
|
479
|
-
case '
|
|
480
|
-
|
|
695
|
+
case 'plugins':
|
|
696
|
+
listPlugins().catch(err => { console.error(err); process.exit(1); });
|
|
481
697
|
break;
|
|
482
698
|
case 'create':
|
|
483
|
-
createPlugin(process.argv[3]);
|
|
699
|
+
createPlugin(process.argv[3], process.argv.slice(4));
|
|
484
700
|
break;
|
|
485
701
|
case 'uninstall':
|
|
486
702
|
case 'remove':
|
|
@@ -495,13 +711,17 @@ switch (command) {
|
|
|
495
711
|
log(`\n${C.bold}oh-my-claude${C.reset} — The framework for Claude Code statuslines\n`);
|
|
496
712
|
log(`${C.bold}Usage:${C.reset} omc <command>\n`);
|
|
497
713
|
log(`${C.bold}Commands:${C.reset}`);
|
|
498
|
-
log(` ${C.cyan}install${C.reset}
|
|
499
|
-
log(` ${C.cyan}
|
|
500
|
-
log(` ${C.cyan}
|
|
501
|
-
log(` ${C.cyan}
|
|
502
|
-
log(` ${C.cyan}
|
|
503
|
-
log(` ${C.cyan}
|
|
504
|
-
log(`
|
|
714
|
+
log(` ${C.cyan}install${C.reset} Install oh-my-claude (interactive wizard)`);
|
|
715
|
+
log(` ${C.cyan}install <url>${C.reset} Install a plugin from a git URL or local path`);
|
|
716
|
+
log(` ${C.cyan}theme${C.reset} Set theme (omc theme <name>) or list themes`);
|
|
717
|
+
log(` ${C.cyan}list${C.reset} List all available plugins`);
|
|
718
|
+
log(` ${C.cyan}create <name>${C.reset} Create a new JS plugin`);
|
|
719
|
+
log(` ${C.cyan}create <name> --script${C.reset}`);
|
|
720
|
+
log(` Create a script plugin (any language)`);
|
|
721
|
+
log(` ${C.dim}--lang=python|bash (default: python)${C.reset}`);
|
|
722
|
+
log(` ${C.cyan}validate${C.reset} Run the plugin contract validator`);
|
|
723
|
+
log(` ${C.cyan}uninstall${C.reset} Remove oh-my-claude from Claude Code`);
|
|
724
|
+
log(` ${C.cyan}help${C.reset} Show this help message\n`);
|
|
505
725
|
break;
|
|
506
726
|
default:
|
|
507
727
|
warn(`Unknown command: ${command}`);
|