@orchid-labs/pluxx 0.1.0 → 0.1.3

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 (110) hide show
  1. package/README.md +110 -515
  2. package/bin/pluxx.js +19 -28
  3. package/dist/agents.d.ts +16 -0
  4. package/dist/agents.d.ts.map +1 -0
  5. package/dist/cli/agent.d.ts +69 -0
  6. package/dist/cli/agent.d.ts.map +1 -1
  7. package/dist/cli/doctor.d.ts +3 -0
  8. package/dist/cli/doctor.d.ts.map +1 -1
  9. package/dist/cli/entry.d.ts +2 -0
  10. package/dist/cli/entry.d.ts.map +1 -0
  11. package/dist/cli/eval.d.ts +22 -0
  12. package/dist/cli/eval.d.ts.map +1 -0
  13. package/dist/cli/index.d.ts +26 -3
  14. package/dist/cli/index.d.ts.map +1 -1
  15. package/dist/cli/index.js +21810 -0
  16. package/dist/cli/init-from-mcp.d.ts +34 -3
  17. package/dist/cli/init-from-mcp.d.ts.map +1 -1
  18. package/dist/cli/install.d.ts +3 -0
  19. package/dist/cli/install.d.ts.map +1 -1
  20. package/dist/cli/lint.d.ts +7 -1
  21. package/dist/cli/lint.d.ts.map +1 -1
  22. package/dist/cli/mcp-proxy.d.ts +10 -0
  23. package/dist/cli/mcp-proxy.d.ts.map +1 -0
  24. package/dist/cli/migrate.d.ts.map +1 -1
  25. package/dist/cli/primitive-summary.d.ts +14 -0
  26. package/dist/cli/primitive-summary.d.ts.map +1 -0
  27. package/dist/cli/prompt.d.ts +1 -1
  28. package/dist/cli/publish.d.ts +6 -1
  29. package/dist/cli/publish.d.ts.map +1 -1
  30. package/dist/cli/sync-from-mcp.d.ts.map +1 -1
  31. package/dist/cli/test.d.ts +2 -0
  32. package/dist/cli/test.d.ts.map +1 -1
  33. package/dist/cli/verify-install.d.ts +25 -0
  34. package/dist/cli/verify-install.d.ts.map +1 -0
  35. package/dist/commands.d.ts +10 -0
  36. package/dist/commands.d.ts.map +1 -0
  37. package/dist/compiler-intent.d.ts +165 -0
  38. package/dist/compiler-intent.d.ts.map +1 -0
  39. package/dist/config/load.d.ts.map +1 -1
  40. package/dist/delegation.d.ts +11 -0
  41. package/dist/delegation.d.ts.map +1 -0
  42. package/dist/generators/amp/index.d.ts.map +1 -1
  43. package/dist/generators/base.d.ts +5 -0
  44. package/dist/generators/base.d.ts.map +1 -1
  45. package/dist/generators/claude-code/index.d.ts +2 -0
  46. package/dist/generators/claude-code/index.d.ts.map +1 -1
  47. package/dist/generators/cline/index.d.ts.map +1 -1
  48. package/dist/generators/codex/index.d.ts +5 -0
  49. package/dist/generators/codex/index.d.ts.map +1 -1
  50. package/dist/generators/cursor/index.d.ts +1 -0
  51. package/dist/generators/cursor/index.d.ts.map +1 -1
  52. package/dist/generators/gemini-cli/index.d.ts.map +1 -1
  53. package/dist/generators/github-copilot/index.d.ts.map +1 -1
  54. package/dist/generators/opencode/index.d.ts +1 -0
  55. package/dist/generators/opencode/index.d.ts.map +1 -1
  56. package/dist/generators/openhands/index.d.ts.map +1 -1
  57. package/dist/generators/roo-code/index.d.ts.map +1 -1
  58. package/dist/generators/shared/claude-family.d.ts.map +1 -1
  59. package/dist/generators/warp/index.d.ts.map +1 -1
  60. package/dist/index.d.ts +4 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +5464 -548
  63. package/dist/mcp/introspect.d.ts +43 -1
  64. package/dist/mcp/introspect.d.ts.map +1 -1
  65. package/dist/permissions.d.ts.map +1 -1
  66. package/dist/schema.d.ts +91 -42
  67. package/dist/schema.d.ts.map +1 -1
  68. package/dist/text-files.d.ts +5 -0
  69. package/dist/text-files.d.ts.map +1 -0
  70. package/dist/validation/platform-rules.d.ts +35 -1
  71. package/dist/validation/platform-rules.d.ts.map +1 -1
  72. package/package.json +16 -14
  73. package/src/cli/agent.ts +0 -1030
  74. package/src/cli/dev.ts +0 -112
  75. package/src/cli/doctor.ts +0 -588
  76. package/src/cli/index.ts +0 -2414
  77. package/src/cli/init-from-mcp.ts +0 -1611
  78. package/src/cli/install.ts +0 -698
  79. package/src/cli/lint.ts +0 -1219
  80. package/src/cli/migrate.ts +0 -614
  81. package/src/cli/prompt.ts +0 -82
  82. package/src/cli/publish.ts +0 -401
  83. package/src/cli/runtime.ts +0 -86
  84. package/src/cli/sync-from-mcp.ts +0 -563
  85. package/src/cli/test.ts +0 -134
  86. package/src/compatibility/matrix.ts +0 -149
  87. package/src/config/define.ts +0 -20
  88. package/src/config/load.ts +0 -74
  89. package/src/generators/amp/index.ts +0 -63
  90. package/src/generators/base.ts +0 -188
  91. package/src/generators/claude-code/index.ts +0 -29
  92. package/src/generators/cline/index.ts +0 -35
  93. package/src/generators/codex/index.ts +0 -120
  94. package/src/generators/cursor/index.ts +0 -158
  95. package/src/generators/gemini-cli/index.ts +0 -83
  96. package/src/generators/github-copilot/index.ts +0 -32
  97. package/src/generators/hooks-warning.ts +0 -51
  98. package/src/generators/index.ts +0 -71
  99. package/src/generators/opencode/index.ts +0 -526
  100. package/src/generators/openhands/index.ts +0 -32
  101. package/src/generators/roo-code/index.ts +0 -35
  102. package/src/generators/shared/claude-family.ts +0 -215
  103. package/src/generators/warp/index.ts +0 -32
  104. package/src/hook-events.ts +0 -33
  105. package/src/index.ts +0 -23
  106. package/src/mcp/introspect.ts +0 -834
  107. package/src/permissions.ts +0 -258
  108. package/src/schema.ts +0 -312
  109. package/src/user-config.ts +0 -177
  110. package/src/validation/platform-rules.ts +0 -565
