@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.
Files changed (63) hide show
  1. package/README.md +36 -21
  2. package/bin/omc.js +284 -64
  3. package/docs/architecture.md +125 -43
  4. package/docs/{segment-contract.md → plugin-contract.md} +21 -21
  5. package/docs/theme-format.md +20 -20
  6. package/package.json +2 -2
  7. package/src/cache.js +78 -1
  8. package/src/color.js +64 -0
  9. package/src/compositor.js +51 -20
  10. package/src/config.js +6 -6
  11. package/src/{segments → plugins}/achievement.js +2 -2
  12. package/src/{segments → plugins}/api-timer.js +2 -2
  13. package/src/{segments → plugins}/battle-log.js +2 -2
  14. package/src/{segments → plugins}/cat.js +2 -2
  15. package/src/{segments → plugins}/coffee-cup.js +2 -2
  16. package/src/{segments → plugins}/commit-msg.js +2 -2
  17. package/src/{segments → plugins}/context-bar.js +2 -2
  18. package/src/{segments → plugins}/context-percent.js +2 -2
  19. package/src/{segments → plugins}/context-tokens.js +2 -2
  20. package/src/{segments → plugins}/cost-budget.js +2 -2
  21. package/src/{segments → plugins}/coworker.js +2 -2
  22. package/src/{segments → plugins}/custom-text.js +2 -2
  23. package/src/{segments → plugins}/directory.js +2 -2
  24. package/src/{segments → plugins}/emoji-story.js +2 -2
  25. package/src/{segments → plugins}/flex-space.js +4 -4
  26. package/src/{segments → plugins}/fortune-cookie.js +2 -2
  27. package/src/{segments → plugins}/garden.js +2 -2
  28. package/src/{segments → plugins}/git-branch.js +2 -2
  29. package/src/{segments → plugins}/git-status.js +2 -2
  30. package/src/{segments → plugins}/horoscope.js +2 -2
  31. package/src/{segments → plugins}/index.js +7 -7
  32. package/src/{segments → plugins}/lines-changed.js +2 -2
  33. package/src/{segments → plugins}/model-name.js +2 -2
  34. package/src/{segments → plugins}/narrator.js +2 -2
  35. package/src/{segments → plugins}/output-style.js +2 -2
  36. package/src/{segments → plugins}/rpg-stats.js +2 -2
  37. package/src/{segments → plugins}/separator-arrow.js +2 -2
  38. package/src/{segments → plugins}/separator-pipe.js +2 -2
  39. package/src/{segments → plugins}/separator-space.js +3 -3
  40. package/src/{segments → plugins}/session-cost.js +2 -2
  41. package/src/{segments → plugins}/session-timer.js +2 -2
  42. package/src/{segments → plugins}/smart-nudge.js +2 -2
  43. package/src/{segments → plugins}/soundtrack.js +3 -3
  44. package/src/{segments → plugins}/speedrun.js +2 -2
  45. package/src/{segments → plugins}/stock-ticker.js +2 -2
  46. package/src/{segments → plugins}/streak.js +2 -2
  47. package/src/{segments → plugins}/tamagotchi.js +2 -2
  48. package/src/{segments → plugins}/token-sparkline.js +2 -2
  49. package/src/{segments → plugins}/version.js +2 -2
  50. package/src/{segments → plugins}/vibe-check.js +3 -3
  51. package/src/{segments → plugins}/vim-mode.js +2 -2
  52. package/src/{segments → plugins}/weather-report.js +2 -2
  53. package/src/plugins.js +76 -13
  54. package/src/runner.js +22 -22
  55. package/themes/boss-battle.json +1 -1
  56. package/themes/coworker.json +1 -1
  57. package/themes/danger-zone.json +1 -1
  58. package/themes/default.json +1 -1
  59. package/themes/minimal.json +1 -1
  60. package/themes/narrator.json +1 -1
  61. package/themes/powerline.json +1 -1
  62. package/themes/rpg.json +1 -1
  63. package/themes/tamagotchi.json +1 -1
package/README.md CHANGED
@@ -1,12 +1,27 @@
1
1
  # oh-my-claude
2
2
 
3
- **Like oh-my-zsh, but for Claude Code.** An extensible statusline framework with themes and plugins.
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
+ ![oh-my-claude themes](screenshots/hero.gif)
4
8
 
