@orchid-labs/pluxx 0.1.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.
- package/LICENSE +21 -0
- package/README.md +574 -0
- package/bin/pluxx.js +37 -0
- package/dist/cli/agent.d.ts +90 -0
- package/dist/cli/agent.d.ts.map +1 -0
- package/dist/cli/dev.d.ts +2 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/doctor.d.ts +19 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/index.d.ts +24 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/init-from-mcp.d.ts +145 -0
- package/dist/cli/init-from-mcp.d.ts.map +1 -0
- package/dist/cli/install.d.ts +56 -0
- package/dist/cli/install.d.ts.map +1 -0
- package/dist/cli/lint.d.ts +18 -0
- package/dist/cli/lint.d.ts.map +1 -0
- package/dist/cli/migrate.d.ts +2 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/prompt.d.ts +20 -0
- package/dist/cli/prompt.d.ts.map +1 -0
- package/dist/cli/publish.d.ts +70 -0
- package/dist/cli/publish.d.ts.map +1 -0
- package/dist/cli/runtime.d.ts +20 -0
- package/dist/cli/runtime.d.ts.map +1 -0
- package/dist/cli/sync-from-mcp.d.ts +32 -0
- package/dist/cli/sync-from-mcp.d.ts.map +1 -0
- package/dist/cli/test.d.ts +33 -0
- package/dist/cli/test.d.ts.map +1 -0
- package/dist/compatibility/matrix.d.ts +14 -0
- package/dist/compatibility/matrix.d.ts.map +1 -0
- package/dist/config/define.d.ts +18 -0
- package/dist/config/define.d.ts.map +1 -0
- package/dist/config/load.d.ts +7 -0
- package/dist/config/load.d.ts.map +1 -0
- package/dist/generators/amp/index.d.ts +13 -0
- package/dist/generators/amp/index.d.ts.map +1 -0
- package/dist/generators/base.d.ts +49 -0
- package/dist/generators/base.d.ts.map +1 -0
- package/dist/generators/claude-code/index.d.ts +7 -0
- package/dist/generators/claude-code/index.d.ts.map +1 -0
- package/dist/generators/cline/index.d.ts +14 -0
- package/dist/generators/cline/index.d.ts.map +1 -0
- package/dist/generators/codex/index.d.ts +9 -0
- package/dist/generators/codex/index.d.ts.map +1 -0
- package/dist/generators/cursor/index.d.ts +11 -0
- package/dist/generators/cursor/index.d.ts.map +1 -0
- package/dist/generators/gemini-cli/index.d.ts +13 -0
- package/dist/generators/gemini-cli/index.d.ts.map +1 -0
- package/dist/generators/github-copilot/index.d.ts +11 -0
- package/dist/generators/github-copilot/index.d.ts.map +1 -0
- package/dist/generators/hooks-warning.d.ts +3 -0
- package/dist/generators/hooks-warning.d.ts.map +1 -0
- package/dist/generators/index.d.ts +11 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/opencode/index.d.ts +15 -0
- package/dist/generators/opencode/index.d.ts.map +1 -0
- package/dist/generators/openhands/index.d.ts +11 -0
- package/dist/generators/openhands/index.d.ts.map +1 -0
- package/dist/generators/roo-code/index.d.ts +14 -0
- package/dist/generators/roo-code/index.d.ts.map +1 -0
- package/dist/generators/shared/claude-family.d.ts +18 -0
- package/dist/generators/shared/claude-family.d.ts.map +1 -0
- package/dist/generators/warp/index.d.ts +13 -0
- package/dist/generators/warp/index.d.ts.map +1 -0
- package/dist/hook-events.d.ts +4 -0
- package/dist/hook-events.d.ts.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5302 -0
- package/dist/mcp/introspect.d.ts +34 -0
- package/dist/mcp/introspect.d.ts.map +1 -0
- package/dist/permissions.d.ts +18 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/schema.d.ts +9457 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/user-config.d.ts +19 -0
- package/dist/user-config.d.ts.map +1 -0
- package/dist/validation/platform-rules.d.ts +64 -0
- package/dist/validation/platform-rules.d.ts.map +1 -0
- package/package.json +76 -0
- package/src/cli/agent.ts +1030 -0
- package/src/cli/dev.ts +112 -0
- package/src/cli/doctor.ts +588 -0
- package/src/cli/index.ts +2414 -0
- package/src/cli/init-from-mcp.ts +1611 -0
- package/src/cli/install.ts +698 -0
- package/src/cli/lint.ts +1219 -0
- package/src/cli/migrate.ts +614 -0
- package/src/cli/prompt.ts +82 -0
- package/src/cli/publish.ts +401 -0
- package/src/cli/runtime.ts +86 -0
- package/src/cli/sync-from-mcp.ts +563 -0
- package/src/cli/test.ts +134 -0
- package/src/compatibility/matrix.ts +149 -0
- package/src/config/define.ts +20 -0
- package/src/config/load.ts +74 -0
- package/src/generators/amp/index.ts +63 -0
- package/src/generators/base.ts +188 -0
- package/src/generators/claude-code/index.ts +29 -0
- package/src/generators/cline/index.ts +35 -0
- package/src/generators/codex/index.ts +120 -0
- package/src/generators/cursor/index.ts +158 -0
- package/src/generators/gemini-cli/index.ts +83 -0
- package/src/generators/github-copilot/index.ts +32 -0
- package/src/generators/hooks-warning.ts +51 -0
- package/src/generators/index.ts +71 -0
- package/src/generators/opencode/index.ts +526 -0
- package/src/generators/openhands/index.ts +32 -0
- package/src/generators/roo-code/index.ts +35 -0
- package/src/generators/shared/claude-family.ts +215 -0
- package/src/generators/warp/index.ts +32 -0
- package/src/hook-events.ts +33 -0
- package/src/index.ts +23 -0
- package/src/mcp/introspect.ts +834 -0
- package/src/permissions.ts +258 -0
- package/src/schema.ts +312 -0
- package/src/user-config.ts +177 -0
- package/src/validation/platform-rules.ts +565 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
export const PERMISSION_SELECTOR_KINDS = ['Bash', 'Edit', 'Read', 'MCP', 'Skill'] as const
|
|
2
|
+
|
|
3
|
+
export type PermissionRuleKind = typeof PERMISSION_SELECTOR_KINDS[number]
|
|
4
|
+
export type PermissionAction = 'allow' | 'ask' | 'deny'
|
|
5
|
+
|
|
6
|
+
export interface ParsedPermissionRule {
|
|
7
|
+
action: PermissionAction
|
|
8
|
+
raw: string
|
|
9
|
+
kind: PermissionRuleKind
|
|
10
|
+
pattern: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface OpenCodePermissionMap {
|
|
14
|
+
[tool: string]: PermissionAction
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PERMISSION_RULE_REGEX = new RegExp(
|
|
18
|
+
`^(${PERMISSION_SELECTOR_KINDS.join('|')})\\((.+)\\)$`,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export function parsePermissionRule(raw: string): Omit<ParsedPermissionRule, 'action'> | null {
|
|
22
|
+
const trimmed = raw.trim()
|
|
23
|
+
const match = trimmed.match(PERMISSION_RULE_REGEX)
|
|
24
|
+
if (!match) return null
|
|
25
|
+
|
|
26
|
+
const kind = match[1] as PermissionRuleKind
|
|
27
|
+
const pattern = match[2].trim()
|
|
28
|
+
if (!pattern) return null
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
raw: trimmed,
|
|
32
|
+
kind,
|
|
33
|
+
pattern,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function collectPermissionRules(
|
|
38
|
+
permissions: Partial<Record<PermissionAction, string[]>> | undefined,
|
|
39
|
+
): ParsedPermissionRule[] {
|
|
40
|
+
if (!permissions) return []
|
|
41
|
+
|
|
42
|
+
const actions: PermissionAction[] = ['allow', 'ask', 'deny']
|
|
43
|
+
const rules: ParsedPermissionRule[] = []
|
|
44
|
+
|
|
45
|
+
for (const action of actions) {
|
|
46
|
+
for (const raw of permissions[action] ?? []) {
|
|
47
|
+
const parsed = parsePermissionRule(raw)
|
|
48
|
+
if (!parsed) continue
|
|
49
|
+
rules.push({
|
|
50
|
+
...parsed,
|
|
51
|
+
action,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return rules
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ACTION_PRIORITY: Record<PermissionAction, number> = {
|
|
60
|
+
allow: 0,
|
|
61
|
+
ask: 1,
|
|
62
|
+
deny: 2,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mergeAction(current: PermissionAction | undefined, next: PermissionAction): PermissionAction {
|
|
66
|
+
if (!current) return next
|
|
67
|
+
return ACTION_PRIORITY[next] > ACTION_PRIORITY[current] ? next : current
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function permissionRulesNeedToolLevelDowngrade(
|
|
71
|
+
permissions: Partial<Record<PermissionAction, string[]>> | undefined,
|
|
72
|
+
): boolean {
|
|
73
|
+
return collectPermissionRules(permissions).some((rule) => rule.pattern !== '*')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildOpenCodePermissionMap(
|
|
77
|
+
permissions: Partial<Record<PermissionAction, string[]>> | undefined,
|
|
78
|
+
): OpenCodePermissionMap {
|
|
79
|
+
const rules = collectPermissionRules(permissions)
|
|
80
|
+
const output: OpenCodePermissionMap = {}
|
|
81
|
+
|
|
82
|
+
const toolAliases: Record<PermissionRuleKind, string[]> = {
|
|
83
|
+
Bash: ['bash', 'shell'],
|
|
84
|
+
Edit: ['edit', 'write'],
|
|
85
|
+
Read: ['read'],
|
|
86
|
+
MCP: ['mcp'],
|
|
87
|
+
Skill: ['skill'],
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const rule of rules) {
|
|
91
|
+
for (const tool of toolAliases[rule.kind]) {
|
|
92
|
+
output[tool] = mergeAction(output[tool], rule.action)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return output
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildGeneratedPermissionHookScript(
|
|
100
|
+
permissions: Partial<Record<PermissionAction, string[]>> | undefined,
|
|
101
|
+
): string | null {
|
|
102
|
+
const rules = collectPermissionRules(permissions)
|
|
103
|
+
if (rules.length === 0) return null
|
|
104
|
+
|
|
105
|
+
return `#!/usr/bin/env node
|
|
106
|
+
const RULES = ${JSON.stringify(rules, null, 2)};
|
|
107
|
+
const ACTION_PRIORITY = { allow: 0, ask: 1, deny: 2 };
|
|
108
|
+
|
|
109
|
+
function wildcardToRegExp(pattern) {
|
|
110
|
+
const escaped = pattern
|
|
111
|
+
.replace(/[.+^$(){}|[\\]\\\\]/g, "\\\\$&")
|
|
112
|
+
.replace(/\\*/g, ".*");
|
|
113
|
+
return new RegExp("^" + escaped + "$", "i");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function matchesPattern(pattern, value) {
|
|
117
|
+
if (!value) return false;
|
|
118
|
+
return wildcardToRegExp(pattern).test(String(value));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function firstValue(...values) {
|
|
122
|
+
return values.find((value) => typeof value === "string" && value.trim() !== "");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseJsonInput() {
|
|
126
|
+
const chunks = [];
|
|
127
|
+
process.stdin.setEncoding("utf8");
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
130
|
+
process.stdin.on("end", () => {
|
|
131
|
+
const raw = chunks.join("").trim();
|
|
132
|
+
if (!raw) return resolve({});
|
|
133
|
+
try {
|
|
134
|
+
resolve(JSON.parse(raw));
|
|
135
|
+
} catch (error) {
|
|
136
|
+
reject(error);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
process.stdin.on("error", reject);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseMcpName(raw) {
|
|
144
|
+
if (!raw) return undefined;
|
|
145
|
+
if (raw.startsWith("mcp__")) {
|
|
146
|
+
const match = raw.match(/^mcp__([^_]+)__(.+)$/);
|
|
147
|
+
if (match) return match[1] + "." + match[2].replace(/__/g, ".");
|
|
148
|
+
}
|
|
149
|
+
return raw;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function inferCandidates(mode, event) {
|
|
153
|
+
const toolName = firstValue(event.tool_name, event.toolName, event.tool, event.matcher);
|
|
154
|
+
const input = event.tool_input ?? event.toolInput ?? event.input ?? {};
|
|
155
|
+
const candidates = [];
|
|
156
|
+
|
|
157
|
+
const filePath = firstValue(
|
|
158
|
+
event.file_path,
|
|
159
|
+
input.file_path,
|
|
160
|
+
input.path,
|
|
161
|
+
input.filePath,
|
|
162
|
+
input.target_file,
|
|
163
|
+
input.targetFile
|
|
164
|
+
);
|
|
165
|
+
const command = firstValue(event.command, input.command, input.cmd, input.shell_command);
|
|
166
|
+
const skillName = firstValue(input.name, input.skill_name, input.skillName);
|
|
167
|
+
const mcpName = parseMcpName(firstValue(event.tool_name, input.tool_name, input.name));
|
|
168
|
+
|
|
169
|
+
if (mode === "cursor-shell") {
|
|
170
|
+
if (command) candidates.push({ kind: "Bash", value: command });
|
|
171
|
+
return candidates;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (mode === "cursor-read") {
|
|
175
|
+
if (filePath) candidates.push({ kind: "Read", value: filePath });
|
|
176
|
+
return candidates;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (mode === "cursor-mcp") {
|
|
180
|
+
if (mcpName) candidates.push({ kind: "MCP", value: mcpName });
|
|
181
|
+
return candidates;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const normalizedTool = String(toolName || "").toLowerCase();
|
|
185
|
+
|
|
186
|
+
if (command && /(bash|shell|terminal|command)/i.test(normalizedTool)) {
|
|
187
|
+
candidates.push({ kind: "Bash", value: command });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (filePath && /(edit|write|applypatch|replace|multiedit)/i.test(normalizedTool)) {
|
|
191
|
+
candidates.push({ kind: "Edit", value: filePath });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (filePath && /(read|view|open)/i.test(normalizedTool)) {
|
|
195
|
+
candidates.push({ kind: "Read", value: filePath });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if ((normalizedTool.includes("mcp") || String(toolName || "").startsWith("mcp__")) && mcpName) {
|
|
199
|
+
candidates.push({ kind: "MCP", value: mcpName });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (skillName && /skill/i.test(normalizedTool)) {
|
|
203
|
+
candidates.push({ kind: "Skill", value: skillName });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return candidates;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function evaluate(mode, event) {
|
|
210
|
+
const candidates = inferCandidates(mode, event);
|
|
211
|
+
let selected;
|
|
212
|
+
|
|
213
|
+
for (const rule of RULES) {
|
|
214
|
+
for (const candidate of candidates) {
|
|
215
|
+
if (candidate.kind !== rule.kind) continue;
|
|
216
|
+
if (!matchesPattern(rule.pattern, candidate.value)) continue;
|
|
217
|
+
|
|
218
|
+
if (!selected || ACTION_PRIORITY[rule.action] > ACTION_PRIORITY[selected.action]) {
|
|
219
|
+
selected = { rule, action: rule.action, candidate };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return selected;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function cursorResponse(match) {
|
|
228
|
+
if (!match) return {};
|
|
229
|
+
return {
|
|
230
|
+
permission: match.action,
|
|
231
|
+
user_message: "Pluxx permissions matched " + match.rule.raw,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function claudeResponse(match) {
|
|
236
|
+
if (!match) return {};
|
|
237
|
+
return {
|
|
238
|
+
hookSpecificOutput: {
|
|
239
|
+
permissionDecision: match.action,
|
|
240
|
+
permissionDecisionReason: "Pluxx permissions matched " + match.rule.raw,
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function main() {
|
|
246
|
+
const mode = process.argv[2];
|
|
247
|
+
const event = await parseJsonInput();
|
|
248
|
+
const match = evaluate(mode, event);
|
|
249
|
+
const payload = mode === "claude-pretool" ? claudeResponse(match) : cursorResponse(match);
|
|
250
|
+
process.stdout.write(JSON.stringify(payload));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
main().catch((error) => {
|
|
254
|
+
process.stderr.write(String(error instanceof Error ? error.message : error));
|
|
255
|
+
process.exit(1);
|
|
256
|
+
});
|
|
257
|
+
`
|
|
258
|
+
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { PERMISSION_SELECTOR_KINDS, parsePermissionRule } from './permissions'
|
|
3
|
+
|
|
4
|
+
// ── MCP Server Auth ──────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const McpAuthNoneSchema = z.object({
|
|
7
|
+
type: z.literal('none'),
|
|
8
|
+
}).strict()
|
|
9
|
+
|
|
10
|
+
const McpAuthBearerSchema = z.object({
|
|
11
|
+
type: z.literal('bearer'),
|
|
12
|
+
envVar: z.string(),
|
|
13
|
+
headerName: z.string().default('Authorization'),
|
|
14
|
+
headerTemplate: z.string().default('Bearer ${value}'),
|
|
15
|
+
}).strict()
|
|
16
|
+
|
|
17
|
+
const McpAuthHeaderSchema = z.object({
|
|
18
|
+
type: z.literal('header'),
|
|
19
|
+
envVar: z.string(),
|
|
20
|
+
headerName: z.string(),
|
|
21
|
+
headerTemplate: z.string().default('Bearer ${value}'),
|
|
22
|
+
}).strict()
|
|
23
|
+
|
|
24
|
+
const McpAuthPlatformSchema = z.object({
|
|
25
|
+
type: z.literal('platform'),
|
|
26
|
+
mode: z.enum(['oauth']).default('oauth'),
|
|
27
|
+
}).strict()
|
|
28
|
+
|
|
29
|
+
export const McpAuthSchema = z.preprocess(
|
|
30
|
+
(value) => {
|
|
31
|
+
if (value && typeof value === 'object' && !('type' in (value as Record<string, unknown>))) {
|
|
32
|
+
return { ...(value as Record<string, unknown>), type: 'bearer' }
|
|
33
|
+
}
|
|
34
|
+
return value
|
|
35
|
+
},
|
|
36
|
+
z.discriminatedUnion('type', [McpAuthNoneSchema, McpAuthBearerSchema, McpAuthHeaderSchema, McpAuthPlatformSchema])
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const McpServerHttpSchema = z.object({
|
|
40
|
+
transport: z.literal('http'),
|
|
41
|
+
url: z.string().url(),
|
|
42
|
+
command: z.never().optional(),
|
|
43
|
+
args: z.array(z.string()).optional(),
|
|
44
|
+
env: z.record(z.string()).optional(),
|
|
45
|
+
auth: McpAuthSchema.optional(),
|
|
46
|
+
}).strict()
|
|
47
|
+
|
|
48
|
+
const McpServerSseSchema = z.object({
|
|
49
|
+
transport: z.literal('sse'),
|
|
50
|
+
url: z.string().url(),
|
|
51
|
+
command: z.never().optional(),
|
|
52
|
+
args: z.array(z.string()).optional(),
|
|
53
|
+
env: z.record(z.string()).optional(),
|
|
54
|
+
auth: McpAuthSchema.optional(),
|
|
55
|
+
}).strict()
|
|
56
|
+
|
|
57
|
+
const McpServerStdioSchema = z.object({
|
|
58
|
+
transport: z.literal('stdio'),
|
|
59
|
+
command: z.string(),
|
|
60
|
+
url: z.never().optional(),
|
|
61
|
+
args: z.array(z.string()).optional(),
|
|
62
|
+
env: z.record(z.string()).optional(),
|
|
63
|
+
auth: McpAuthSchema.optional(),
|
|
64
|
+
}).strict()
|
|
65
|
+
|
|
66
|
+
export const McpServerSchema = z.preprocess(
|
|
67
|
+
(value) => {
|
|
68
|
+
if (value && typeof value === 'object' && !('transport' in (value as Record<string, unknown>))) {
|
|
69
|
+
return { ...(value as Record<string, unknown>), transport: 'http' }
|
|
70
|
+
}
|
|
71
|
+
return value
|
|
72
|
+
},
|
|
73
|
+
z.discriminatedUnion('transport', [McpServerHttpSchema, McpServerSseSchema, McpServerStdioSchema])
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// ── Hooks ────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export const HookEntrySchema = z.object({
|
|
79
|
+
type: z.enum(['command', 'prompt']).default('command'),
|
|
80
|
+
command: z.string().optional(),
|
|
81
|
+
prompt: z.string().optional(),
|
|
82
|
+
model: z.string().optional(),
|
|
83
|
+
timeout: z.number().optional(),
|
|
84
|
+
matcher: z.union([z.string(), z.record(z.unknown())]).optional(),
|
|
85
|
+
failClosed: z.boolean().optional(),
|
|
86
|
+
loop_limit: z.number().nullable().optional(),
|
|
87
|
+
}).superRefine((entry, ctx) => {
|
|
88
|
+
if (entry.type === 'command' && !entry.command) {
|
|
89
|
+
ctx.addIssue({
|
|
90
|
+
code: z.ZodIssueCode.custom,
|
|
91
|
+
path: ['command'],
|
|
92
|
+
message: 'Command hooks require a command.',
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (entry.type === 'prompt' && !entry.prompt) {
|
|
97
|
+
ctx.addIssue({
|
|
98
|
+
code: z.ZodIssueCode.custom,
|
|
99
|
+
path: ['prompt'],
|
|
100
|
+
message: 'Prompt hooks require a prompt.',
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
export const HooksSchema = z.object({
|
|
106
|
+
sessionStart: z.array(HookEntrySchema).optional(),
|
|
107
|
+
sessionEnd: z.array(HookEntrySchema).optional(),
|
|
108
|
+
preToolUse: z.array(HookEntrySchema).optional(),
|
|
109
|
+
postToolUse: z.array(HookEntrySchema).optional(),
|
|
110
|
+
postToolUseFailure: z.array(HookEntrySchema).optional(),
|
|
111
|
+
subagentStart: z.array(HookEntrySchema).optional(),
|
|
112
|
+
subagentStop: z.array(HookEntrySchema).optional(),
|
|
113
|
+
beforeShellExecution: z.array(HookEntrySchema).optional(),
|
|
114
|
+
afterShellExecution: z.array(HookEntrySchema).optional(),
|
|
115
|
+
beforeMCPExecution: z.array(HookEntrySchema).optional(),
|
|
116
|
+
afterMCPExecution: z.array(HookEntrySchema).optional(),
|
|
117
|
+
afterFileEdit: z.array(HookEntrySchema).optional(),
|
|
118
|
+
beforeReadFile: z.array(HookEntrySchema).optional(),
|
|
119
|
+
beforeSubmitPrompt: z.array(HookEntrySchema).optional(),
|
|
120
|
+
preCompact: z.array(HookEntrySchema).optional(),
|
|
121
|
+
stop: z.array(HookEntrySchema).optional(),
|
|
122
|
+
afterAgentResponse: z.array(HookEntrySchema).optional(),
|
|
123
|
+
afterAgentThought: z.array(HookEntrySchema).optional(),
|
|
124
|
+
beforeTabFileRead: z.array(HookEntrySchema).optional(),
|
|
125
|
+
afterTabFileEdit: z.array(HookEntrySchema).optional(),
|
|
126
|
+
}).catchall(z.array(HookEntrySchema))
|
|
127
|
+
|
|
128
|
+
// ── Brand / Interface ────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
export const BrandSchema = z.object({
|
|
131
|
+
displayName: z.string(),
|
|
132
|
+
shortDescription: z.string().optional(),
|
|
133
|
+
longDescription: z.string().optional(),
|
|
134
|
+
category: z.string().default('Productivity'),
|
|
135
|
+
color: z.string().optional(),
|
|
136
|
+
icon: z.string().optional(),
|
|
137
|
+
logo: z.string().optional(),
|
|
138
|
+
screenshots: z.array(z.string()).optional(),
|
|
139
|
+
defaultPrompts: z.array(z.string()).optional(),
|
|
140
|
+
websiteURL: z.string().url().optional(),
|
|
141
|
+
privacyPolicyURL: z.string().url().optional(),
|
|
142
|
+
termsOfServiceURL: z.string().url().optional(),
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// ── Targets / User Config ───────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
export const TargetPlatform = z.enum([
|
|
148
|
+
'claude-code',
|
|
149
|
+
'cursor',
|
|
150
|
+
'codex',
|
|
151
|
+
'opencode',
|
|
152
|
+
'github-copilot',
|
|
153
|
+
'openhands',
|
|
154
|
+
'warp',
|
|
155
|
+
'gemini-cli',
|
|
156
|
+
'roo-code',
|
|
157
|
+
'cline',
|
|
158
|
+
'amp',
|
|
159
|
+
])
|
|
160
|
+
export type TargetPlatform = z.infer<typeof TargetPlatform>
|
|
161
|
+
|
|
162
|
+
const UserConfigValueSchema = z.union([z.string(), z.number(), z.boolean()])
|
|
163
|
+
|
|
164
|
+
export const UserConfigEntrySchema = z.object({
|
|
165
|
+
key: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Use lowercase kebab-case for user config keys'),
|
|
166
|
+
title: z.string(),
|
|
167
|
+
description: z.string(),
|
|
168
|
+
type: z.enum(['secret', 'string', 'number', 'boolean']).default('string'),
|
|
169
|
+
required: z.boolean().default(true),
|
|
170
|
+
envVar: z.string().optional(),
|
|
171
|
+
defaultValue: UserConfigValueSchema.optional(),
|
|
172
|
+
placeholder: z.string().optional(),
|
|
173
|
+
targets: z.array(TargetPlatform).optional(),
|
|
174
|
+
}).superRefine((entry, ctx) => {
|
|
175
|
+
if (entry.type === 'secret' && entry.defaultValue !== undefined) {
|
|
176
|
+
ctx.addIssue({
|
|
177
|
+
code: z.ZodIssueCode.custom,
|
|
178
|
+
path: ['defaultValue'],
|
|
179
|
+
message: 'Secret userConfig entries should not define a defaultValue.',
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (entry.type === 'number' && entry.defaultValue !== undefined && typeof entry.defaultValue !== 'number') {
|
|
184
|
+
ctx.addIssue({
|
|
185
|
+
code: z.ZodIssueCode.custom,
|
|
186
|
+
path: ['defaultValue'],
|
|
187
|
+
message: 'Number userConfig entries require a numeric defaultValue.',
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (entry.type === 'boolean' && entry.defaultValue !== undefined && typeof entry.defaultValue !== 'boolean') {
|
|
192
|
+
ctx.addIssue({
|
|
193
|
+
code: z.ZodIssueCode.custom,
|
|
194
|
+
path: ['defaultValue'],
|
|
195
|
+
message: 'Boolean userConfig entries require a boolean defaultValue.',
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
export const UserConfigSchema = z.array(UserConfigEntrySchema)
|
|
201
|
+
|
|
202
|
+
// ── Permissions ─────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
export const PermissionRuleSchema = z.string().superRefine((value, ctx) => {
|
|
205
|
+
if (!parsePermissionRule(value)) {
|
|
206
|
+
ctx.addIssue({
|
|
207
|
+
code: z.ZodIssueCode.custom,
|
|
208
|
+
message: `Permission rules must use the canonical DSL: ${PERMISSION_SELECTOR_KINDS.map(kind => `${kind}(...)`).join(', ')}`,
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
export const PermissionsSchema = z.object({
|
|
214
|
+
allow: z.array(PermissionRuleSchema).optional(),
|
|
215
|
+
ask: z.array(PermissionRuleSchema).optional(),
|
|
216
|
+
deny: z.array(PermissionRuleSchema).optional(),
|
|
217
|
+
}).refine(
|
|
218
|
+
permissions => (permissions.allow?.length ?? 0) + (permissions.ask?.length ?? 0) + (permissions.deny?.length ?? 0) > 0,
|
|
219
|
+
{
|
|
220
|
+
message: 'Permissions must declare at least one allow/ask/deny rule.',
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
// ── Platform Overrides ───────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
export const ClaudeCodeOverridesSchema = z.object({
|
|
227
|
+
skillDefaults: z.record(z.unknown()).optional(),
|
|
228
|
+
mcpAuth: z.enum(['inline', 'platform']).optional(),
|
|
229
|
+
}).catchall(z.unknown())
|
|
230
|
+
|
|
231
|
+
export const CursorOverridesSchema = z.object({
|
|
232
|
+
mcpAuth: z.enum(['inline', 'platform']).optional(),
|
|
233
|
+
rules: z.array(z.object({
|
|
234
|
+
description: z.string(),
|
|
235
|
+
globs: z.union([z.string(), z.array(z.string())]).optional(),
|
|
236
|
+
alwaysApply: z.boolean().optional(),
|
|
237
|
+
content: z.string().optional(),
|
|
238
|
+
})).optional(),
|
|
239
|
+
}).catchall(z.unknown())
|
|
240
|
+
|
|
241
|
+
export const CodexOverridesSchema = z.object({
|
|
242
|
+
interface: z.record(z.unknown()).optional(),
|
|
243
|
+
}).catchall(z.unknown())
|
|
244
|
+
|
|
245
|
+
export const OpenCodeOverridesSchema = z.object({
|
|
246
|
+
npmPackage: z.string().optional(),
|
|
247
|
+
}).catchall(z.unknown())
|
|
248
|
+
|
|
249
|
+
export const PlatformOverridesSchema = z.object({
|
|
250
|
+
'claude-code': ClaudeCodeOverridesSchema.optional(),
|
|
251
|
+
cursor: CursorOverridesSchema.optional(),
|
|
252
|
+
codex: CodexOverridesSchema.optional(),
|
|
253
|
+
opencode: OpenCodeOverridesSchema.optional(),
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// ── Main Plugin Config ───────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
export const PluginConfigSchema = z.object({
|
|
259
|
+
// Identity
|
|
260
|
+
name: z.string().regex(/^[a-z0-9-]+$/, 'Must be lowercase with hyphens only'),
|
|
261
|
+
version: z.string().default('0.1.0'),
|
|
262
|
+
description: z.string(),
|
|
263
|
+
author: z.object({
|
|
264
|
+
name: z.string(),
|
|
265
|
+
url: z.string().url().optional(),
|
|
266
|
+
email: z.string().email().optional(),
|
|
267
|
+
}),
|
|
268
|
+
repository: z.string().optional(),
|
|
269
|
+
license: z.string().default('MIT'),
|
|
270
|
+
keywords: z.array(z.string()).optional(),
|
|
271
|
+
|
|
272
|
+
// Brand
|
|
273
|
+
brand: BrandSchema.optional(),
|
|
274
|
+
userConfig: UserConfigSchema.optional(),
|
|
275
|
+
permissions: PermissionsSchema.optional(),
|
|
276
|
+
|
|
277
|
+
// Plugin components (paths relative to config file)
|
|
278
|
+
skills: z.string().default('./skills/'),
|
|
279
|
+
commands: z.string().optional(),
|
|
280
|
+
agents: z.string().optional(),
|
|
281
|
+
instructions: z.string().optional(),
|
|
282
|
+
|
|
283
|
+
// MCP servers
|
|
284
|
+
mcp: z.record(McpServerSchema).optional(),
|
|
285
|
+
|
|
286
|
+
// Hooks
|
|
287
|
+
hooks: HooksSchema.optional(),
|
|
288
|
+
|
|
289
|
+
// Scripts (copied to all targets)
|
|
290
|
+
scripts: z.string().optional(),
|
|
291
|
+
|
|
292
|
+
// Assets (copied to all targets)
|
|
293
|
+
assets: z.string().optional(),
|
|
294
|
+
|
|
295
|
+
// Platform-specific overrides
|
|
296
|
+
platforms: PlatformOverridesSchema.optional(),
|
|
297
|
+
|
|
298
|
+
// Which platforms to generate
|
|
299
|
+
targets: z.array(TargetPlatform).default(['claude-code', 'cursor', 'codex', 'opencode']),
|
|
300
|
+
|
|
301
|
+
// Output directory
|
|
302
|
+
outDir: z.string().default('./dist'),
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
export type PluginConfig = z.infer<typeof PluginConfigSchema>
|
|
306
|
+
export type McpServer = z.infer<typeof McpServerSchema>
|
|
307
|
+
export type McpAuth = z.infer<typeof McpAuthSchema>
|
|
308
|
+
export type HookEntry = z.infer<typeof HookEntrySchema>
|
|
309
|
+
export type Brand = z.infer<typeof BrandSchema>
|
|
310
|
+
export type UserConfigEntry = z.infer<typeof UserConfigEntrySchema>
|
|
311
|
+
export type PermissionRule = z.infer<typeof PermissionRuleSchema>
|
|
312
|
+
export type Permissions = z.infer<typeof PermissionsSchema>
|