@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.
- package/README.md +100 -522
- package/dist/cli/agent.d.ts +7 -0
- package/dist/cli/agent.d.ts.map +1 -1
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/eval.d.ts +22 -0
- package/dist/cli/eval.d.ts.map +1 -0
- package/dist/cli/index.d.ts +19 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/init-from-mcp.d.ts +17 -2
- package/dist/cli/init-from-mcp.d.ts.map +1 -1
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/install.d.ts.map +1 -1
- package/dist/cli/lint.d.ts +5 -1
- package/dist/cli/lint.d.ts.map +1 -1
- package/dist/cli/mcp-proxy.d.ts +10 -0
- package/dist/cli/mcp-proxy.d.ts.map +1 -0
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/sync-from-mcp.d.ts.map +1 -1
- package/dist/cli/test.d.ts +2 -0
- package/dist/cli/test.d.ts.map +1 -1
- package/dist/generators/claude-code/index.d.ts +2 -0
- package/dist/generators/claude-code/index.d.ts.map +1 -1
- package/dist/generators/codex/index.d.ts +1 -0
- package/dist/generators/codex/index.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +99 -1
- package/dist/mcp/introspect.d.ts +43 -1
- package/dist/mcp/introspect.d.ts.map +1 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/validation/platform-rules.d.ts +20 -0
- package/dist/validation/platform-rules.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/cli/agent.ts +459 -34
- package/src/cli/doctor.ts +400 -1
- package/src/cli/eval.ts +470 -0
- package/src/cli/index.ts +633 -114
- package/src/cli/init-from-mcp.ts +545 -41
- package/src/cli/install.ts +166 -4
- package/src/cli/lint.ts +56 -26
- package/src/cli/mcp-proxy.ts +322 -0
- package/src/cli/migrate.ts +256 -3
- package/src/cli/sync-from-mcp.ts +23 -0
- package/src/cli/test.ts +10 -2
- package/src/generators/claude-code/index.ts +143 -0
- package/src/generators/codex/index.ts +23 -0
- package/src/index.ts +12 -1
- package/src/mcp/introspect.ts +297 -24
- package/src/permissions.ts +3 -1
- 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 {
|
|
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,
|