@oh-my-pi/cli 0.3.0 → 0.4.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.
Files changed (48) hide show
  1. package/.editorconfig +14 -0
  2. package/.prettierrc +6 -0
  3. package/README.md +65 -71
  4. package/bun.lock +13 -10
  5. package/dist/cli.js +114 -34
  6. package/dist/commands/config.d.ts.map +1 -1
  7. package/dist/commands/features.d.ts.map +1 -1
  8. package/dist/commands/install.d.ts.map +1 -1
  9. package/dist/commands/list.d.ts.map +1 -1
  10. package/dist/commands/uninstall.d.ts.map +1 -1
  11. package/dist/commands/update.d.ts.map +1 -1
  12. package/dist/runtime.d.ts.map +1 -1
  13. package/dist/symlinks.d.ts +1 -0
  14. package/dist/symlinks.d.ts.map +1 -1
  15. package/package.json +11 -4
  16. package/plugins/exa/README.md +44 -38
  17. package/plugins/exa/package.json +44 -11
  18. package/plugins/exa/tools/exa/company.ts +24 -13
  19. package/plugins/exa/tools/exa/index.ts +43 -34
  20. package/plugins/exa/tools/exa/linkedin.ts +24 -13
  21. package/plugins/exa/tools/exa/researcher.ts +26 -18
  22. package/plugins/exa/tools/exa/search.ts +31 -20
  23. package/plugins/exa/tools/exa/shared.ts +182 -156
  24. package/plugins/exa/tools/exa/websets.ts +39 -28
  25. package/plugins/metal-theme/package.json +13 -4
  26. package/plugins/subagents/README.md +3 -0
  27. package/plugins/subagents/agents/explore.md +11 -0
  28. package/plugins/subagents/agents/planner.md +3 -0
  29. package/plugins/subagents/agents/reviewer.md +6 -0
  30. package/plugins/subagents/agents/task.md +8 -1
  31. package/plugins/subagents/commands/architect-plan.md +1 -0
  32. package/plugins/subagents/commands/implement-with-critic.md +1 -0
  33. package/plugins/subagents/commands/implement.md +1 -0
  34. package/plugins/subagents/package.json +43 -11
  35. package/plugins/subagents/tools/task/index.ts +1089 -861
  36. package/plugins/user-prompt/README.md +22 -66
  37. package/plugins/user-prompt/package.json +15 -4
  38. package/plugins/user-prompt/tools/user-prompt/index.ts +185 -157
  39. package/scripts/bump-version.sh +10 -13
  40. package/src/cli.ts +1 -1
  41. package/src/commands/config.ts +21 -6
  42. package/src/commands/features.ts +49 -12
  43. package/src/commands/install.ts +91 -24
  44. package/src/commands/list.ts +14 -3
  45. package/src/commands/uninstall.ts +4 -1
  46. package/src/commands/update.ts +6 -1
  47. package/src/runtime.ts +3 -10
  48. package/src/symlinks.ts +12 -7
@@ -41,15 +41,25 @@ export async function interactiveFeatures(name: string, options: FeaturesOptions
41
41
  return;
42
42
  }
43
43
 
44
- // Get runtime config path and current enabled features
44
+ // Get runtime config path
45
45
  const runtimePath = getRuntimeConfigPath(pkgJson, isGlobal);
46
46
  if (!runtimePath) {
47
47
  console.log(chalk.yellow(`Plugin "${name}" does not have a runtime.json config file.`));
48
48
  return;
49
49
  }
50
50
 
51
- const runtimeConfig = readRuntimeConfig(runtimePath);
52
- const enabledFeatures = runtimeConfig.features ?? getDefaultFeatures(features);
51
+ // Determine currently enabled features:
52
+ // 1. Check plugins.json config (source of truth after omp features changes)
53
+ // 2. Fall back to runtime.json
54
+ // 3. Fall back to plugin defaults
55
+ const pluginConfig = pluginsJson.config?.[name];
56
+ let enabledFeatures: string[];
57
+ if (Array.isArray(pluginConfig?.features)) {
58
+ enabledFeatures = pluginConfig.features;
59
+ } else {
60
+ const runtimeConfig = readRuntimeConfig(runtimePath);
61
+ enabledFeatures = runtimeConfig.features ?? getDefaultFeatures(features);
62
+ }
53
63
 