package/src/cli/agent.ts DELETED
@@ -1,1030 +0,0 @@
1
- import { existsSync } from 'fs'
2
- import { mkdir } from 'fs/promises'
3
- import { resolve } from 'path'
4
- import { spawn } from 'child_process'
5
- import { loadConfig } from '../config/load'
6
- import { lintProject, type LintResult } from './lint'
7
- import { runTestSuite, type TestRunResult } from './test'
8
- import {
9
- MCP_SCAFFOLD_METADATA_PATH,
10
- MCP_TAXONOMY_PATH,
11
- PLUXX_CUSTOM_END,
12
- PLUXX_CUSTOM_START,
13
- PLUXX_GENERATED_END,
14
- PLUXX_GENERATED_START,
15
- type McpScaffoldMetadata,
16
- } from './init-from-mcp'
17
- import { applyPersistedTaxonomy } from './sync-from-mcp'
18
-
19
- export const AGENT_CONTEXT_PATH = '.pluxx/agent/context.md'
20
- export const AGENT_PLAN_PATH = '.pluxx/agent/plan.json'
21
- export const AGENT_OVERRIDES_PATH = 'pluxx.agent.md'
22
- export const AGENT_PROMPT_KINDS = ['taxonomy', 'instructions', 'review'] as const
23
- export const AGENT_RUNNERS = ['claude', 'opencode', 'codex', 'cursor'] as const
24
- export type AgentPromptKind = typeof AGENT_PROMPT_KINDS[number]
25
- export type AgentRunner = typeof AGENT_RUNNERS[number]
26
-
27
- const AGENT_PROMPT_PATHS: Record<AgentPromptKind, string> = {
28
- taxonomy: '.pluxx/agent/taxonomy-prompt.md',
29
- instructions: '.pluxx/agent/instructions-prompt.md',
30
- review: '.pluxx/agent/review-prompt.md',
31
- }
32
-
33
- const AGENT_RUNNER_BINARIES: Record<AgentRunner, string> = {
34
- claude: 'claude',
35
- opencode: 'opencode',
36
- codex: 'codex',
37
- cursor: 'agent',
38
- }
39
-
40
- export interface AgentPreparePlannedFile {
41
- relativePath: string
42
- content: string
43
- action: 'create' | 'update' | 'unchanged'
44
- }
45
-
46
- export interface AgentPrepareSummary {
47
- pluginName: string
48
- targetCount: number
49
- toolCount: number
50
- skillCount: number
51
- editableFiles: string[]
52
- protectedFiles: string[]
53
- generatedFiles: string[]
54
- createdFiles: string[]
55
- updatedFiles: string[]
56
- lint: {
57
- errors: number
58
- warnings: number
59
- }
60
- contextInputs: string[]
61
- dryRun?: boolean
62
- }
63
-
64
- export interface AgentPreparePlan extends AgentPrepareSummary {
65
- files: AgentPreparePlannedFile[]
66
- }
67
-
68
- export interface AgentPrepareOptions {
69
- docsUrl?: string
70
- websiteUrl?: string
71
- contextPaths?: string[]
72
- }
73
-
74
- interface AgentPromptOptions {
75
- allowMissingContext?: boolean
76
- }
77
-
78
- export interface AgentPromptSummary {
79
- pluginName: string
80
- kind: AgentPromptKind
81
- outputPath: string
82
- createdFiles: string[]
83
- updatedFiles: string[]
84
- dryRun?: boolean
85
- }
86
-
87
- export interface AgentPromptPlan extends AgentPromptSummary {
88
- files: AgentPreparePlannedFile[]
89
- }
90
-
91
- export interface AgentRunOptions {
92
- runner: AgentRunner
93
- model?: string
94
- attach?: string
95
- verify?: boolean
96
- }
97
-
98
- export interface AgentRunSummary {
99
- pluginName: string
100
- kind: AgentPromptKind
101
- runner: AgentRunner
102
- verify: boolean
103
- command: string[]
104
- commandDisplay: string
105
- promptPath: string
106
- contextPath: string
107
- createdFiles: string[]
108
- updatedFiles: string[]
109
- contextInputs: string[]
110
- dryRun?: boolean
111
- }
112
-
113
- export interface AgentRunPlan extends AgentRunSummary {
114
- files: AgentPreparePlannedFile[]
115
- }
116
-
117
- export interface AgentRunResult extends AgentRunSummary {
118
- ok: boolean
119
- runnerExitCode: number
120
- verification?: TestRunResult
121
- }
122
-
123
- interface AgentPlanFile {
124
- path: string
125
- managedSections?: Array<{
126
- start: string
127
- end: string
128
- }>
129
- }
130
-
131
- interface AgentModePlanFile {
132
- version: 1
133
- plugin: {
134
- name: string
135
- displayName: string
136
- targets: string[]
137
- }
138
- mcp: {
139
- metadataPath: string
140
- toolCount: number
141
- serverName: string
142
- transport: string
143
- auth: string
144
- }
145
- contextInputs: string[]
146
- files: {
147
- editable: AgentPlanFile[]
148
- protected: string[]
149
- generated: string[]
150
- }
151
- successCriteria: string[]
152
- caveats: string[]
153
- }
154
-
155
- interface AgentContextSource {
156
- label: string
157
- kind: 'website' | 'docs' | 'file'
158
- summary: string
159
- }
160
-
161
- interface AgentOverrides {
162
- path: string
163
- contextPaths: string[]
164
- productHints?: string
165
- setupAuthNotes?: string
166
- groupingHints?: string
167
- taxonomyGuidance?: string
168
- instructionsGuidance?: string
169
- reviewCriteria?: string
170
- }
171
-
172
- function hasManagedCommands(metadata: McpScaffoldMetadata): boolean {
173
- return metadata.managedFiles.some((file) => file.startsWith('commands/'))
174
- }
175
-
176
- export async function planAgentPrepare(
177
- rootDir: string = process.cwd(),
178
- options: AgentPrepareOptions = {},
179
- ): Promise<AgentPreparePlan> {
180
- const config = await loadConfig(rootDir)
181
- const metadata = await loadMcpScaffoldMetadata(rootDir)
182
- const lint = await lintProject(rootDir)
183
- const overrides = await loadAgentOverrides(rootDir)
184
- const contextSources = await collectAgentContextSources(rootDir, options, overrides)
185
- const editableFiles = buildEditableFiles(metadata)
186
- const protectedFiles = buildProtectedFiles()
187
- const generatedFiles = [AGENT_CONTEXT_PATH, AGENT_PLAN_PATH]
188
- const contextContent = buildAgentContext(config, metadata, lint, contextSources, overrides)
189
- const planContent = buildAgentModePlanJson(config, metadata, lint, editableFiles, protectedFiles, generatedFiles, contextSources)
190
-
191
- const files = await Promise.all([
192
- planFile(rootDir, AGENT_CONTEXT_PATH, contextContent),
193
- planFile(rootDir, AGENT_PLAN_PATH, `${planContent}\n`),
194
- ])
195
-
196
- return {
197
- pluginName: config.name,
198
- targetCount: config.targets.length,
199
- toolCount: metadata.tools.length,
200
- skillCount: metadata.skills.length,
201
- editableFiles: editableFiles.map((file) => file.path),
202
- protectedFiles,
203
- generatedFiles,
204
- createdFiles: files.filter((file) => file.action === 'create').map((file) => file.relativePath),
205
- updatedFiles: files.filter((file) => file.action === 'update').map((file) => file.relativePath),
206
- lint: {
207
- errors: lint.errors,
208
- warnings: lint.warnings,
209
- },
210
- contextInputs: contextSources.map((source) => source.label),
211
- files,
212
- }
213
- }
214
-
215
- export async function applyAgentPreparePlan(rootDir: string, plan: AgentPreparePlan): Promise<void> {
216
- for (const file of plan.files) {
217
- const filePath = resolve(rootDir, file.relativePath)
218
- const parentDir = file.relativePath.split('/').slice(0, -1).join('/')
219
- if (parentDir) {
220
- await mkdir(resolve(rootDir, parentDir), { recursive: true })
221
- }
222
- await Bun.write(filePath, file.content)
223
- }
224
- }
225
-
226
- export async function planAgentPrompt(
227
- rootDir: string,
228
- kind: AgentPromptKind,
229
- options: AgentPromptOptions = {},
230
- ): Promise<AgentPromptPlan> {
231
- const config = await loadConfig(rootDir)
232
- const metadata = await loadMcpScaffoldMetadata(rootDir)
233
- const overrides = await loadAgentOverrides(rootDir)
234
- const contextPath = resolve(rootDir, AGENT_CONTEXT_PATH)
235
-
236
- if (!options.allowMissingContext && !existsSync(contextPath)) {
237
- throw new Error(`No agent context found at ${AGENT_CONTEXT_PATH}. Run "pluxx agent prepare" first.`)
238
- }
239
-
240
- const outputPath = AGENT_PROMPT_PATHS[kind]
241
- const content = buildAgentPrompt(kind, {
242
- pluginName: config.name,
243
- displayName: config.brand?.displayName ?? metadata.settings.displayName ?? config.name,
244
- skillPaths: metadata.skills.map((skill) => `skills/${skill.dirName}/SKILL.md`),
245
- commandPaths: hasManagedCommands(metadata)
246
- ? metadata.skills.map((skill) => `commands/${skill.dirName}.md`)
247
- : [],
248
- overrides,
249
- })
250
- const file = await planFile(rootDir, outputPath, content)
251
-
252
- return {
253
- pluginName: config.name,
254
- kind,
255
- outputPath,
256
- createdFiles: file.action === 'create' ? [outputPath] : [],
257
- updatedFiles: file.action === 'update' ? [outputPath] : [],
258
- files: [file],
259
- }
260
- }
261
-
262
- export async function applyAgentPromptPlan(rootDir: string, plan: AgentPromptPlan): Promise<void> {
263
- for (const file of plan.files) {
264
- const filePath = resolve(rootDir, file.relativePath)
265
- const parentDir = file.relativePath.split('/').slice(0, -1).join('/')
266
- if (parentDir) {
267
- await mkdir(resolve(rootDir, parentDir), { recursive: true })
268
- }
269
- await Bun.write(filePath, file.content)
270
- }
271
- }
272
-
273
- export async function planAgentRun(
274
- rootDir: string = process.cwd(),
275
- kind: AgentPromptKind,
276
- options: AgentRunOptions,
277
- prepareOptions: AgentPrepareOptions = {},
278
- ): Promise<AgentRunPlan> {
279
- if (options.runner !== 'opencode' && options.attach) {
280
- throw new Error('--attach is only supported for the opencode runner.')
281
- }
282
-
283
- if (options.runner === 'codex' && options.model) {
284
- throw new Error('--model is not yet supported for the codex runner in Pluxx. Use the default Codex CLI model selection for now.')
285
- }
286
-
287
- const preparePlan = await planAgentPrepare(rootDir, prepareOptions)
288
- const promptPlan = await planAgentPrompt(rootDir, kind, { allowMissingContext: true })
289
- const promptPath = AGENT_PROMPT_PATHS[kind]
290
- const verify = kind === 'review' ? false : options.verify !== false
291
- const command = buildAgentRunnerCommand(options.runner, kind, buildAgentRunnerPrompt(kind, promptPath), {
292
- model: options.model,
293
- attach: options.attach,
294
- workspace: rootDir,
295
- })
296
-
297
- return {
298
- pluginName: preparePlan.pluginName,
299
- kind,
300
- runner: options.runner,
301
- verify,
302
- command,
303
- commandDisplay: command.map(shellQuote).join(' '),
304
- promptPath,
305
- contextPath: AGENT_CONTEXT_PATH,
306
- createdFiles: [...preparePlan.createdFiles, ...promptPlan.createdFiles],
307
- updatedFiles: [...preparePlan.updatedFiles, ...promptPlan.updatedFiles],
308
- contextInputs: preparePlan.contextInputs,
309
- files: [...preparePlan.files, ...promptPlan.files],
310
- }
311
- }
312
-
313
- export async function runAgentPlan(
314
- rootDir: string,
315
- plan: AgentRunPlan,
316
- options: {
317
- streamOutput?: boolean
318
- } = {},
319
- ): Promise<AgentRunResult> {
320
- for (const file of plan.files) {
321
- const filePath = resolve(rootDir, file.relativePath)
322
- const parentDir = file.relativePath.split('/').slice(0, -1).join('/')
323
- if (parentDir) {
324
- await mkdir(resolve(rootDir, parentDir), { recursive: true })
325
- }
326
- await Bun.write(filePath, file.content)
327
- }
328
-
329
- await ensureRunnerAvailable(plan.runner)
330
- await ensureRunnerAuthenticated(plan.runner)
331
- const runnerExitCode = await executeCommand(plan.command, rootDir, {
332
- streamOutput: options.streamOutput === true,
333
- })
334
- if (runnerExitCode === 0 && plan.kind === 'taxonomy') {
335
- await applyPersistedTaxonomy(rootDir)
336
- }
337
- const verification = runnerExitCode === 0 && plan.verify
338
- ? await runTestSuite({ rootDir })
339
- : undefined
340
-
341
- return {
342
- ...plan,
343
- ok: runnerExitCode === 0 && (verification?.ok ?? true),
344
- runnerExitCode,
345
- verification,
346
- }
347
- }
348
-
349
- function buildEditableFiles(metadata: McpScaffoldMetadata): AgentPlanFile[] {
350
- const files: AgentPlanFile[] = [{
351
- path: MCP_TAXONOMY_PATH,
352
- }, {
353
- path: 'INSTRUCTIONS.md',
354
- managedSections: [{ start: PLUXX_GENERATED_START, end: PLUXX_GENERATED_END }],
355
- }]
356
-
357
- for (const skill of metadata.skills) {
358
- files.push({
359
- path: `skills/${skill.dirName}/SKILL.md`,
360
- managedSections: [{ start: PLUXX_GENERATED_START, end: PLUXX_GENERATED_END }],
361
- })
362
- }
363
-
364
- if (hasManagedCommands(metadata)) {
365
- for (const skill of metadata.skills) {
366
- files.push({
367
- path: `commands/${skill.dirName}.md`,
368
- managedSections: [{ start: PLUXX_GENERATED_START, end: PLUXX_GENERATED_END }],
369
- })
370
- }
371
- }
372
-
373
- return files
374
- }
375
-
376
- function buildProtectedFiles(): string[] {
377
- return [
378
- 'pluxx.config.ts',
379
- 'pluxx.config.js',
380
- 'pluxx.config.json',
381
- AGENT_OVERRIDES_PATH,
382
- MCP_SCAFFOLD_METADATA_PATH,
383
- 'dist/',
384
- ]
385
- }
386
-
387
- async function loadMcpScaffoldMetadata(rootDir: string): Promise<McpScaffoldMetadata> {
388
- const metadataPath = resolve(rootDir, MCP_SCAFFOLD_METADATA_PATH)
389
- if (!existsSync(metadataPath)) {
390
- throw new Error(`No MCP scaffold metadata found at ${MCP_SCAFFOLD_METADATA_PATH}. Run "pluxx init --from-mcp" first.`)
391
- }
392
-
393
- try {
394
- const text = await Bun.file(metadataPath).text()
395
- return JSON.parse(text) as McpScaffoldMetadata
396
- } catch (error) {
397
- throw new Error(
398
- `Failed to parse ${MCP_SCAFFOLD_METADATA_PATH}: ${error instanceof Error ? error.message : String(error)}`,
399
- )
400
- }
401
- }
402
-
403
- async function planFile(rootDir: string, relativePath: string, content: string): Promise<AgentPreparePlannedFile> {
404
- const filePath = resolve(rootDir, relativePath)
405
- const action = existsSync(filePath)
406
- ? ((await Bun.file(filePath).text()) === content ? 'unchanged' : 'update')
407
- : 'create'
408
- return { relativePath, content, action }
409
- }
410
-
411
- function buildAgentContext(
412
- config: Awaited<ReturnType<typeof loadConfig>>,
413
- metadata: McpScaffoldMetadata,
414
- lint: LintResult,
415
- contextSources: AgentContextSource[],
416
- overrides: AgentOverrides | null,
417
- ): string {
418
- const serverEntry = Object.entries(config.mcp ?? {})[0]
419
- const [serverName, server] = serverEntry ?? ['unknown', undefined]
420
- const displayName = config.brand?.displayName ?? metadata.settings.displayName ?? config.name
421
- const lines = [
422
- '# Pluxx Agent Context',
423
- '',
424
- '## Plugin',
425
- '',
426
- `- Name: \`${config.name}\``,
427
- `- Display name: ${displayName}`,
428
- `- Targets: ${config.targets.join(', ')}`,
429
- '',
430
- '## MCP',
431
- '',
432
- `- Metadata source: \`${MCP_SCAFFOLD_METADATA_PATH}\``,
433
- `- Semantic taxonomy: \`${MCP_TAXONOMY_PATH}\``,
434
- `- Server name: \`${serverName}\``,
435
- `- Transport: ${server?.transport ?? metadata.source.transport}`,
436
- `- Auth: ${describeAuth(server ?? metadata.source)}`,
437
- `- Tool count: ${metadata.tools.length}`,
438
- '',
439
- '## Generated Skills',
440
- '',
441
- ]
442
-
443
- for (const skill of metadata.skills) {
444
- lines.push(`### \`${skill.dirName}\``)
445
- lines.push('')
446
- lines.push(`- Title: ${skill.title}`)
447
- lines.push(`- Tools: ${skill.toolNames.join(', ') || 'none'}`)
448
- lines.push('')
449
- }
450
-
451
- lines.push('## Lint Snapshot')
452
- lines.push('')
453
- lines.push(`- Errors: ${lint.errors}`)
454
- lines.push(`- Warnings: ${lint.warnings}`)
455
- lines.push('')
456
-
457
- if (lint.issues.length > 0) {
458
- lines.push('### Current Issues')
459
- lines.push('')
460
- for (const issue of lint.issues.slice(0, 20)) {
461
- lines.push(`- [${issue.level}] ${issue.code}: ${issue.message}`)
462
- }
463
- lines.push('')
464
- }
465
-
466
- if (contextSources.length > 0) {
467
- lines.push('## Additional Context')
468
- lines.push('')
469
- for (const source of contextSources) {
470
- lines.push(`### ${source.kind === 'file' ? '`' + source.label + '`' : source.label}`)
471
- lines.push('')
472
- lines.push(source.summary)
473
- lines.push('')
474
- }
475
- }
476
-
477
- if (overrides) {
478
- lines.push('## Project Overrides')
479
- lines.push('')
480
- lines.push(`- Source: \`${overrides.path}\``)
481
- lines.push('')
482
-
483
- appendOverrideSection(lines, 'Product Hints', overrides.productHints)
484
- appendOverrideSection(lines, 'Setup/Auth Notes', overrides.setupAuthNotes)
485
- appendOverrideSection(lines, 'Grouping Hints', overrides.groupingHints)
486
- appendOverrideSection(lines, 'Taxonomy Guidance', overrides.taxonomyGuidance)
487
- appendOverrideSection(lines, 'Instructions Guidance', overrides.instructionsGuidance)
488
- appendOverrideSection(lines, 'Review Criteria', overrides.reviewCriteria)
489
- }
490
-
491
- lines.push('## Write Contract')
492
- lines.push('')
493
- lines.push('- Edit only Pluxx-managed generated sections.')
494
- lines.push(`- Preserve custom sections marked by \`${PLUXX_CUSTOM_START}\` and \`${PLUXX_CUSTOM_END}\`.`)
495
- lines.push('- Do not change auth wiring or target-platform config unless explicitly requested.')
496
- lines.push('- Do not edit generated platform bundles in `dist/`.')
497
- lines.push('')
498
- lines.push('## Quality Bar')
499
- lines.push('')
500
- lines.push('- Each skill should represent a real user workflow or product surface.')
501
- lines.push('- Setup, admin, account, and runtime workflows should be grouped intentionally.')
502
- lines.push('- Prefer branded product language in user-facing content; avoid exposing raw MCP server identifiers unless they are operationally required.')
503
- lines.push('- Avoid tiny singleton skills unless the surface is genuinely standalone.')
504
- lines.push('- Examples should be concrete and specific, not generic placeholders.')
505
- lines.push('- Weak MCP metadata (missing/generic tool descriptions) should be called out explicitly before publishing.')
506
- lines.push('- The wording should match the MCP product narrative, not just raw tool names.')
507
- lines.push('')
508
-
509
- return `${lines.join('\n')}\n`
510
- }
511
-
512
- function buildAgentModePlanJson(
513
- config: Awaited<ReturnType<typeof loadConfig>>,
514
- metadata: McpScaffoldMetadata,
515
- lint: LintResult,
516
- editableFiles: AgentPlanFile[],
517
- protectedFiles: string[],
518
- generatedFiles: string[],
519
- contextSources: AgentContextSource[],
520
- ): string {
521
- const serverEntry = Object.entries(config.mcp ?? {})[0]
522
- const [serverName, server] = serverEntry ?? ['unknown', metadata.source]
523
- const plan: AgentModePlanFile = {
524
- version: 1,
525
- plugin: {
526
- name: config.name,
527
- displayName: config.brand?.displayName ?? metadata.settings.displayName ?? config.name,
528
- targets: [...config.targets],
529
- },
530
- mcp: {
531
- metadataPath: MCP_SCAFFOLD_METADATA_PATH,
532
- toolCount: metadata.tools.length,
533
- serverName,
534
- transport: server.transport,
535
- auth: describeAuth(server),
536
- },
537
- contextInputs: contextSources.map((source) => source.label),
538
- files: {
539
- editable: editableFiles,
540
- protected: protectedFiles,
541
- generated: generatedFiles,
542
- },
543
- successCriteria: [
544
- 'Each skill represents a real user workflow or product surface.',
545
- 'Setup/admin/account tools are grouped intentionally.',
546
- 'Examples are concrete and realistic.',
547
- 'Weak MCP metadata is surfaced before publishing.',
548
- 'Only Pluxx-managed sections are modified.',
549
- ],
550
- caveats: lint.issues.map((issue) => `[${issue.level}] ${issue.code}: ${issue.message}`),
551
- }
552
-
553
- return JSON.stringify(plan, null, 2)
554
- }
555
-
556
- async function collectAgentContextSources(
557
- rootDir: string,
558
- options: AgentPrepareOptions,
559
- overrides: AgentOverrides | null,
560
- ): Promise<AgentContextSource[]> {
561
- const sources: AgentContextSource[] = []
562
- const seenFilePaths = new Set<string>()
563
-
564
- if (options.websiteUrl) {
565
- sources.push(await fetchContextSource(options.websiteUrl, 'website'))
566
- }
567
-
568
- if (options.docsUrl) {
569
- sources.push(await fetchContextSource(options.docsUrl, 'docs'))
570
- }
571
-
572
- const contextPaths = [
573
- ...(overrides?.contextPaths ?? []),
574
- ...(options.contextPaths ?? []),
575
- ]
576
-
577
- for (const relativePath of contextPaths) {
578
- if (seenFilePaths.has(relativePath)) continue
579
- seenFilePaths.add(relativePath)
580
- const filePath = resolve(rootDir, relativePath)
581
- if (!existsSync(filePath)) {
582
- sources.push({
583
- label: relativePath,
584
- kind: 'file',
585
- summary: `Unavailable: local file not found.`,
586
- })
587
- continue
588
- }
589
-
590
- const content = await Bun.file(filePath).text()
591
- sources.push({
592
- label: relativePath,
593
- kind: 'file',
594
- summary: summarizePlainText(content),
595
- })
596
- }
597
-
598
- return sources
599
- }
600
-
601
- async function fetchContextSource(url: string, kind: 'website' | 'docs'): Promise<AgentContextSource> {
602
- try {
603
- const response = await fetch(url)
604
- if (!response.ok) {
605
- return {
606
- label: url,
607
- kind,
608
- summary: `Unavailable: fetch failed with ${response.status} ${response.statusText}.`,
609
- }
610
- }
611
-
612
- const contentType = response.headers.get('content-type') ?? ''
613
- const body = await response.text()
614
-
615
- return {
616
- label: url,
617
- kind,
618
- summary: contentType.includes('html')
619
- ? summarizeHtml(body)
620
- : summarizePlainText(body),
621
- }
622
- } catch (error) {
623
- return {
624
- label: url,
625
- kind,
626
- summary: `Unavailable: ${error instanceof Error ? error.message : String(error)}.`,
627
- }
628
- }
629
- }
630
-
631
- function summarizeHtml(html: string): string {
632
- const title = matchHtmlTag(html, 'title')
633
- const description = matchMetaDescription(html)
634
- const headings = matchHtmlTags(html, ['h1', 'h2', 'h3']).slice(0, 5)
635
- const paragraphs = matchHtmlTags(html, ['p']).slice(0, 3)
636
- const lines: string[] = []
637
-
638
- if (title) {
639
- lines.push(`Title: ${title}`)
640
- }
641
- if (description) {
642
- lines.push(`Description: ${description}`)
643
- }
644
- if (headings.length > 0) {
645
- lines.push(`Headings: ${headings.join(' | ')}`)
646
- }
647
- if (paragraphs.length > 0) {
648
- lines.push(`Excerpt: ${paragraphs.join(' ').slice(0, 900)}`)
649
- }
650
-
651
- return lines.join('\n')
652
- }
653
-
654
- function summarizePlainText(content: string): string {
655
- return content
656
- .replace(/\s+/g, ' ')
657
- .trim()
658
- .slice(0, 1200)
659
- }
660
-
661
- function matchHtmlTag(html: string, tag: string): string | null {
662
- const match = html.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, 'i'))
663
- return match ? cleanHtmlText(match[1]) : null
664
- }
665
-
666
- function matchMetaDescription(html: string): string | null {
667
- const match = html.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["'][^>]*>/i)
668
- return match ? cleanHtmlText(match[1]) : null
669
- }
670
-
671
- function matchHtmlTags(html: string, tags: string[]): string[] {
672
- const pattern = new RegExp(`<(?:${tags.join('|')})[^>]*>([\\s\\S]*?)</(?:${tags.join('|')})>`, 'ig')
673
- const values: string[] = []
674
- for (const match of html.matchAll(pattern)) {
675
- const value = cleanHtmlText(match[1])
676
- if (value) values.push(value)
677
- }
678
- return values
679
- }
680
-
681
- function cleanHtmlText(value: string): string {
682
- return value
683
- .replace(/<[^>]+>/g, ' ')
684
- .replace(/&nbsp;/g, ' ')
685
- .replace(/&amp;/g, '&')
686
- .replace(/&quot;/g, '"')
687
- .replace(/&#39;/g, "'")
688
- .replace(/\s+/g, ' ')
689
- .trim()
690
- }
691
-
692
- function buildAgentPrompt(
693
- kind: AgentPromptKind,
694
- input: {
695
- pluginName: string
696
- displayName: string
697
- skillPaths: string[]
698
- commandPaths: string[]
699
- overrides: AgentOverrides | null
700
- },
701
- ): string {
702
- const sharedIntro = [
703
- `# ${titleCase(kind)} Prompt`,
704
- '',
705
- `You are refining the Pluxx-generated plugin scaffold for \`${input.pluginName}\` (${input.displayName}).`,
706
- '',
707
- 'Inputs:',
708
- '- `.pluxx/agent/context.md`',
709
- '- `.pluxx/agent/plan.json`',
710
- `- \`${MCP_TAXONOMY_PATH}\``,
711
- '- `INSTRUCTIONS.md`',
712
- ...input.skillPaths.map((path) => `- \`${path}\``),
713
- ...input.commandPaths.map((path) => `- \`${path}\``),
714
- '',
715
- 'Rules:',
716
- '- Only edit Pluxx-managed generated sections.',
717
- `- Preserve all custom-note blocks between \`${PLUXX_CUSTOM_START}\` and \`${PLUXX_CUSTOM_END}\`.`,
718
- '- Do not change auth wiring or target-platform config.',
719
- '- Do not edit files under `dist/`.',
720
- '',
721
- ]
722
-
723
- if (kind === 'taxonomy') {
724
- return `${sharedIntro.join('\n')}Your job:\n1. Treat \`${MCP_TAXONOMY_PATH}\` as the semantic source of truth for skill grouping and naming.\n2. Infer the MCP's real product surfaces and workflows.\n3. Merge, split, or rename generated skills so labels are product-facing, not lexical buckets.\n4. Update the taxonomy file first; Pluxx will re-render generated skills and commands from that taxonomy after the pass.\n5. Keep setup/onboarding, account-admin, and runtime workflows intentionally separated when appropriate.\n6. Eliminate misleading labels such as contact or people discovery when the tools do not actually perform direct lookup.\n${buildPromptOverrideBlock(kind, input.overrides)}\nSuccess criteria:\n- each skill represents a real user workflow or product surface\n- skill names are product-shaped and avoid raw MCP tool/server identifiers when possible\n- setup/onboarding, account-admin, and runtime workflows are grouped intentionally\n- singleton skills are avoided unless they represent a real standalone user workflow\n- commands stay aligned with the chosen taxonomy\n`
725
- }
726
-
727
- if (kind === 'instructions') {
728
- return `${sharedIntro.join('\n')}Your job:\n1. Rewrite only the generated block in \`INSTRUCTIONS.md\`.\n2. Explain what the plugin is for, how the skills should be used, and which setup/admin/account/runtime boundaries matter.\n3. Keep wording aligned to the MCP's product narrative and branded language; avoid raw MCP server/tool identifiers except when technically required.\n4. Prefer the branded product name in user-facing copy; do not lead with internal MCP server identifiers.\n${buildPromptOverrideBlock(kind, input.overrides)}\nSuccess criteria:\n- instructions are concise, actionable, and product-shaped\n- wording is branded and product-facing, not raw MCP-internal naming\n- auth/setup/admin caveats are explicit when relevant\n- raw MCP server identifiers are omitted unless operationally necessary\n- the file remains safe for future \`pluxx sync --from-mcp\`\n`
729
- }
730
-
731
- return `${sharedIntro.join('\n')}Your job:\n1. Review the current scaffold critically.\n2. Call out weak skill groupings, missing setup guidance, vague examples, product/category mismatches, or weak MCP metadata signals.\n3. Separate scaffold quality findings from runtime-correctness findings.\n4. Propose only the highest-value changes needed to make the scaffold useful.\n${buildPromptOverrideBlock(kind, input.overrides)}\nSuccess criteria:\n- findings are concrete and tied to files\n- scaffold quality gaps are distinguished from runtime correctness\n- suggested changes improve user-facing plugin quality\n- recommendations stay inside Pluxx-managed boundaries\n`
732
- }
733
-
734
- function buildAgentRunnerPrompt(kind: AgentPromptKind, promptPath: string): string {
735
- const lines = [
736
- 'You are running inside a Pluxx-generated plugin scaffold.',
737
- `Read and follow \`${AGENT_CONTEXT_PATH}\`, \`${AGENT_PLAN_PATH}\`, and \`${promptPath}\` before doing anything else.`,
738
- 'Use the prompt file as the task definition.',
739
- 'Respect the write contract in the plan file.',
740
- `Preserve all custom-note blocks between \`${PLUXX_CUSTOM_START}\` and \`${PLUXX_CUSTOM_END}\`.`,
741
- 'Do not change auth wiring, target-platform config, or generated files under `dist/`.',
742
- ]
743
-
744
- if (kind === 'review') {
745
- lines.push('Do not edit files. Return findings only.')
746
- } else {
747
- lines.push('Edit only the Pluxx-managed generated sections allowed by the plan file.')
748
- lines.push('Do not run lint, build, or tests; Pluxx will verify the result afterward.')
749
- lines.push('When finished, provide a short summary of what you changed.')
750
- }
751
-
752
- return `${lines.join('\n')}\n`
753
- }
754
-
755
- function buildAgentRunnerCommand(
756
- runner: AgentRunner,
757
- kind: AgentPromptKind,
758
- prompt: string,
759
- options: {
760
- model?: string
761
- attach?: string
762
- workspace?: string
763
- } = {},
764
- ): string[] {
765
- const binary = AGENT_RUNNER_BINARIES[runner]
766
-
767
- if (runner === 'claude') {
768
- const args = [binary]
769
- if (options.model) {
770
- args.push('--model', options.model)
771
- }
772
- args.push('--permission-mode', kind === 'review' ? 'plan' : 'acceptEdits', '-p', prompt)
773
- return args
774
- }
775
-
776
- if (runner === 'codex') {
777
- const args = [binary, 'exec']
778
- if (kind !== 'review') {
779
- args.push('--full-auto')
780
- }
781
- args.push(prompt)
782
- return args
783
- }
784
-
785
- if (runner === 'cursor') {
786
- if (!options.workspace) {
787
- throw new Error('Cursor runner requires a workspace path.')
788
- }
789
-
790
- const args = [binary, '-p', '--trust', '--workspace', options.workspace]
791
- if (kind !== 'review') {
792
- args.push('--force')
793
- }
794
- if (options.model) {
795
- args.push('--model', options.model)
796
- }
797
- args.push(prompt)
798
- return args
799
- }
800
-
801
- const args = [binary, 'run']
802
- if (options.model) {
803
- args.push('--model', options.model)
804
- }
805
- if (options.attach) {
806
- args.push('--attach', options.attach)
807
- }
808
- args.push(prompt)
809
- return args
810
- }
811
-
812
- async function ensureRunnerAvailable(runner: AgentRunner): Promise<void> {
813
- const binary = AGENT_RUNNER_BINARIES[runner]
814
- const available = await commandExists(binary)
815
- if (!available) {
816
- if (runner === 'cursor') {
817
- throw new Error('The cursor runner requires the Cursor CLI `agent` binary on PATH. Install it with `curl https://cursor.com/install -fsS | bash` or choose a different runner.')
818
- }
819
- throw new Error(`The ${runner} runner is not available on PATH. Install \`${binary}\` or choose a different runner.`)
820
- }
821
- }
822
-
823
- async function ensureRunnerAuthenticated(runner: AgentRunner): Promise<void> {
824
- if (runner !== 'cursor') return
825
-
826
- if (process.env.CURSOR_API_KEY && process.env.CURSOR_API_KEY.trim().length > 0) {
827
- return
828
- }
829
-
830
- const isAuthenticated = await commandSucceeds(['agent', 'status'])
831
- if (!isAuthenticated) {
832
- throw new Error('Cursor CLI authentication is required. Run `agent login` (browser auth) or export `CURSOR_API_KEY` before running Pluxx with `--runner cursor`.')
833
- }
834
- }
835
-
836
- async function commandExists(binary: string): Promise<boolean> {
837
- return await new Promise<boolean>((resolvePromise) => {
838
- const child = spawn('sh', ['-c', `command -v ${shellQuote(binary)} >/dev/null 2>&1`], {
839
- stdio: 'ignore',
840
- env: process.env,
841
- })
842
- child.on('close', (code) => resolvePromise(code === 0))
843
- child.on('error', () => resolvePromise(false))
844
- })
845
- }
846
-
847
- async function commandSucceeds(command: string[]): Promise<boolean> {
848
- return await new Promise<boolean>((resolvePromise) => {
849
- const child = spawn(command[0], command.slice(1), {
850
- stdio: 'ignore',
851
- env: process.env,
852
- })
853
- child.on('close', (code) => resolvePromise(code === 0))
854
- child.on('error', () => resolvePromise(false))
855
- })
856
- }
857
-
858
- async function executeCommand(
859
- command: string[],
860
- cwd: string,
861
- options: {
862
- streamOutput?: boolean
863
- } = {},
864
- ): Promise<number> {
865
- return await new Promise<number>((resolvePromise, reject) => {
866
- const child = spawn(command[0], command.slice(1), {
867
- cwd,
868
- stdio: ['ignore', 'pipe', 'pipe'],
869
- env: process.env,
870
- })
871
-
872
- if (options.streamOutput) {
873
- child.stdout?.on('data', (chunk) => process.stdout.write(chunk))
874
- child.stderr?.on('data', (chunk) => process.stderr.write(chunk))
875
- }
876
-
877
- child.on('error', (error) => reject(error))
878
- child.on('close', (code) => resolvePromise(code ?? 1))
879
- })
880
- }
881
-
882
- function shellQuote(value: string): string {
883
- if (/^[A-Za-z0-9_/:=.,-]+$/.test(value)) {
884
- return value
885
- }
886
-
887
- return `'${value.replace(/'/g, `'\"'\"'`)}'`
888
- }
889
-
890
- function describeAuth(server: { auth?: { type: string; envVar?: string; headerName?: string } }): string {
891
- const auth = server.auth
892
- if (!auth || auth.type === 'none') {
893
- return 'none'
894
- }
895
-
896
- if (auth.type === 'header') {
897
- return `header via ${auth.headerName ?? 'custom header'} from ${auth.envVar ?? 'env'}`
898
- }
899
-
900
- if (auth.type === 'platform') {
901
- return 'platform-managed auth'
902
- }
903
-
904
- return `bearer via ${auth.envVar ?? 'env'}`
905
- }
906
-
907
- function titleCase(value: string): string {
908
- return value.charAt(0).toUpperCase() + value.slice(1)
909
- }
910
-
911
- async function loadAgentOverrides(rootDir: string): Promise<AgentOverrides | null> {
912
- const overridesPath = resolve(rootDir, AGENT_OVERRIDES_PATH)
913
- if (!existsSync(overridesPath)) {
914
- return null
915
- }
916
-
917
- const content = await Bun.file(overridesPath).text()
918
- return parseAgentOverrides(content, AGENT_OVERRIDES_PATH)
919
- }
920
-
921
- function parseAgentOverrides(content: string, path: string): AgentOverrides {
922
- const sections = new Map<string, string[]>()
923
- let currentSection: string | null = null
924
-
925
- for (const rawLine of content.split(/\r?\n/)) {
926
- const heading = rawLine.match(/^##\s+(.+?)\s*$/)
927
- if (heading) {
928
- currentSection = normalizeOverrideHeading(heading[1])
929
- if (currentSection && !sections.has(currentSection)) {
930
- sections.set(currentSection, [])
931
- }
932
- continue
933
- }
934
-
935
- if (!currentSection) continue
936
- sections.get(currentSection)?.push(rawLine)
937
- }
938
-
939
- const contextPaths = extractListItems(sections.get('context-paths') ?? [])
940
-
941
- return {
942
- path,
943
- contextPaths,
944
- productHints: normalizeOverrideBody(sections.get('product-hints')),
945
- setupAuthNotes: normalizeOverrideBody(sections.get('setup-auth-notes')),
946
- groupingHints: normalizeOverrideBody(sections.get('grouping-hints')),
947
- taxonomyGuidance: normalizeOverrideBody(sections.get('taxonomy-guidance')),
948
- instructionsGuidance: normalizeOverrideBody(sections.get('instructions-guidance')),
949
- reviewCriteria: normalizeOverrideBody(sections.get('review-criteria')),
950
- }
951
- }
952
-
953
- function normalizeOverrideHeading(value: string): string | null {
954
- const normalized = value
955
- .trim()
956
- .toLowerCase()
957
- .replace(/[^a-z0-9]+/g, '-')
958
- .replace(/^-+|-+$/g, '')
959
-
960
- const aliases: Record<string, string> = {
961
- 'context-paths': 'context-paths',
962
- 'context-files': 'context-paths',
963
- 'product-hints': 'product-hints',
964
- 'setup-auth-notes': 'setup-auth-notes',
965
- 'setup-and-auth-notes': 'setup-auth-notes',
966
- 'setup-auth-guidance': 'setup-auth-notes',
967
- 'grouping-hints': 'grouping-hints',
968
- 'tool-grouping-hints': 'grouping-hints',
969
- 'taxonomy-guidance': 'taxonomy-guidance',
970
- 'instructions-guidance': 'instructions-guidance',
971
- 'review-criteria': 'review-criteria',
972
- }
973
-
974
- return aliases[normalized] ?? null
975
- }
976
-
977
- function extractListItems(lines: string[]): string[] {
978
- return lines
979
- .map((line) => line.trim())
980
- .filter((line) => line.startsWith('- ') || line.startsWith('* '))
981
- .map((line) => line.slice(2).trim())
982
- .filter(Boolean)
983
- }
984
-
985
- function normalizeOverrideBody(lines: string[] | undefined): string | undefined {
986
- if (!lines) return undefined
987
- const value = lines.join('\n').trim()
988
- return value || undefined
989
- }
990
-
991
- function appendOverrideSection(lines: string[], heading: string, content: string | undefined): void {
992
- if (!content) return
993
- lines.push(`### ${heading}`)
994
- lines.push('')
995
- lines.push(content)
996
- lines.push('')
997
- }
998
-
999
- function buildPromptOverrideBlock(kind: AgentPromptKind, overrides: AgentOverrides | null): string {
1000
- if (!overrides) return ''
1001
-
1002
- const additions: string[] = []
1003
-
1004
- if (overrides.productHints) {
1005
- additions.push(`Product hints:\n${overrides.productHints}`)
1006
- }
1007
- if (overrides.setupAuthNotes) {
1008
- additions.push(`Setup/auth notes:\n${overrides.setupAuthNotes}`)
1009
- }
1010
-
1011
- if (kind === 'taxonomy') {
1012
- if (overrides.groupingHints) {
1013
- additions.push(`Grouping hints:\n${overrides.groupingHints}`)
1014
- }
1015
- if (overrides.taxonomyGuidance) {
1016
- additions.push(`Taxonomy guidance:\n${overrides.taxonomyGuidance}`)
1017
- }
1018
- }
1019
-
1020
- if (kind === 'instructions' && overrides.instructionsGuidance) {
1021
- additions.push(`Instructions guidance:\n${overrides.instructionsGuidance}`)
1022
- }
1023
-
1024
- if (kind === 'review' && overrides.reviewCriteria) {
1025
- additions.push(`Additional review criteria:\n${overrides.reviewCriteria}`)
1026
- }
1027
-
1028
- if (additions.length === 0) return ''
1029
- return `\nProject overrides:\n${additions.map((block) => `- ${block.replace(/\n/g, '\n ')}`).join('\n')}\n`
1030
- }