@orchid-labs/pluxx 0.1.0 → 0.1.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 (51) hide show
  1. package/README.md +100 -522
  2. package/dist/cli/agent.d.ts +7 -0
  3. package/dist/cli/agent.d.ts.map +1 -1
  4. package/dist/cli/doctor.d.ts +1 -0
  5. package/dist/cli/doctor.d.ts.map +1 -1
  6. package/dist/cli/eval.d.ts +22 -0
  7. package/dist/cli/eval.d.ts.map +1 -0
  8. package/dist/cli/index.d.ts +19 -2
  9. package/dist/cli/index.d.ts.map +1 -1
  10. package/dist/cli/init-from-mcp.d.ts +17 -2
  11. package/dist/cli/init-from-mcp.d.ts.map +1 -1
  12. package/dist/cli/install.d.ts +2 -0
  13. package/dist/cli/install.d.ts.map +1 -1
  14. package/dist/cli/lint.d.ts +5 -1
  15. package/dist/cli/lint.d.ts.map +1 -1
  16. package/dist/cli/mcp-proxy.d.ts +10 -0
  17. package/dist/cli/mcp-proxy.d.ts.map +1 -0
  18. package/dist/cli/migrate.d.ts.map +1 -1
  19. package/dist/cli/sync-from-mcp.d.ts.map +1 -1
  20. package/dist/cli/test.d.ts +2 -0
  21. package/dist/cli/test.d.ts.map +1 -1
  22. package/dist/generators/claude-code/index.d.ts +2 -0
  23. package/dist/generators/claude-code/index.d.ts.map +1 -1
  24. package/dist/generators/codex/index.d.ts +1 -0
  25. package/dist/generators/codex/index.d.ts.map +1 -1
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +99 -1
  29. package/dist/mcp/introspect.d.ts +43 -1
  30. package/dist/mcp/introspect.d.ts.map +1 -1
  31. package/dist/permissions.d.ts.map +1 -1
  32. package/dist/validation/platform-rules.d.ts +20 -0
  33. package/dist/validation/platform-rules.d.ts.map +1 -1
  34. package/package.json +2 -2
  35. package/src/cli/agent.ts +459 -34
  36. package/src/cli/doctor.ts +400 -1
  37. package/src/cli/eval.ts +470 -0
  38. package/src/cli/index.ts +633 -114
  39. package/src/cli/init-from-mcp.ts +545 -41
  40. package/src/cli/install.ts +166 -4
  41. package/src/cli/lint.ts +56 -26
  42. package/src/cli/mcp-proxy.ts +322 -0
  43. package/src/cli/migrate.ts +256 -3
  44. package/src/cli/sync-from-mcp.ts +23 -0
  45. package/src/cli/test.ts +10 -2
  46. package/src/generators/claude-code/index.ts +143 -0
  47. package/src/generators/codex/index.ts +23 -0
  48. package/src/index.ts +12 -1
  49. package/src/mcp/introspect.ts +297 -24
  50. package/src/permissions.ts +3 -1
  51. package/src/validation/platform-rules.ts +121 -0
@@ -1,4 +1,4 @@
1
- import { resolve, basename } from 'path'
1
+ import { resolve, basename, dirname } from 'path'
2
2
  import { existsSync, symlinkSync, mkdirSync, rmSync, readFileSync, writeFileSync, cpSync } from 'fs'
3
3
  import { spawnSync } from 'child_process'
4
4
  import * as readline from 'readline'
@@ -30,6 +30,25 @@ interface CommandResult {
30
30
  stderr: string
31
31
  }
32
32
 
33
+ interface CodexMarketplaceFile {
34
+ name?: string
35
+ interface?: {
36
+ displayName?: string
37
+ }
38
+ plugins?: Array<{
39
+ name: string
40
+ source?: {
41
+ source?: string
42
+ path?: string
43
+ }
44
+ policy?: {
45
+ installation?: string
46
+ authentication?: string
47
+ }
48
+ category?: string
49
+ }>
50
+ }
51
+
33
52
  type CommandRunner = (command: string, args: string[]) => CommandResult
34
53
 