54
64
  // JSON output mode - just list
55
65
  if (options.json) {
@@ -99,8 +109,8 @@ export async function interactiveFeatures(name: string, options: FeaturesOptions
99
109
  });
100
110
 
101
111
  // Apply changes
102
- await applyFeatureChanges(name, runtimePath, features, enabledFeatures, selected);
103
- } catch (err) {
112
+ await applyFeatureChanges(name, runtimePath, features, enabledFeatures, selected, isGlobal);
113
+ } catch (_err) {
104
114
  // User cancelled (Ctrl+C)
105
115
  console.log(chalk.dim("\nCancelled."));
106
116
  }
@@ -111,7 +121,14 @@ export async function interactiveFeatures(name: string, options: FeaturesOptions
111
121
  */
112
122
  function listFeaturesNonInteractive(
113
123
  name: string,
114
- features: Record<string, { description?: string; default?: boolean; variables?: Record<string, unknown> }>,
124
+ features: Record<
125
+ string,
126
+ {
127
+ description?: string;
128
+ default?: boolean;
129
+ variables?: Record<string, unknown>;
130
+ }
131
+ >,
115
132
  enabledFeatures: string[],
116
133
  ): void {
117
134
  console.log(chalk.bold(`\nFeatures for ${name}:\n`));
@@ -141,7 +158,14 @@ function listFeaturesNonInteractive(
141
158
  async function applyFeatureChanges(
142
159
  name: string,
143
160
  runtimePath: string,
144
- features: Record<string, { description?: string; default?: boolean; variables?: Record<string, unknown> }>,
161
+ _features: Record<
162
+ string,
163
+ {
164
+ description?: string;
165
+ default?: boolean;
166
+ variables?: Record<string, unknown>;
167
+ }
168
+ >,
145
169
  currentlyEnabled: string[],
146
170
  newEnabled: string[],
147
171
  isGlobal: boolean,
@@ -214,7 +238,7 @@ export async function configureFeatures(name: string, options: FeaturesOptions =
214
238
 
215
239
  const allFeatureNames = Object.keys(features);
216
240
 
217
- // Get runtime config
241
+ // Get runtime config path
218
242
  const runtimePath = getRuntimeConfigPath(pkgJson, isGlobal);
219
243
  if (!runtimePath) {
220
244
  console.log(chalk.yellow(`Plugin "${name}" does not have a runtime.json config file.`));
@@ -222,8 +246,18 @@ export async function configureFeatures(name: string, options: FeaturesOptions =
222
246
  return;
223
247
  }
224
248
 
225
- const runtimeConfig = readRuntimeConfig(runtimePath);
226
- const currentlyEnabled = runtimeConfig.features ?? getDefaultFeatures(features);
249
+ // Determine currently enabled features:
250
+ // 1. Check plugins.json config (source of truth after omp features changes)
251
+ // 2. Fall back to runtime.json
252
+ // 3. Fall back to plugin defaults
253
+ const pluginConfig = pluginsJson.config?.[name];
254
+ let currentlyEnabled: string[];
255
+ if (Array.isArray(pluginConfig?.features)) {
256
+ currentlyEnabled = pluginConfig.features;
257
+ } else {
258
+ const runtimeConfig = readRuntimeConfig(runtimePath);
259
+ currentlyEnabled = runtimeConfig.features ?? getDefaultFeatures(features);
260
+ }
227
261
 
228
262
  let newEnabled: string[];
229
263
 
@@ -234,7 +268,10 @@ export async function configureFeatures(name: string, options: FeaturesOptions =
234
268
  } else if (options.set === "") {
235
269
  newEnabled = [];
236
270
  } else {
237
- newEnabled = options.set.split(",").map((f) => f.trim()).filter(Boolean);
271
+ newEnabled = options.set
272
+ .split(",")
273
+ .map((f) => f.trim())
274
+ .filter(Boolean);
238
275
  // Validate
239
276
  for (const f of newEnabled) {
240
277
  if (!features[f]) {
@@ -273,7 +310,7 @@ export async function configureFeatures(name: string, options: FeaturesOptions =
273
310
  }
274
311
  }
275
312
 
276
- await applyFeatureChanges(name, runtimePath, features, currentlyEnabled, newEnabled);
313
+ await applyFeatureChanges(name, runtimePath, features, currentlyEnabled, newEnabled, isGlobal);
277
314
 
278
315
  if (options.json) {
279
316
  console.log(JSON.stringify({ plugin: name, enabled: newEnabled }, null, 2));
@@ -12,7 +12,6 @@ import {
12
12
  loadPluginsJson,
13
13
  type PluginConfig,
14
14
  type PluginPackageJson,
15
- type PluginsJson,
16
15
  readPluginPackageJson,
17
16
  savePluginsJson,
18
17
  } from "@omp/manifest";
@@ -59,7 +58,12 @@ export function parsePackageSpecWithFeatures(spec: string): ParsedPackageSpec {
59
58
 
60
59
  if (!match) {
61
60
  // Fallback: treat as plain name
62
- return { name: spec, version: "latest", features: null, allFeatures: false };
61
+ return {
62
+ name: spec,
63
+ version: "latest",
64
+ features: null,
65
+ allFeatures: false,
66
+ };
63
67
  }
64
68
 
65
69
  const [, name, featuresStr, version = "latest"] = match;
@@ -80,7 +84,10 @@ export function parsePackageSpecWithFeatures(spec: string): ParsedPackageSpec {
80
84
  }
81
85
 
82
86
  // [f1,f2,...] = specific features
83
- const features = featuresStr.split(",").map((f) => f.trim()).filter(Boolean);
87
+ const features = featuresStr
88
+ .split(",")
89
+ .map((f) => f.trim())
90
+ .filter(Boolean);
84
91
  return { name, version, features, allFeatures: false };
85
92
  }
86
93
 
@@ -287,7 +294,12 @@ export async function installPlugin(packages?: string[], options: InstallOptions
287
294
  // Get existing plugins for conflict detection
288
295
  const existingPlugins = await getInstalledPlugins(isGlobal);
289
296
 
290
- const results: Array<{ name: string; version: string; success: boolean; error?: string }> = [];
297
+ const results: Array<{
298
+ name: string;
299
+ version: string;
300
+ success: boolean;
301
+ error?: string;
302
+ }> = [];
291
303
 
292
304
  // Load plugins.json once for reinstall detection and config storage
293
305
  let pluginsJson = await loadPluginsJson(isGlobal);
@@ -322,7 +334,12 @@ export async function installPlugin(packages?: string[], options: InstallOptions
322
334
  if (!info) {
323
335
  console.log(chalk.red(` ✗ Package not found: ${name}`));
324
336
  process.exitCode = 1;
325
- results.push({ name, version, success: false, error: "Package not found" });
337
+ results.push({
338
+ name,
339
+ version,
340
+ success: false,
341
+ error: "Package not found",
342
+ });
326
343
  continue;
327
344
  }
328
345
  resolvedVersion = info.version;
@@ -340,7 +357,12 @@ export async function installPlugin(packages?: string[], options: InstallOptions
340
357
  console.log(chalk.red(` ${dupe.dest} ← ${dupe.sources.join(", ")}`));
341
358
  }
342
359
  process.exitCode = 1;
343
- results.push({ name, version: info.version, success: false, error: "Duplicate destinations in plugin" });
360
+ results.push({
361
+ name,
362
+ version: info.version,
363
+ success: false,
364
+ error: "Duplicate destinations in plugin",
365
+ });
344
366
  continue;
345
367
  }
346
368
 
@@ -382,7 +404,12 @@ export async function installPlugin(packages?: string[], options: InstallOptions
382
404
  if (abort) {
383
405
  console.log(chalk.yellow(` Aborted due to conflicts (before download)`));
384
406
  process.exitCode = 1;
385
- results.push({ name, version: info.version, success: false, error: "Conflicts" });
407
+ results.push({
408
+ name,
409
+ version: info.version,
410
+ success: false,
411
+ error: "Conflicts",
412
+ });
386
413
  continue;
387
414
  }
388
415
  }
@@ -412,9 +439,16 @@ export async function installPlugin(packages?: string[], options: InstallOptions
412
439
  console.log(chalk.red(` ${dupe.dest} ← ${dupe.sources.join(", ")}`));
413
440
  }
414
441
  // Rollback: uninstall the package
415
- execFileSync("npm", ["uninstall", "--prefix", prefix, name], { stdio: "pipe" });
442
+ execFileSync("npm", ["uninstall", "--prefix", prefix, name], {
443
+ stdio: "pipe",
444
+ });
416
445
  process.exitCode = 1;
417
- results.push({ name, version: info.version, success: false, error: "Duplicate destinations in plugin" });
446
+ results.push({
447
+ name,
448
+ version: info.version,
449
+ success: false,
450
+ error: "Duplicate destinations in plugin",
451
+ });
418
452
  continue;
419
453
  }
420
454
 
@@ -428,7 +462,9 @@ export async function installPlugin(packages?: string[], options: InstallOptions
428
462
  console.log(chalk.yellow(` ⚠ ${formatConflicts([conflict])[0]}`));
429
463
  }
430
464
  // Rollback: uninstall the package
431
- execFileSync("npm", ["uninstall", "--prefix", prefix, name], { stdio: "pipe" });
465
+ execFileSync("npm", ["uninstall", "--prefix", prefix, name], {
466
+ stdio: "pipe",
467
+ });
432
468
  process.exitCode = 1;
433
469
  results.push({
434
470
  name,
@@ -455,21 +491,23 @@ export async function installPlugin(packages?: string[], options: InstallOptions
455
491
  if (abort) {
456
492
  console.log(chalk.yellow(` Aborted due to conflicts`));
457
493
  // Rollback: uninstall the package
458
- execFileSync("npm", ["uninstall", "--prefix", prefix, name], { stdio: "pipe" });
494
+ execFileSync("npm", ["uninstall", "--prefix", prefix, name], {
495
+ stdio: "pipe",
496
+ });
459
497
  process.exitCode = 1;
460
- results.push({ name, version: info.version, success: false, error: "Conflicts" });
498
+ results.push({
499
+ name,
500
+ version: info.version,
501
+ success: false,
502
+ error: "Conflicts",
503
+ });
461
504
  continue;
462
505
  }
463
506
  }
464
507
  }
465
508
 
466
509
  // 6. Resolve features and create symlinks
467
- const { enabledFeatures, configToStore } = resolveFeatures(
468
- pkgJson,
469
- parsed,
470
- existingConfig,
471
- isReinstall,
472
- );
510
+ const { enabledFeatures, configToStore } = resolveFeatures(pkgJson, parsed, existingConfig, isReinstall);
473
511
 
474
512
  // Log feature selection if plugin has features
475
513
  const allFeatureNames = getAllFeatureNames(pkgJson);
@@ -484,7 +522,14 @@ export async function installPlugin(packages?: string[], options: InstallOptions
484
522
  }
485
523
 
486
524
  // Create symlinks for omp.install entries (skip destinations user assigned to existing plugins)
487
- const symlinkResult = await createPluginSymlinks(name, pkgJson, isGlobal, true, skipDestinations, enabledFeatures);
525
+ const symlinkResult = await createPluginSymlinks(
526
+ name,
527
+ pkgJson,
528
+ isGlobal,
529
+ true,
530
+ skipDestinations,
531
+ enabledFeatures,
532
+ );
488
533
  createdSymlinks = symlinkResult.created;
489
534
 
490
535
  // 7. Process dependencies with omp field (with cycle detection)
@@ -553,14 +598,21 @@ export async function installPlugin(packages?: string[], options: InstallOptions
553
598
  if (npmInstallSucceeded) {
554
599
  console.log(chalk.dim(" Rolling back npm install..."));
555
600
  try {
556
- execFileSync("npm", ["uninstall", "--prefix", prefix, name], { stdio: "pipe" });
601
+ execFileSync("npm", ["uninstall", "--prefix", prefix, name], {
602
+ stdio: "pipe",
603
+ });
557
604
  } catch {
558
605
  // Ignore cleanup errors
559
606
  }
560
607
  }
561
608
 
562
609
  process.exitCode = 1;
563
- results.push({ name, version: resolvedVersion, success: false, error: errorMsg });
610
+ results.push({
611
+ name,
612
+ version: resolvedVersion,
613
+ success: false,
614
+ error: errorMsg,
615
+ });
564
616
  }
565
617
  }
566
618
 
@@ -589,7 +641,12 @@ async function installLocalPlugin(
589
641
  localPath: string,
590
642
  isGlobal: boolean,
591
643
  _options: InstallOptions,
592
- ): Promise<{ name: string; version: string; success: boolean; error?: string }> {
644
+ ): Promise<{
645
+ name: string;
646
+ version: string;
647
+ success: boolean;
648
+ error?: string;
649
+ }> {
593
650
  // Expand ~ to home directory
594
651
  if (localPath.startsWith("~")) {
595
652
  localPath = join(process.env.HOME || "", localPath.slice(1));
@@ -599,7 +656,12 @@ async function installLocalPlugin(
599
656
  if (!existsSync(localPath)) {
600
657
  console.log(chalk.red(`Error: Path does not exist: ${localPath}`));
601
658
  process.exitCode = 1;
602
- return { name: basename(localPath), version: "local", success: false, error: "Path not found" };
659
+ return {
660
+ name: basename(localPath),
661
+ version: "local",
662
+ success: false,
663
+ error: "Path not found",
664
+ };
603
665
  }
604
666
 
605
667
  const _prefix = isGlobal ? PLUGINS_DIR : ".pi";
@@ -695,6 +757,11 @@ async function installLocalPlugin(
695
757
  const errorMsg = (err as Error).message;
696
758
  console.log(chalk.red(` ✗ Failed: ${errorMsg}`));
697
759
  process.exitCode = 1;
698
- return { name: basename(localPath), version: "local", success: false, error: errorMsg };
760
+ return {
761
+ name: basename(localPath),
762
+ version: "local",
763
+ success: false,
764
+ error: errorMsg,
765
+ };
699
766
  }
700
767
  }
@@ -47,20 +47,31 @@ const FILE_CATEGORIES: FileCategory[] = [
47
47
  pattern: /^agent\/prompts?\//,
48
48
  label: "Prompts",
49
49
  color: chalk.blue,
50
- extractName: (dest) => dest.split("/").pop()?.replace(/\.[^.]+$/, "") || dest,
50
+ extractName: (dest) =>
51
+ dest
52
+ .split("/")
53
+ .pop()
54
+ ?.replace(/\.[^.]+$/, "") || dest,
51
55
  },
52
56
  {
53
57
  pattern: /^agent\/hooks?\//,
54
58
  label: "Hooks",
55
59
  color: chalk.red,
56
- extractName: (dest) => dest.split("/").pop()?.replace(/\.[^.]+$/, "") || dest,
60
+ extractName: (dest) =>
61
+ dest
62
+ .split("/")
63
+ .pop()
64
+ ?.replace(/\.[^.]+$/, "") || dest,
57
65
  },
58
66
  ];
59
67
 
60
68
  /**
61
69
  * Categorize installed files into known categories
62
70
  */
63
- function categorizeFiles(files: string[]): { categorized: Map<string, string[]>; uncategorized: string[] } {
71
+ function categorizeFiles(files: string[]): {
72
+ categorized: Map<string, string[]>;
73
+ uncategorized: string[];
74
+ } {
64
75
  const categorized = new Map<string, string[]>();
65
76
  const uncategorized: string[] = [];
66
77
 
@@ -70,7 +70,10 @@ export async function uninstallPlugin(name: string, options: UninstallOptions =
70
70
  }
71
71
 
72
72
  if (process.stdin.isTTY && process.stdout.isTTY) {
73
- const rl = createInterface({ input: process.stdin, output: process.stdout });
73
+ const rl = createInterface({
74
+ input: process.stdin,
75
+ output: process.stdout,
76
+ });
74
77
  const answer = await new Promise<string>((resolve) => {
75
78
  rl.question(chalk.yellow("Delete these files anyway? [y/N] "), (ans) => {
76
79
  rl.close();
@@ -68,7 +68,12 @@ export async function updatePlugin(name?: string, options: UpdateOptions = {}):
68
68
 
69
69
  console.log(chalk.blue(`Updating ${npmPlugins.length} plugin(s)...`));
70
70
 
71
- const results: Array<{ name: string; from: string; to: string; success: boolean }> = [];
71
+ const results: Array<{
72
+ name: string;
73
+ from: string;
74
+ to: string;
75
+ success: boolean;
76
+ }> = [];
72
77
 
73
78
  // Save old package info before removing symlinks (for recovery on failure)
74
79
  const oldPkgJsons = new Map<string, PluginPackageJson>();
package/src/runtime.ts CHANGED
@@ -1,13 +1,10 @@
1
- import type { OmpVariable, PluginPackageJson, PluginsJson } from "@omp/manifest";
1
+ import type { OmpVariable, PluginPackageJson } from "@omp/manifest";
2
2
  import { loadPluginsJson, readPluginPackageJson } from "@omp/manifest";
3
3
 
4
4
  /**
5
5
  * Collect all variables from a plugin (top-level + enabled features)
6
6
  */
7
- function collectVariables(
8
- pkgJson: PluginPackageJson,
9
- enabledFeatures: string[],
10
- ): Record<string, OmpVariable> {
7
+ function collectVariables(pkgJson: PluginPackageJson, enabledFeatures: string[]): Record<string, OmpVariable> {
11
8
  const vars: Record<string, OmpVariable> = {};
12
9
 
13
10
  // Top-level variables
@@ -67,11 +64,7 @@ export async function getPluginEnvVars(global = true): Promise<Record<string, st
67
64
 
68
65
  const config = pluginsJson.config?.[pluginName];
69
66
  const allFeatureNames = Object.keys(pkgJson.omp.features || {});
70
- const enabledFeatures = resolveEnabledFeatures(
71
- allFeatureNames,
72
- config?.features,
73
- pkgJson.omp.features || {},
74
- );
67
+ const enabledFeatures = resolveEnabledFeatures(allFeatureNames, config?.features, pkgJson.omp.features || {});
75
68
 
76
69
  // Collect variables from top-level and enabled features
77
70
  const variables = collectVariables(pkgJson, enabledFeatures);
package/src/symlinks.ts CHANGED
@@ -18,10 +18,7 @@ export function getInstallEntries(pkgJson: PluginPackageJson): OmpInstallEntry[]
18
18
  /**
19
19
  * @deprecated Use getInstallEntries instead. Features no longer have install arrays.
20
20
  */
21
- export function getEnabledInstallEntries(
22
- pkgJson: PluginPackageJson,
23
- _enabledFeatures?: string[],
24
- ): OmpInstallEntry[] {
21
+ export function getEnabledInstallEntries(pkgJson: PluginPackageJson, _enabledFeatures?: string[]): OmpInstallEntry[] {
25
22
  return getInstallEntries(pkgJson);
26
23
  }
27
24
 
@@ -245,7 +242,7 @@ export async function writeRuntimeConfig(
245
242
  features: config.features ?? existing.features ?? [],
246
243
  options: { ...existing.options, ...config.options },
247
244
  };
248
- writeFileSync(runtimePath, JSON.stringify(merged, null, 2) + "\n");
245
+ writeFileSync(runtimePath, `${JSON.stringify(merged, null, 2)}\n`);
249
246
  if (verbose) {
250
247
  console.log(chalk.dim(` Updated: ${runtimePath}`));
251
248
  }
@@ -275,7 +272,11 @@ export async function removePluginSymlinks(
275
272
  global = true,
276
273
  verbose = true,
277
274
  ): Promise<SymlinkRemovalResult> {
278
- const result: SymlinkRemovalResult = { removed: [], errors: [], skippedNonSymlinks: [] };
275
+ const result: SymlinkRemovalResult = {
276
+ removed: [],
277
+ errors: [],
278
+ skippedNonSymlinks: [],
279
+ };
279
280
 
280
281
  const installEntries = getInstallEntries(pkgJson);
281
282
  if (installEntries.length === 0) {
@@ -350,7 +351,11 @@ export async function checkPluginSymlinks(
350
351
  pkgJson: PluginPackageJson,
351
352
  global = true,
352
353
  ): Promise<{ valid: string[]; broken: string[]; missing: string[] }> {
353
- const result = { valid: [] as string[], broken: [] as string[], missing: [] as string[] };
354
+ const result = {
355
+ valid: [] as string[],
356
+ broken: [] as string[],
357
+ missing: [] as string[],
358
+ };
354
359
  const sourceDir = getPluginSourceDir(pluginName, global);
355
360
  const baseDir = getBaseDir(global);
356
361