@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
package/src/cli/test.ts CHANGED
@@ -3,6 +3,7 @@ import { resolve } from 'path'
3
3
  import { loadConfig } from '../config/load'
4
4
  import { build } from '../generators'
5
5
  import { lintProject, type LintResult } from './lint'
6
+ import { runEvalSuite, type EvalReport } from './eval'
6
7
  import type { TargetPlatform } from '../schema'
7
8
 
8
9
  export interface TestRunOptions {
@@ -25,6 +26,7 @@ export interface TestRunResult {
25
26
  error?: string
26
27
  }
27
28
  lint?: LintResult
29
+ eval?: EvalReport
28
30
  build?: {
29
31
  ok: boolean
30
32
  outDir: string
@@ -56,7 +58,7 @@ export async function runTestSuite(options: TestRunOptions = {}): Promise<TestRu
56
58
  try {
57
59
  const config = await loadConfig(rootDir)
58
60
  const targets = options.targets ?? config.targets
59
- const lint = await lintProject(rootDir)
61
+ const lint = await lintProject(rootDir, { targets })
60
62
 
61
63
  if (lint.errors > 0) {
62
64
  return {
@@ -71,6 +73,7 @@ export async function runTestSuite(options: TestRunOptions = {}): Promise<TestRu
71
73
  }
72
74
 
73
75
  await build(config, rootDir, { targets })
76
+ const evalReport = await runEvalSuite({ rootDir })
74
77
 
75
78
  const checks = targets.map((platform) => {
76
79
  const requiredPath = SMOKE_PATHS[platform]
@@ -79,13 +82,14 @@ export async function runTestSuite(options: TestRunOptions = {}): Promise<TestRu
79
82
  })
80
83
 
81
84
  return {
82
- ok: checks.every((check) => check.ok),
85
+ ok: checks.every((check) => check.ok) && evalReport.errors === 0,
83
86
  config: {
84
87
  ok: true,
85
88
  name: config.name,
86
89
  version: config.version,
87
90
  },
88
91
  lint,
92
+ eval: evalReport,
89
93
  build: {
90
94
  ok: true,
91
95
  outDir: config.outDir,
@@ -119,6 +123,10 @@ export function printTestResult(result: TestRunResult): void {
119
123
  console.log(`Lint: ${result.lint.errors} error(s), ${result.lint.warnings} warning(s)`)
120
124
  }
121
125
 
126
+ if (result.eval) {
127
+ console.log(`Eval: ${result.eval.errors} error(s), ${result.eval.warnings} warning(s), ${result.eval.infos} info message(s)`)
128
+ }
129
+
122
130
  if (result.build) {
123
131
  console.log(`Build: ${result.build.targets.join(', ')} -> ${result.build.outDir}`)
124
132
  }
@@ -1,6 +1,8 @@
1
1
  import { Generator } from '../base'
2
2
  import { generateClaudeFamilyOutputs } from '../shared/claude-family'
3
3
  import type { TargetPlatform } from '../../schema'
4
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
5
+ import { basename, join } from 'path'
4
6
 
5
7
  export class ClaudeCodeGenerator extends Generator {
6
8
  readonly platform: TargetPlatform = 'claude-code'
@@ -26,4 +28,145 @@ export class ClaudeCodeGenerator extends Generator {
26
28
  this.copyScripts()
27
29
  this.copyAssets()
28
30
  }
31
+
32
+ protected copySkills(): void {
33
+ super.copySkills()
34
+
35
+ const collidingSkills = this.collectCollidingSkills()
36
+ for (const skill of collidingSkills) {
37
+ const outputPath = join(this.outDir, 'skills', skill.dirName, 'SKILL.md')
38
+ if (!existsSync(outputPath)) continue
39
+
40
+ const current = readFileSync(outputPath, 'utf-8')
41
+ const hiddenName = buildHiddenSkillName(skill.effectiveName)
42
+ const rewritten = rewriteClaudeCollidingSkill(current, hiddenName)
43
+ if (rewritten !== current) {
44
+ writeFileSync(outputPath, rewritten, 'utf-8')
45
+ }
46
+ }
47
+ }
48
+
49
+ private collectCollidingSkills(): Array<{ dirName: string; effectiveName: string }> {
50
+ if (!this.config.commands) return []
51
+
52
+ const commandsSrc = this.resolveConfigPath(this.config.commands, 'commands')
53
+ const skillsSrc = this.resolveConfigPath(this.config.skills, 'skills')
54
+ if (!existsSync(commandsSrc) || !existsSync(skillsSrc)) return []
55
+
56
+ const commandNames = collectTopLevelCommandNames(commandsSrc)
57
+ const collidingSkills: Array<{ dirName: string; effectiveName: string }> = []
58
+
59
+ for (const entry of readdirSync(skillsSrc, { withFileTypes: true })) {
60
+ if (!entry.isDirectory()) continue
61
+ const skillFile = join(skillsSrc, entry.name, 'SKILL.md')
62
+ if (!existsSync(skillFile)) continue
63
+
64
+ const content = readFileSync(skillFile, 'utf-8')
65
+ const effectiveName = getEffectiveSkillName(content, entry.name)
66
+ if (commandNames.has(effectiveName)) {
67
+ collidingSkills.push({ dirName: entry.name, effectiveName })
68
+ }
69
+ }
70
+
71
+ return collidingSkills
72
+ }
73
+ }
74
+
75
+ function collectTopLevelCommandNames(commandsRoot: string): Set<string> {
76
+ const commandNames = new Set<string>()
77
+ for (const entry of readdirSync(commandsRoot, { withFileTypes: true })) {
78
+ if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
79
+ commandNames.add(basename(entry.name, '.md'))
80
+ }
81
+ }
82
+ return commandNames
83
+ }
84
+
85
+ function getEffectiveSkillName(content: string, fallback: string): string {
86
+ const frontmatter = extractFrontmatterLines(content)
87
+ if (!frontmatter) return fallback
88
+
89
+ for (const line of frontmatter) {
90
+ const match = /^name:\s*(.+)\s*$/i.exec(line.trim())
91
+ if (match?.[1]) {
92
+ return stripYamlScalar(match[1]) || fallback
93
+ }
94
+ }
95
+
96
+ return fallback
97
+ }
98
+
99
+ function buildHiddenSkillName(name: string): string {
100
+ const maxBaseLength = 64 - '-skill'.length
101
+ const trimmed = name.length > maxBaseLength ? name.slice(0, maxBaseLength) : name
102
+ return `${trimmed}-skill`
103
+ }
104
+
105
+ function rewriteClaudeCollidingSkill(content: string, hiddenName: string): string {
106
+ const frontmatter = extractFrontmatterLines(content)
107
+ if (!frontmatter) {
108
+ return [
109
+ '---',
110
+ `name: ${hiddenName}`,
111
+ 'user-invocable: false',
112
+ '---',
113
+ '',
114
+ content.trimStart(),
115
+ ].join('\n')
116
+ }
117
+
118
+ const rewritten = [...frontmatter]
119
+ let sawName = false
120
+ let sawUserInvocable = false
121
+
122
+ for (let index = 0; index < rewritten.length; index += 1) {
123
+ const trimmed = rewritten[index].trim()
124
+ if (/^name:\s*/i.test(trimmed)) {
125
+ rewritten[index] = `name: ${hiddenName}`
126
+ sawName = true
127
+ continue
128
+ }
129
+ if (/^user-invocable:\s*/i.test(trimmed)) {
130
+ rewritten[index] = 'user-invocable: false'
131
+ sawUserInvocable = true
132
+ }
133
+ }
134
+
135
+ if (!sawName) rewritten.push(`name: ${hiddenName}`)
136
+ if (!sawUserInvocable) rewritten.push('user-invocable: false')
137
+
138
+ const lines = content.split('\n')
139
+ const endIndex = findFrontmatterEndIndex(lines)
140
+ const body = endIndex === -1 ? content : lines.slice(endIndex + 1).join('\n')
141
+ return ['---', ...rewritten, '---', body ? `\n${body.replace(/^\n/, '')}` : ''].join('\n')
142
+ }
143
+
144
+ function extractFrontmatterLines(content: string): string[] | null {
145
+ const lines = content.split('\n')
146
+ const endIndex = findFrontmatterEndIndex(lines)
147
+ if (endIndex === -1) return null
148
+ return lines.slice(1, endIndex)
149
+ }
150
+
151
+ function findFrontmatterEndIndex(lines: string[]): number {
152
+ if (lines[0]?.trim() !== '---') return -1
153
+
154
+ for (let index = 1; index < lines.length; index += 1) {
155
+ if (lines[index].trim() === '---') {
156
+ return index
157
+ }
158
+ }
159
+
160
+ return -1
161
+ }
162
+
163
+ function stripYamlScalar(value: string): string {
164
+ const trimmed = value.trim()
165
+ if (
166
+ (trimmed.startsWith('"') && trimmed.endsWith('"'))
167
+ || (trimmed.startsWith("'") && trimmed.endsWith("'"))
168
+ ) {
169
+ return trimmed.slice(1, -1).trim()
170
+ }
171
+ return trimmed
29
172
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'fs'
2
2
  import { Generator } from '../base'
3
3
  import type { TargetPlatform } from '../../schema'
4
+ import { collectPermissionRules } from '../../permissions'
4
5
 
5
6
  export class CodexGenerator extends Generator {
6
7
  readonly platform: TargetPlatform = 'codex'
@@ -44,6 +45,7 @@ export class CodexGenerator extends Generator {
44
45
  },
45
46
  }),
46
47
  this.generateAgentsMd(),
48
+ this.generatePermissionsCompanion(),
47
49
  ])
48
50
 
49
51
  this.copySkills()
@@ -92,6 +94,12 @@ export class CodexGenerator extends Generator {
92
94
  if (this.config.brand.websiteURL) {
93
95
  iface.websiteURL = this.config.brand.websiteURL
94
96
  }
97
+ if (this.config.brand.privacyPolicyURL) {
98
+ iface.privacyPolicyURL = this.config.brand.privacyPolicyURL
99
+ }
100
+ if (this.config.brand.termsOfServiceURL) {
101
+ iface.termsOfServiceURL = this.config.brand.termsOfServiceURL
102
+ }
95
103
  if (this.config.brand.screenshots) {
96
104
  iface.screenshots = this.config.brand.screenshots
97
105
  }
@@ -109,6 +117,21 @@ export class CodexGenerator extends Generator {
109
117
 
110
118
  await this.writeJson('.codex-plugin/plugin.json', manifest)
111
119
  }
120
+
121
+ private async generatePermissionsCompanion(): Promise<void> {
122
+ if (!this.config.permissions) return
123
+
124
+ const rules = collectPermissionRules(this.config.permissions)
125
+ if (rules.length === 0) return
126
+
127
+ await this.writeJson('.codex/permissions.generated.json', {
128
+ model: 'pluxx.permissions.v1',
129
+ enforcedByPluginBundle: false,
130
+ note: 'Codex permissions are configured externally. Use this file as a generated mirror of canonical rules for Codex user/admin policy or hook configuration.',
131
+ rules,
132
+ })
133
+ }
134
+
112
135
  private async generateAgentsMd(): Promise<void> {
113
136
  if (!this.config.instructions) return
114
137
  const srcPath = this.resolveConfigPath(this.config.instructions, 'instructions')
package/src/index.ts CHANGED
@@ -7,7 +7,18 @@ export {
7
7
  type Permissions,
8
8
  } from './schema'
9
9
  export { definePlugin } from './config/define'
10
- export { PLATFORM_LIMITS, PLATFORM_VALIDATION_RULES, getPlatformRules, type PlatformLimits, type PlatformRules, type PlatformRuleSource } from './validation/platform-rules'
10
+ export {
11
+ PLATFORM_LIMITS,
12
+ PLATFORM_LIMIT_POLICIES,
13
+ PLATFORM_VALIDATION_RULES,
14
+ getPlatformRules,
15
+ type PlatformLimitKind,
16
+ type PlatformLimitPolicy,
17
+ type PlatformLimitPolicies,
18
+ type PlatformLimits,
19
+ type PlatformRules,
20
+ type PlatformRuleSource,
21
+ } from './validation/platform-rules'
11
22
  export { getPlatformCompatibilityMatrix, renderCompatibilityMatrixMarkdown, type PlatformCompatibilityRow } from './compatibility/matrix'
12
23
  export {
13
24
  buildGeneratedPermissionHookScript,