35
54
  export interface HookCommand {
@@ -244,8 +263,8 @@ function getInstallTargets(pluginName: string): InstallTarget[] {
244
263
  },
245
264
  {
246
265
  platform: 'codex',
247
- pluginDir: resolve(home, 'plugins', pluginName),
248
- description: `~/plugins/${pluginName}`,
266
+ pluginDir: resolve(home, '.codex/plugins', pluginName),
267
+ description: `~/.codex/plugins/${pluginName} (via ~/.agents/plugins/marketplace.json)`,
249
268
  },
250
269
  {
251
270
  platform: 'opencode',
@@ -290,6 +309,16 @@ function getInstallTargets(pluginName: string): InstallTarget[] {
290
309
  ]
291
310
  }
292
311
 
312
+ export function getInstallFollowupNotes(platforms: TargetPlatform[]): string[] {
313
+ const notes: string[] = []
314
+
315
+ if (platforms.includes('claude-code')) {
316
+ notes.push('Claude Code note: if Claude is already open, run /reload-plugins in the session to pick up the new install.')
317
+ }
318
+
319
+ return notes
320
+ }
321
+
293
322
  function runCommandDefault(command: string, args: string[]): CommandResult {
294
323
  const result = spawnSync(command, args, { encoding: 'utf-8' })
295
324
  return {
@@ -310,6 +339,90 @@ function createSymlinkInstall(target: PlannedInstallTarget): void {
310
339
  symlinkSync(target.sourceDir, target.pluginDir)
311
340
  }
312
341
 
342
+ function getCodexMarketplacePath(): string {
343
+ const home = process.env.HOME ?? '~'
344
+ return resolve(home, '.agents/plugins/marketplace.json')
345
+ }
346
+
347
+ function getCodexMarketplacePluginPath(pluginName: string): string {
348
+ return `./.codex/plugins/${pluginName}`
349
+ }
350
+
351
+ function readCodexMarketplace(filepath: string): CodexMarketplaceFile {
352
+ if (!existsSync(filepath)) {
353
+ return {
354
+ name: 'pluxx-local',
355
+ interface: {
356
+ displayName: 'Pluxx Local',
357
+ },
358
+ plugins: [],
359
+ }
360
+ }
361
+
362
+ const raw = readFileSync(filepath, 'utf-8')
363
+ const parsed = JSON.parse(raw) as CodexMarketplaceFile
364
+ return {
365
+ name: parsed.name ?? 'pluxx-local',
366
+ interface: parsed.interface ?? { displayName: 'Pluxx Local' },
367
+ plugins: Array.isArray(parsed.plugins) ? parsed.plugins : [],
368
+ }
369
+ }
370
+
371
+ function ensureCodexMarketplace(pluginName: string): void {
372
+ const filepath = getCodexMarketplacePath()
373
+ mkdirSync(dirname(filepath), { recursive: true })
374
+
375
+ const marketplace = readCodexMarketplace(filepath)
376
+ const nextPlugins = (marketplace.plugins ?? []).filter((plugin) => plugin.name !== pluginName)
377
+ nextPlugins.push({
378
+ name: pluginName,
379
+ source: {
380
+ source: 'local',
381
+ path: getCodexMarketplacePluginPath(pluginName),
382
+ },
383
+ policy: {
384
+ installation: 'AVAILABLE',
385
+ authentication: 'ON_INSTALL',
386
+ },
387
+ category: 'Productivity',
388
+ })
389
+
390
+ writeFileSync(
391
+ filepath,
392
+ JSON.stringify({
393
+ name: marketplace.name ?? 'pluxx-local',
394
+ interface: marketplace.interface ?? { displayName: 'Pluxx Local' },
395
+ plugins: nextPlugins,
396
+ }, null, 2) + '\n',
397
+ )
398
+ }
399
+
400
+ function removeCodexMarketplacePlugin(pluginName: string): void {
401
+ const filepath = getCodexMarketplacePath()
402
+ if (!existsSync(filepath)) return
403
+
404
+ const marketplace = readCodexMarketplace(filepath)
405
+ const nextPlugins = (marketplace.plugins ?? []).filter((plugin) => plugin.name !== pluginName)
406
+
407
+ if (nextPlugins.length === (marketplace.plugins ?? []).length) {
408
+ return
409
+ }
410
+
411
+ if (nextPlugins.length === 0) {
412
+ rmSync(filepath, { force: true })
413
+ return
414
+ }
415
+
416
+ writeFileSync(
417
+ filepath,
418
+ JSON.stringify({
419
+ name: marketplace.name ?? 'pluxx-local',
420
+ interface: marketplace.interface ?? { displayName: 'Pluxx Local' },
421
+ plugins: nextPlugins,
422
+ }, null, 2) + '\n',
423
+ )
424
+ }
425
+
313
426
  function createCopiedInstall(target: PlannedInstallTarget): void {
314
427
  const parentDir = resolve(target.pluginDir, '..')
315
428
  mkdirSync(parentDir, { recursive: true })
@@ -585,6 +698,34 @@ function installClaudePlugin(
585
698
  }
586
699
  }
587
700
 
701
+ function uninstallClaudePlugin(
702
+ target: InstallTarget,
703
+ pluginName: string,
704
+ runCommand: CommandRunner,
705
+ options: { quiet?: boolean } = {},
706
+ ): boolean {
707
+ const marketplaceName = getClaudeMarketplaceName(pluginName)
708
+ const uninstall = runCommand('claude', ['plugin', 'uninstall', `${pluginName}@${marketplaceName}`])
709
+
710
+ if (uninstall.status !== 0 && !options.quiet) {
711
+ const detail = uninstall.stderr || uninstall.stdout
712
+ if (detail.trim().length > 0) {
713
+ console.warn(` warning claude-code uninstall: ${detail.trim()}`)
714
+ }
715
+ }
716
+
717
+ const marketplaceRoot = getClaudeMarketplaceRoot(pluginName)
718
+ const hadMarketplaceRoot = existsSync(marketplaceRoot)
719
+ rmSync(marketplaceRoot, { recursive: true, force: true })
720
+
721
+ const hadLegacyPluginDir = existsSync(target.pluginDir)
722
+ if (hadLegacyPluginDir) {
723
+ rmSync(target.pluginDir, { recursive: true, force: true })
724
+ }
725
+
726
+ return uninstall.status === 0 || hadMarketplaceRoot || hadLegacyPluginDir
727
+ }
728
+
588
729
  export function planInstallPlugin(
589
730
  distDir: string,
590
731
  pluginName: string,
@@ -655,6 +796,9 @@ export async function installPlugin(
655
796
  } else {
656
797
  createSymlinkInstall(target)
657
798
  }
799
+ if (target.platform === 'codex') {
800
+ ensureCodexMarketplace(pluginName)
801
+ }
658
802
  if (!options.quiet) {
659
803
  console.log(` ${target.platform} -> ${target.description}`)
660
804
  }
@@ -665,22 +809,37 @@ export async function installPlugin(
665
809
  console.log('Nothing to install. Run `pluxx build` first.')
666
810
  } else if (!options.quiet) {
667
811
  console.log(`\nInstalled ${installed} plugin(s). Reload or restart your tools to pick them up.`)
812
+ for (const note of getInstallFollowupNotes(filtered.map((target) => target.platform))) {
813
+ console.log(note)
814
+ }
668
815
  }
669
816
  }
670
817
 
671
818
  export async function uninstallPlugin(
672
819
  pluginName: string,
673
820
  platforms?: TargetPlatform[],
674
- options: { quiet?: boolean } = {},
821
+ options: { quiet?: boolean; runCommand?: CommandRunner } = {},
675
822
  ): Promise<void> {
676
823
  const targets = getInstallTargets(pluginName)
677
824
  const filtered = platforms
678
825
  ? targets.filter(t => platforms.includes(t.platform))
679
826
  : targets
827
+ const runCommand = options.runCommand ?? runCommandDefault
680
828
 
681
829
  let removed = 0
682
830
 
683
831
  for (const target of filtered) {
832
+ if (target.platform === 'claude-code') {
833
+ const removedClaude = uninstallClaudePlugin(target, pluginName, runCommand, { quiet: options.quiet })
834
+ if (removedClaude) {
835
+ if (!options.quiet) {
836
+ console.log(` removed ${target.description}`)
837
+ }
838
+ removed++
839
+ }
840
+ continue
841
+ }
842
+
684
843
  if (existsSync(target.pluginDir)) {
685
844
  rmSync(target.pluginDir, { recursive: true, force: true })
686
845
  if (!options.quiet) {
@@ -688,6 +847,9 @@ export async function uninstallPlugin(
688
847
  }
689
848
  removed++
690
849
  }
850
+ if (target.platform === 'codex') {
851
+ removeCodexMarketplacePlugin(pluginName)
852
+ }
691
853
  }
692
854
 
693
855
  if (removed === 0 && !options.quiet) {
package/src/cli/lint.ts CHANGED
@@ -66,6 +66,10 @@ export interface LintResult {
66
66
  issues: LintIssue[]
67
67
  }
68
68
 
69
+ export interface LintProjectOptions {
70
+ targets?: TargetPlatform[]
71
+ }
72
+
69
73
  const SKILL_NAME_REGEX = AGENT_SKILLS_RULES.name.pattern
70
74
  const MAX_AGENT_SKILLS_DESCRIPTION = AGENT_SKILLS_RULES.description.maxLength
71
75
  const MAX_CLAUDE_DESCRIPTION = CLAUDE_CODE_RULES.description.maxDisplayLength
@@ -1035,6 +1039,25 @@ function lintCursorRuleContentLimits(config: PluginConfig, issues: LintIssue[]):
1035
1039
  function lintPermissions(config: PluginConfig, issues: LintIssue[]): void {
1036
1040
  if (!config.permissions) return
1037
1041
 
1042
+ for (const action of ['allow', 'ask', 'deny'] as const) {
1043
+ const seen = new Set<string>()
1044
+ for (const raw of config.permissions[action] ?? []) {
1045
+ const trimmed = raw.trim()
1046
+
1047
+ if (seen.has(trimmed)) {
1048
+ pushIssue(issues, {
1049
+ level: 'warning',
1050
+ code: 'permissions-duplicate',
1051
+ message: `Permission rule "${trimmed}" is duplicated in "${action}".`,
1052
+ file: 'pluxx.config.ts',
1053
+ platform: 'Permissions',
1054
+ })
1055
+ }
1056
+
1057
+ seen.add(trimmed)
1058
+ }
1059
+ }
1060
+
1038
1061
  const rules = collectPermissionRules(config.permissions)
1039
1062
  const seen = new Map<string, Set<string>>()
1040
1063
 
@@ -1061,7 +1084,7 @@ function lintPermissions(config: PluginConfig, issues: LintIssue[]): void {
1061
1084
  pushIssue(issues, {
1062
1085
  level: 'warning',
1063
1086
  code: 'codex-permissions-external-config',
1064
- message: 'Codex does not currently support plugin-packaged permission enforcement. Mirror canonical permissions into Codex user/admin config or external hooks for real enforcement.',
1087
+ message: 'Codex does not currently support plugin-packaged permission enforcement. Mirror canonical permissions into Codex user/admin config or external hooks for real enforcement (Pluxx emits .codex/permissions.generated.json as a starter mirror).',
1065
1088
  file: 'pluxx.config.ts',
1066
1089
  platform: 'Codex',
1067
1090
  })
@@ -1100,7 +1123,10 @@ function sortIssues(issues: LintIssue[]): LintIssue[] {
1100
1123
  })
1101
1124
  }
1102
1125
 
1103
- export async function lintProject(dir: string = process.cwd()): Promise<LintResult> {
1126
+ export async function lintProject(
1127
+ dir: string = process.cwd(),
1128
+ options: LintProjectOptions = {},
1129
+ ): Promise<LintResult> {
1104
1130
  const issues: LintIssue[] = []
1105
1131
  const frontmatterCache = new Map<string, ParsedFrontmatterFile>()
1106
1132
 
@@ -1118,17 +1144,21 @@ export async function lintProject(dir: string = process.cwd()): Promise<LintResu
1118
1144
  return { errors: 1, warnings: 0, issues }
1119
1145
  }
1120
1146
 
1147
+ const lintConfig: PluginConfig = options.targets
1148
+ ? { ...config, targets: options.targets }
1149
+ : config
1150
+
1121
1151
  // Plugin structure checks
1122
- lintPluginName(config, issues)
1123
- lintVersionFormat(config, issues)
1124
- lintManifestPaths(config, issues)
1152
+ lintPluginName(lintConfig, issues)
1153
+ lintVersionFormat(lintConfig, issues)
1154
+ lintManifestPaths(lintConfig, issues)
1125
1155
  lintPluginDirectoryPlacement(dir, issues)
1126
- lintAbsolutePaths(config, issues)
1156
+ lintAbsolutePaths(lintConfig, issues)
1127
1157
  lintSettingsJson(dir, issues)
1128
- lintLegacyCommandsDir(dir, config, issues)
1158
+ lintLegacyCommandsDir(dir, lintConfig, issues)
1129
1159
 
1130
1160
  // Hook and event validation
1131
- lintHookEvents(config, issues)
1161
+ lintHookEvents(lintConfig, issues)
1132
1162
 
1133
1163
  // Agent file checks
1134
1164
  const agentsDir = resolve(dir, 'agents')
@@ -1137,25 +1167,25 @@ export async function lintProject(dir: string = process.cwd()): Promise<LintResu
1137
1167
  lintAgentIsolation(agentFiles, issues, frontmatterCache)
1138
1168
 
1139
1169
  // MCP and brand
1140
- lintMcpUrls(config, issues)
1141
- lintBrandMetadata(config, issues)
1142
- lintCodexOverrides(config, issues)
1143
- lintCodexHookCompatibility(config, issues)
1144
- lintCodexAgentsConfig(config, issues)
1145
- lintCodexHooksExternalConfig(config, issues)
1146
- lintPermissions(config, issues)
1170
+ lintMcpUrls(lintConfig, issues)
1171
+ lintBrandMetadata(lintConfig, issues)
1172
+ lintCodexOverrides(lintConfig, issues)
1173
+ lintCodexHookCompatibility(lintConfig, issues)
1174
+ lintCodexAgentsConfig(lintConfig, issues)
1175
+ lintCodexHooksExternalConfig(lintConfig, issues)
1176
+ lintPermissions(lintConfig, issues)
1147
1177
 
1148
1178
  // Cursor-specific checks
1149
- lintCursorHooks(config, issues)
1150
- lintCursorRuleContentLimits(config, issues)
1179
+ lintCursorHooks(lintConfig, issues)
1180
+ lintCursorRuleContentLimits(lintConfig, issues)
1151
1181
 
1152
- const skillsDir = resolve(dir, config.skills)
1182
+ const skillsDir = resolve(dir, lintConfig.skills)
1153
1183
  let skillFiles: string[] = []
1154
1184
  if (!existsSync(skillsDir)) {
1155
1185
  pushIssue(issues, {
1156
1186
  level: 'error',
1157
1187
  code: 'skills-dir-missing',
1158
- message: `Skills directory not found: ${config.skills}`,
1188
+ message: `Skills directory not found: ${lintConfig.skills}`,
1159
1189
  file: 'pluxx.config.ts',
1160
1190
  platform: 'Agent Skills',
1161
1191
  })
@@ -1165,29 +1195,29 @@ export async function lintProject(dir: string = process.cwd()): Promise<LintResu
1165
1195
  pushIssue(issues, {
1166
1196
  level: 'warning',
1167
1197
  code: 'skills-none-found',
1168
- message: `No SKILL.md files found in ${config.skills}`,
1198
+ message: `No SKILL.md files found in ${lintConfig.skills}`,
1169
1199
  file: 'pluxx.config.ts',
1170
1200
  platform: 'Agent Skills',
1171
1201
  })
1172
1202
  }
1173
1203
 
1174
1204
  for (const skillFile of skillFiles) {
1175
- lintSkillFile(skillFile, config.targets, issues, frontmatterCache)
1205
+ lintSkillFile(skillFile, lintConfig.targets, issues, frontmatterCache)
1176
1206
  }
1177
1207
  }
1178
1208
 
1179
1209
  // Cursor skill frontmatter checks
1180
- lintCursorSkillFrontmatter(config, skillFiles, issues, frontmatterCache)
1181
- lintSkillListingBudgets(skillFiles, config.targets, issues, frontmatterCache)
1210
+ lintCursorSkillFrontmatter(lintConfig, skillFiles, issues, frontmatterCache)
1211
+ lintSkillListingBudgets(skillFiles, lintConfig.targets, issues, frontmatterCache)
1182
1212
 
1183
1213
  // Platform limit checks for manifest prompts (Codex)
1184
- lintManifestPromptLimits(config, issues)
1214
+ lintManifestPromptLimits(lintConfig, issues)
1185
1215
 
1186
1216
  // Platform limit checks for instructions file size
1187
- lintInstructionsFileLimits(config, dir, issues)
1217
+ lintInstructionsFileLimits(lintConfig, dir, issues)
1188
1218
 
1189
1219
  // Platform limit checks for rules file line count
1190
- lintRulesFileLimits(config, dir, issues)
1220
+ lintRulesFileLimits(lintConfig, dir, issues)
1191
1221
 
1192
1222
  const sorted = sortIssues(issues)
1193
1223
  const errors = sorted.filter(i => i.level === 'error').length