5
9
  [![npm version](https://img.shields.io/npm/v/@npow/oh-my-claude)](https://www.npmjs.com/package/@npow/oh-my-claude)
6
10
  [![CI](https://img.shields.io/github/actions/workflow/status/npow/oh-my-claude/ci.yml?label=CI)](https://github.com/npow/oh-my-claude/actions/workflows/ci.yml)
7
11
  [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
8
12
  [![node](https://img.shields.io/node/v/@npow/oh-my-claude)](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
- Restart Claude Code and you're done.
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 segments, mix and match
112
+ ## 41 plugins, mix and match
98
113
 
99
114
  ### Have fun while you wait
100
115
 
101
- | Segment | What it does | Example |
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
- | Segment | What it does | Example |
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
- | Segment | What it shows | Example |
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
- | Segment | Purpose |
166
+ | Plugin | Purpose |
152
167
  |---------|---------|
153
- | `separator-pipe` | Pipe `│` between segments |
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
- ## Add your own segment
225
+ ## Write your own
211
226
 
212
- No fork needed. Plugins live in their own directory:
227
+ No fork needed:
213
228
 
214
229
  ```bash
215
- omc create my-segment
230
+ omc create my-plugin
216
231
  ```
217
232
 
218
- Creates `~/.claude/oh-my-claude/plugins/my-segment/segment.js`:
233
+ Creates `~/.claude/oh-my-claude/plugins/my-plugin/plugin.js`:
219
234
 
220
235
  ```js
221
236
  export const meta = {
222
- name: 'my-segment',
223
- description: 'My custom segment',
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, restart Claude Code, done. Three rules: export `meta`, export `render`, return `{ text, style }` or `null`.
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/segment-contract.md](docs/segment-contract.md)
250
+ Full data field reference: [docs/plugin-contract.md](docs/plugin-contract.md)
236
251
 
237
- **Share your segment:** PR it into `src/segments/` or post your `segment.js` anywhere -- others drop it in their plugins directory.
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 segment
248
- omc list List all 41 segments
249
- omc validate Check segment contract compliance
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 segment, export `meta` + `render`, handle nulls, run `npm run validate`.
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
- segments: {
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} | Segments: ${[...(theme.lines || [])].flatMap(l => [...(l.left || []), ...(l.right || [])]).length}${C.reset}`);
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 Segments ───────────────────────────────
262
+ // ─── List Plugins ────────────────────────────────
175
263
 
176
- async function listSegments() {
177
- log(`\n${C.bold}Built-in segments:${C.reset}\n`);
178
- const segDir = existsSync(join(OMC_DIR, 'src', 'segments'))
179
- ? join(OMC_DIR, 'src', 'segments')
180
- : join(PACKAGE_ROOT, 'src', 'segments');
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(segDir).filter(f => f.endsWith('.js') && f !== 'index.js').sort();
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(segDir, file));
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} segments available${C.reset}`);
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
- const { pathToFileURL } = await import('node:url');
227
- const mod = await import(pathToFileURL(segmentPath).href);
228
- if (!mod.meta || typeof mod.meta.name !== 'string' || typeof mod.render !== 'function') {
229
- validPlugins.push({
230
- name: entry,
231
- desc: `${C.red}(invalid: missing meta.name or render)${C.reset}`,
232
- path: segmentPath,
233
- });
234
- } else {
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: mod.meta.name,
237
- desc: mod.meta.description || '',
238
- path: segmentPath,
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: segmentPath,
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
- log(` ${C.magenta}${p.name}${C.reset} ${p.desc}`);
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 <segment-name>');
274
- log(`\n${C.dim}Example: omc create my-segment${C.reset}\n`);
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 segment name: lowercase letters, numbers, hyphens only
405
+ // Validate plugin name: lowercase letters, numbers, hyphens only
279
406
  if (!/^[a-z][a-z0-9-]*$/.test(name)) {
280
- warn(`Invalid segment name: "${name}"`);
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-segment, cpu-usage, weather-v2${C.reset}\n`);
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(segmentPath)) {
421
+ if (existsSync(pluginDir)) {
291
422
  warn(`Plugin "${name}" already exists at:`);
292
- log(` ${C.dim}${segmentPath}${C.reset}\n`);
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
- const template = `// ${segmentPath}
300
- // Custom segment for oh-my-claude.
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 segment',
512
+ description: 'My custom plugin',
306
513
  requires: [],
307
514
  defaultConfig: {},
308
515
  };
309
516
 
310
517
  /**
311
- * Render this segment.
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-segment config from your theme/config.json
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 segment when data is unavailable.
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
- writeFileSync(segmentPath, template);
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 segment file to customize it:`);
341
- log(` ${C.dim}${segmentPath}${C.reset}\n`);
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 segment.\n`);
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
- install().catch(err => { console.error(err); process.exit(1); });
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 'segments':
480
- listSegments().catch(err => { console.error(err); process.exit(1); });
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} Install oh-my-claude (interactive wizard)`);
499
- log(` ${C.cyan}theme${C.reset} Set theme (omc theme <name>) or list themes`);
500
- log(` ${C.cyan}list${C.reset} List all available segments`);
501
- log(` ${C.cyan}create${C.reset} Create a new plugin segment (omc create <name>)`);
502
- log(` ${C.cyan}validate${C.reset} Run the segment contract validator`);
503
- log(` ${C.cyan}uninstall${C.reset} Remove oh-my-claude from Claude Code`);
504
- log(` ${C.cyan}help${C.reset} Show this help message\n`);
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}`);