@orchid-labs/pluxx 0.1.1 → 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.
- package/README.md +25 -8
- package/bin/pluxx.js +19 -28
- package/dist/agents.d.ts +16 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/cli/agent.d.ts +62 -0
- package/dist/cli/agent.d.ts.map +1 -1
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/entry.d.ts +2 -0
- package/dist/cli/entry.d.ts.map +1 -0
- package/dist/cli/index.d.ts +7 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +21810 -0
- package/dist/cli/init-from-mcp.d.ts +17 -1
- package/dist/cli/init-from-mcp.d.ts.map +1 -1
- package/dist/cli/install.d.ts +1 -0
- package/dist/cli/install.d.ts.map +1 -1
- package/dist/cli/lint.d.ts +3 -1
- package/dist/cli/lint.d.ts.map +1 -1
- package/dist/cli/mcp-proxy.d.ts.map +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/primitive-summary.d.ts +14 -0
- package/dist/cli/primitive-summary.d.ts.map +1 -0
- package/dist/cli/prompt.d.ts +1 -1
- package/dist/cli/publish.d.ts +6 -1
- package/dist/cli/publish.d.ts.map +1 -1
- package/dist/cli/sync-from-mcp.d.ts.map +1 -1
- package/dist/cli/verify-install.d.ts +25 -0
- package/dist/cli/verify-install.d.ts.map +1 -0
- package/dist/commands.d.ts +10 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/compiler-intent.d.ts +165 -0
- package/dist/compiler-intent.d.ts.map +1 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/delegation.d.ts +11 -0
- package/dist/delegation.d.ts.map +1 -0
- package/dist/generators/amp/index.d.ts.map +1 -1
- package/dist/generators/base.d.ts +5 -0
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/claude-code/index.d.ts.map +1 -1
- package/dist/generators/cline/index.d.ts.map +1 -1
- package/dist/generators/codex/index.d.ts +4 -0
- package/dist/generators/codex/index.d.ts.map +1 -1
- package/dist/generators/cursor/index.d.ts +1 -0
- package/dist/generators/cursor/index.d.ts.map +1 -1
- package/dist/generators/gemini-cli/index.d.ts.map +1 -1
- package/dist/generators/github-copilot/index.d.ts.map +1 -1
- package/dist/generators/opencode/index.d.ts +1 -0
- package/dist/generators/opencode/index.d.ts.map +1 -1
- package/dist/generators/openhands/index.d.ts.map +1 -1
- package/dist/generators/roo-code/index.d.ts.map +1 -1
- package/dist/generators/shared/claude-family.d.ts.map +1 -1
- package/dist/generators/warp/index.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5371 -553
- package/dist/schema.d.ts +91 -42
- package/dist/schema.d.ts.map +1 -1
- package/dist/text-files.d.ts +5 -0
- package/dist/text-files.d.ts.map +1 -0
- package/dist/validation/platform-rules.d.ts +15 -1
- package/dist/validation/platform-rules.d.ts.map +1 -1
- package/package.json +15 -13
- package/src/cli/agent.ts +0 -1455
- package/src/cli/dev.ts +0 -112
- package/src/cli/doctor.ts +0 -987
- package/src/cli/eval.ts +0 -470
- package/src/cli/index.ts +0 -2933
- package/src/cli/init-from-mcp.ts +0 -2115
- package/src/cli/install.ts +0 -860
- package/src/cli/lint.ts +0 -1249
- package/src/cli/mcp-proxy.ts +0 -322
- package/src/cli/migrate.ts +0 -867
- package/src/cli/prompt.ts +0 -82
- package/src/cli/publish.ts +0 -401
- package/src/cli/runtime.ts +0 -86
- package/src/cli/sync-from-mcp.ts +0 -586
- package/src/cli/test.ts +0 -142
- package/src/compatibility/matrix.ts +0 -149
- package/src/config/define.ts +0 -20
- package/src/config/load.ts +0 -74
- package/src/generators/amp/index.ts +0 -63
- package/src/generators/base.ts +0 -188
- package/src/generators/claude-code/index.ts +0 -172
- package/src/generators/cline/index.ts +0 -35
- package/src/generators/codex/index.ts +0 -143
- package/src/generators/cursor/index.ts +0 -158
- package/src/generators/gemini-cli/index.ts +0 -83
- package/src/generators/github-copilot/index.ts +0 -32
- package/src/generators/hooks-warning.ts +0 -51
- package/src/generators/index.ts +0 -71
- package/src/generators/opencode/index.ts +0 -526
- package/src/generators/openhands/index.ts +0 -32
- package/src/generators/roo-code/index.ts +0 -35
- package/src/generators/shared/claude-family.ts +0 -215
- package/src/generators/warp/index.ts +0 -32
- package/src/hook-events.ts +0 -33
- package/src/index.ts +0 -34
- package/src/mcp/introspect.ts +0 -1107
- package/src/permissions.ts +0 -260
- package/src/schema.ts +0 -312
- package/src/user-config.ts +0 -177
- package/src/validation/platform-rules.ts +0 -686
package/src/cli/agent.ts
DELETED
|
@@ -1,1455 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'fs'
|
|
2
|
-
import { chmod, copyFile, mkdir, mkdtemp, readFile, rm } from 'fs/promises'
|
|
3
|
-
import { homedir, tmpdir } from 'os'
|
|
4
|
-
import { resolve } from 'path'
|
|
5
|
-
import { spawn } from 'child_process'
|
|
6
|
-
import { loadConfig } from '../config/load'
|
|
7
|
-
import { lintProject, type LintResult } from './lint'
|
|
8
|
-
import { runTestSuite, type TestRunResult } from './test'
|
|
9
|
-
import {
|
|
10
|
-
MCP_SCAFFOLD_METADATA_PATH,
|
|
11
|
-
MCP_TAXONOMY_PATH,
|
|
12
|
-
PLUXX_CUSTOM_END,
|
|
13
|
-
PLUXX_CUSTOM_START,
|
|
14
|
-
PLUXX_GENERATED_END,
|
|
15
|
-
PLUXX_GENERATED_START,
|
|
16
|
-
type McpScaffoldMetadata,
|
|
17
|
-
} from './init-from-mcp'
|
|
18
|
-
import { applyPersistedTaxonomy } from './sync-from-mcp'
|
|
19
|
-
|
|
20
|
-
export const AGENT_CONTEXT_PATH = '.pluxx/agent/context.md'
|
|
21
|
-
export const AGENT_PLAN_PATH = '.pluxx/agent/plan.json'
|
|
22
|
-
export const AGENT_OVERRIDES_PATH = 'pluxx.agent.md'
|
|
23
|
-
export const AGENT_PROMPT_KINDS = ['taxonomy', 'instructions', 'review'] as const
|
|
24
|
-
export const AGENT_RUNNERS = ['claude', 'opencode', 'codex', 'cursor'] as const
|
|
25
|
-
export type AgentPromptKind = typeof AGENT_PROMPT_KINDS[number]
|
|
26
|
-
export type AgentRunner = typeof AGENT_RUNNERS[number]
|
|
27
|
-
|
|
28
|
-
const AGENT_PROMPT_PATHS: Record<AgentPromptKind, string> = {
|
|
29
|
-
taxonomy: '.pluxx/agent/taxonomy-prompt.md',
|
|
30
|
-
instructions: '.pluxx/agent/instructions-prompt.md',
|
|
31
|
-
review: '.pluxx/agent/review-prompt.md',
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const AGENT_RUNNER_BINARIES: Record<AgentRunner, string> = {
|
|
35
|
-
claude: 'claude',
|
|
36
|
-
opencode: 'opencode',
|
|
37
|
-
codex: 'codex',
|
|
38
|
-
cursor: 'agent',
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const CURSOR_RUNNER_BINARIES = ['agent', 'cursor-agent'] as const
|
|
42
|
-
|
|
43
|
-
export interface AgentPreparePlannedFile {
|
|
44
|
-
relativePath: string
|
|
45
|
-
content: string
|
|
46
|
-
action: 'create' | 'update' | 'unchanged'
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface AgentPrepareSummary {
|
|
50
|
-
pluginName: string
|
|
51
|
-
targetCount: number
|
|
52
|
-
toolCount: number
|
|
53
|
-
skillCount: number
|
|
54
|
-
editableFiles: string[]
|
|
55
|
-
protectedFiles: string[]
|
|
56
|
-
generatedFiles: string[]
|
|
57
|
-
createdFiles: string[]
|
|
58
|
-
updatedFiles: string[]
|
|
59
|
-
lint: {
|
|
60
|
-
errors: number
|
|
61
|
-
warnings: number
|
|
62
|
-
}
|
|
63
|
-
contextInputs: string[]
|
|
64
|
-
dryRun?: boolean
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface AgentPreparePlan extends AgentPrepareSummary {
|
|
68
|
-
files: AgentPreparePlannedFile[]
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface AgentPrepareOptions {
|
|
72
|
-
docsUrl?: string
|
|
73
|
-
websiteUrl?: string
|
|
74
|
-
contextPaths?: string[]
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
interface AgentPromptOptions {
|
|
78
|
-
allowMissingContext?: boolean
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface AgentPromptSummary {
|
|
82
|
-
pluginName: string
|
|
83
|
-
kind: AgentPromptKind
|
|
84
|
-
outputPath: string
|
|
85
|
-
createdFiles: string[]
|
|
86
|
-
updatedFiles: string[]
|
|
87
|
-
dryRun?: boolean
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface AgentPromptPlan extends AgentPromptSummary {
|
|
91
|
-
files: AgentPreparePlannedFile[]
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export interface AgentRunOptions {
|
|
95
|
-
runner: AgentRunner
|
|
96
|
-
model?: string
|
|
97
|
-
attach?: string
|
|
98
|
-
verify?: boolean
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface AgentRunnerModelSummary {
|
|
102
|
-
value?: string
|
|
103
|
-
source: 'explicit' | 'default' | 'unknown'
|
|
104
|
-
display: string
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export interface AgentRunSummary {
|
|
108
|
-
pluginName: string
|
|
109
|
-
kind: AgentPromptKind
|
|
110
|
-
runner: AgentRunner
|
|
111
|
-
model: AgentRunnerModelSummary
|
|
112
|
-
verify: boolean
|
|
113
|
-
command: string[]
|
|
114
|
-
commandDisplay: string
|
|
115
|
-
promptPath: string
|
|
116
|
-
contextPath: string
|
|
117
|
-
createdFiles: string[]
|
|
118
|
-
updatedFiles: string[]
|
|
119
|
-
contextInputs: string[]
|
|
120
|
-
dryRun?: boolean
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export interface AgentRunPlan extends AgentRunSummary {
|
|
124
|
-
files: AgentPreparePlannedFile[]
|
|
125
|
-
prepareOptions?: AgentPrepareOptions
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export interface AgentRunResult extends AgentRunSummary {
|
|
129
|
-
ok: boolean
|
|
130
|
-
runnerExitCode: number
|
|
131
|
-
verification?: TestRunResult
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
interface AgentPlanFile {
|
|
135
|
-
path: string
|
|
136
|
-
managedSections?: Array<{
|
|
137
|
-
start: string
|
|
138
|
-
end: string
|
|
139
|
-
}>
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
interface AgentModePlanFile {
|
|
143
|
-
version: 1
|
|
144
|
-
plugin: {
|
|
145
|
-
name: string
|
|
146
|
-
displayName: string
|
|
147
|
-
targets: string[]
|
|
148
|
-
}
|
|
149
|
-
mcp: {
|
|
150
|
-
metadataPath: string
|
|
151
|
-
toolCount: number
|
|
152
|
-
serverName: string
|
|
153
|
-
transport: string
|
|
154
|
-
auth: string
|
|
155
|
-
}
|
|
156
|
-
contextInputs: string[]
|
|
157
|
-
files: {
|
|
158
|
-
editable: AgentPlanFile[]
|
|
159
|
-
protected: string[]
|
|
160
|
-
generated: string[]
|
|
161
|
-
}
|
|
162
|
-
successCriteria: string[]
|
|
163
|
-
caveats: string[]
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
interface AgentContextSource {
|
|
167
|
-
label: string
|
|
168
|
-
kind: 'website' | 'docs' | 'file'
|
|
169
|
-
summary: string
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
interface AgentOverrides {
|
|
173
|
-
path: string
|
|
174
|
-
contextPaths: string[]
|
|
175
|
-
productHints?: string
|
|
176
|
-
setupAuthNotes?: string
|
|
177
|
-
groupingHints?: string
|
|
178
|
-
taxonomyGuidance?: string
|
|
179
|
-
instructionsGuidance?: string
|
|
180
|
-
reviewCriteria?: string
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function hasManagedCommands(metadata: McpScaffoldMetadata): boolean {
|
|
184
|
-
return metadata.managedFiles.some((file) => file.startsWith('commands/'))
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export async function planAgentPrepare(
|
|
188
|
-
rootDir: string = process.cwd(),
|
|
189
|
-
options: AgentPrepareOptions = {},
|
|
190
|
-
): Promise<AgentPreparePlan> {
|
|
191
|
-
const config = await loadConfig(rootDir)
|
|
192
|
-
const metadata = await loadMcpScaffoldMetadata(rootDir)
|
|
193
|
-
const lint = await lintProject(rootDir)
|
|
194
|
-
const overrides = await loadAgentOverrides(rootDir)
|
|
195
|
-
const contextSources = await collectAgentContextSources(rootDir, options, overrides)
|
|
196
|
-
const editableFiles = buildEditableFiles(metadata)
|
|
197
|
-
const protectedFiles = buildProtectedFiles()
|
|
198
|
-
const generatedFiles = [AGENT_CONTEXT_PATH, AGENT_PLAN_PATH]
|
|
199
|
-
const contextContent = buildAgentContext(config, metadata, lint, contextSources, overrides)
|
|
200
|
-
const planContent = buildAgentModePlanJson(config, metadata, lint, editableFiles, protectedFiles, generatedFiles, contextSources)
|
|
201
|
-
|
|
202
|
-
const files = await Promise.all([
|
|
203
|
-
planFile(rootDir, AGENT_CONTEXT_PATH, contextContent),
|
|
204
|
-
planFile(rootDir, AGENT_PLAN_PATH, `${planContent}\n`),
|
|
205
|
-
])
|
|
206
|
-
|
|
207
|
-
return {
|
|
208
|
-
pluginName: config.name,
|
|
209
|
-
targetCount: config.targets.length,
|
|
210
|
-
toolCount: metadata.tools.length,
|
|
211
|
-
skillCount: metadata.skills.length,
|
|
212
|
-
editableFiles: editableFiles.map((file) => file.path),
|
|
213
|
-
protectedFiles,
|
|
214
|
-
generatedFiles,
|
|
215
|
-
createdFiles: files.filter((file) => file.action === 'create').map((file) => file.relativePath),
|
|
216
|
-
updatedFiles: files.filter((file) => file.action === 'update').map((file) => file.relativePath),
|
|
217
|
-
lint: {
|
|
218
|
-
errors: lint.errors,
|
|
219
|
-
warnings: lint.warnings,
|
|
220
|
-
},
|
|
221
|
-
contextInputs: contextSources.map((source) => source.label),
|
|
222
|
-
files,
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export async function applyAgentPreparePlan(rootDir: string, plan: AgentPreparePlan): Promise<void> {
|
|
227
|
-
for (const file of plan.files) {
|
|
228
|
-
const filePath = resolve(rootDir, file.relativePath)
|
|
229
|
-
const parentDir = file.relativePath.split('/').slice(0, -1).join('/')
|
|
230
|
-
if (parentDir) {
|
|
231
|
-
await mkdir(resolve(rootDir, parentDir), { recursive: true })
|
|
232
|
-
}
|
|
233
|
-
await Bun.write(filePath, file.content)
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export async function planAgentPrompt(
|
|
238
|
-
rootDir: string,
|
|
239
|
-
kind: AgentPromptKind,
|
|
240
|
-
options: AgentPromptOptions = {},
|
|
241
|
-
): Promise<AgentPromptPlan> {
|
|
242
|
-
const config = await loadConfig(rootDir)
|
|
243
|
-
const metadata = await loadMcpScaffoldMetadata(rootDir)
|
|
244
|
-
const overrides = await loadAgentOverrides(rootDir)
|
|
245
|
-
const contextPath = resolve(rootDir, AGENT_CONTEXT_PATH)
|
|
246
|
-
|
|
247
|
-
if (!options.allowMissingContext && !existsSync(contextPath)) {
|
|
248
|
-
throw new Error(`No agent context found at ${AGENT_CONTEXT_PATH}. Run "pluxx agent prepare" first.`)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const outputPath = AGENT_PROMPT_PATHS[kind]
|
|
252
|
-
const content = buildAgentPrompt(kind, {
|
|
253
|
-
pluginName: config.name,
|
|
254
|
-
displayName: config.brand?.displayName ?? metadata.settings.displayName ?? config.name,
|
|
255
|
-
skillPaths: metadata.skills.map((skill) => `skills/${skill.dirName}/SKILL.md`),
|
|
256
|
-
commandPaths: hasManagedCommands(metadata)
|
|
257
|
-
? metadata.skills.map((skill) => `commands/${skill.dirName}.md`)
|
|
258
|
-
: [],
|
|
259
|
-
overrides,
|
|
260
|
-
})
|
|
261
|
-
const file = await planFile(rootDir, outputPath, content)
|
|
262
|
-
|
|
263
|
-
return {
|
|
264
|
-
pluginName: config.name,
|
|
265
|
-
kind,
|
|
266
|
-
outputPath,
|
|
267
|
-
createdFiles: file.action === 'create' ? [outputPath] : [],
|
|
268
|
-
updatedFiles: file.action === 'update' ? [outputPath] : [],
|
|
269
|
-
files: [file],
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export async function applyAgentPromptPlan(rootDir: string, plan: AgentPromptPlan): Promise<void> {
|
|
274
|
-
for (const file of plan.files) {
|
|
275
|
-
const filePath = resolve(rootDir, file.relativePath)
|
|
276
|
-
const parentDir = file.relativePath.split('/').slice(0, -1).join('/')
|
|
277
|
-
if (parentDir) {
|
|
278
|
-
await mkdir(resolve(rootDir, parentDir), { recursive: true })
|
|
279
|
-
}
|
|
280
|
-
await Bun.write(filePath, file.content)
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export async function planAgentRun(
|
|
285
|
-
rootDir: string = process.cwd(),
|
|
286
|
-
kind: AgentPromptKind,
|
|
287
|
-
options: AgentRunOptions,
|
|
288
|
-
prepareOptions: AgentPrepareOptions = {},
|
|
289
|
-
): Promise<AgentRunPlan> {
|
|
290
|
-
if (options.runner !== 'opencode' && options.attach) {
|
|
291
|
-
throw new Error('--attach is only supported for the opencode runner.')
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const preparePlan = await planAgentPrepare(rootDir, prepareOptions)
|
|
295
|
-
const promptPlan = await planAgentPrompt(rootDir, kind, { allowMissingContext: true })
|
|
296
|
-
const promptPath = AGENT_PROMPT_PATHS[kind]
|
|
297
|
-
const verify = kind === 'review' ? false : options.verify !== false
|
|
298
|
-
const command = buildAgentRunnerCommand(options.runner, kind, buildAgentRunnerPrompt(kind, promptPath), {
|
|
299
|
-
model: options.model,
|
|
300
|
-
attach: options.attach,
|
|
301
|
-
workspace: rootDir,
|
|
302
|
-
})
|
|
303
|
-
const model = await resolveAgentRunnerModel(options.runner, options.model)
|
|
304
|
-
|
|
305
|
-
return {
|
|
306
|
-
pluginName: preparePlan.pluginName,
|
|
307
|
-
kind,
|
|
308
|
-
runner: options.runner,
|
|
309
|
-
model,
|
|
310
|
-
verify,
|
|
311
|
-
command,
|
|
312
|
-
commandDisplay: command.map(shellQuote).join(' '),
|
|
313
|
-
promptPath,
|
|
314
|
-
contextPath: AGENT_CONTEXT_PATH,
|
|
315
|
-
createdFiles: [...preparePlan.createdFiles, ...promptPlan.createdFiles],
|
|
316
|
-
updatedFiles: [...preparePlan.updatedFiles, ...promptPlan.updatedFiles],
|
|
317
|
-
contextInputs: preparePlan.contextInputs,
|
|
318
|
-
files: [...preparePlan.files, ...promptPlan.files],
|
|
319
|
-
prepareOptions,
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
export async function runAgentPlan(
|
|
324
|
-
rootDir: string,
|
|
325
|
-
plan: AgentRunPlan,
|
|
326
|
-
options: {
|
|
327
|
-
streamOutput?: boolean
|
|
328
|
-
} = {},
|
|
329
|
-
): Promise<AgentRunResult> {
|
|
330
|
-
const preparePlan = await planAgentPrepare(rootDir, plan.prepareOptions ?? {})
|
|
331
|
-
const promptPlan = await planAgentPrompt(rootDir, plan.kind, { allowMissingContext: true })
|
|
332
|
-
await writePlannedFiles(rootDir, [...preparePlan.files, ...promptPlan.files])
|
|
333
|
-
let createdFiles = [...preparePlan.createdFiles, ...promptPlan.createdFiles]
|
|
334
|
-
let updatedFiles = [...preparePlan.updatedFiles, ...promptPlan.updatedFiles]
|
|
335
|
-
let contextInputs = preparePlan.contextInputs
|
|
336
|
-
|
|
337
|
-
await ensureRunnerAvailable(plan.runner)
|
|
338
|
-
await ensureRunnerAuthenticated(plan.runner)
|
|
339
|
-
const executionContext = await prepareRunnerExecution(plan.runner)
|
|
340
|
-
let runnerExitCode: number
|
|
341
|
-
try {
|
|
342
|
-
runnerExitCode = await executeCommand(plan.command, rootDir, {
|
|
343
|
-
streamOutput: options.streamOutput === true,
|
|
344
|
-
env: executionContext.env,
|
|
345
|
-
})
|
|
346
|
-
} finally {
|
|
347
|
-
await executionContext.cleanup?.()
|
|
348
|
-
}
|
|
349
|
-
if (runnerExitCode === 0 && plan.kind === 'taxonomy') {
|
|
350
|
-
await applyPersistedTaxonomy(rootDir)
|
|
351
|
-
const refreshedPack = await refreshAgentPack(rootDir, plan.prepareOptions ?? {})
|
|
352
|
-
createdFiles = mergeUnique(createdFiles, refreshedPack.createdFiles)
|
|
353
|
-
updatedFiles = mergeUnique(updatedFiles, refreshedPack.updatedFiles)
|
|
354
|
-
contextInputs = refreshedPack.contextInputs
|
|
355
|
-
}
|
|
356
|
-
const verification = runnerExitCode === 0 && plan.verify
|
|
357
|
-
? await runTestSuite({ rootDir })
|
|
358
|
-
: undefined
|
|
359
|
-
|
|
360
|
-
return {
|
|
361
|
-
...plan,
|
|
362
|
-
createdFiles,
|
|
363
|
-
updatedFiles,
|
|
364
|
-
contextInputs,
|
|
365
|
-
ok: runnerExitCode === 0 && (verification?.ok ?? true),
|
|
366
|
-
runnerExitCode,
|
|
367
|
-
verification,
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
async function refreshAgentPack(
|
|
372
|
-
rootDir: string,
|
|
373
|
-
prepareOptions: AgentPrepareOptions,
|
|
374
|
-
): Promise<{
|
|
375
|
-
createdFiles: string[]
|
|
376
|
-
updatedFiles: string[]
|
|
377
|
-
contextInputs: string[]
|
|
378
|
-
}> {
|
|
379
|
-
const preparePlan = await planAgentPrepare(rootDir, prepareOptions)
|
|
380
|
-
const promptPlans = await Promise.all(
|
|
381
|
-
AGENT_PROMPT_KINDS.map((kind) => planAgentPrompt(rootDir, kind, { allowMissingContext: true })),
|
|
382
|
-
)
|
|
383
|
-
const files = [
|
|
384
|
-
...preparePlan.files,
|
|
385
|
-
...promptPlans.flatMap((promptPlan) => promptPlan.files),
|
|
386
|
-
]
|
|
387
|
-
await writePlannedFiles(rootDir, files)
|
|
388
|
-
|
|
389
|
-
return {
|
|
390
|
-
createdFiles: mergeUnique(
|
|
391
|
-
preparePlan.createdFiles,
|
|
392
|
-
promptPlans.flatMap((promptPlan) => promptPlan.createdFiles),
|
|
393
|
-
),
|
|
394
|
-
updatedFiles: mergeUnique(
|
|
395
|
-
preparePlan.updatedFiles,
|
|
396
|
-
promptPlans.flatMap((promptPlan) => promptPlan.updatedFiles),
|
|
397
|
-
),
|
|
398
|
-
contextInputs: preparePlan.contextInputs,
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
async function writePlannedFiles(rootDir: string, files: AgentPreparePlannedFile[]): Promise<void> {
|
|
403
|
-
for (const file of files) {
|
|
404
|
-
const filePath = resolve(rootDir, file.relativePath)
|
|
405
|
-
const parentDir = file.relativePath.split('/').slice(0, -1).join('/')
|
|
406
|
-
if (parentDir) {
|
|
407
|
-
await mkdir(resolve(rootDir, parentDir), { recursive: true })
|
|
408
|
-
}
|
|
409
|
-
await Bun.write(filePath, file.content)
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function mergeUnique(existing: string[], next: string[]): string[] {
|
|
414
|
-
return [...new Set([...existing, ...next])]
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function buildEditableFiles(metadata: McpScaffoldMetadata): AgentPlanFile[] {
|
|
418
|
-
const files: AgentPlanFile[] = [{
|
|
419
|
-
path: MCP_TAXONOMY_PATH,
|
|
420
|
-
}, {
|
|
421
|
-
path: 'INSTRUCTIONS.md',
|
|
422
|
-
managedSections: [{ start: PLUXX_GENERATED_START, end: PLUXX_GENERATED_END }],
|
|
423
|
-
}]
|
|
424
|
-
|
|
425
|
-
for (const skill of metadata.skills) {
|
|
426
|
-
files.push({
|
|
427
|
-
path: `skills/${skill.dirName}/SKILL.md`,
|
|
428
|
-
managedSections: [{ start: PLUXX_GENERATED_START, end: PLUXX_GENERATED_END }],
|
|
429
|
-
})
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
if (hasManagedCommands(metadata)) {
|
|
433
|
-
for (const skill of metadata.skills) {
|
|
434
|
-
files.push({
|
|
435
|
-
path: `commands/${skill.dirName}.md`,
|
|
436
|
-
managedSections: [{ start: PLUXX_GENERATED_START, end: PLUXX_GENERATED_END }],
|
|
437
|
-
})
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return files
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function buildProtectedFiles(): string[] {
|
|
445
|
-
return [
|
|
446
|
-
'pluxx.config.ts',
|
|
447
|
-
'pluxx.config.js',
|
|
448
|
-
'pluxx.config.json',
|
|
449
|
-
AGENT_OVERRIDES_PATH,
|
|
450
|
-
MCP_SCAFFOLD_METADATA_PATH,
|
|
451
|
-
'dist/',
|
|
452
|
-
]
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
async function loadMcpScaffoldMetadata(rootDir: string): Promise<McpScaffoldMetadata> {
|
|
456
|
-
const metadataPath = resolve(rootDir, MCP_SCAFFOLD_METADATA_PATH)
|
|
457
|
-
if (!existsSync(metadataPath)) {
|
|
458
|
-
throw new Error(`No MCP scaffold metadata found at ${MCP_SCAFFOLD_METADATA_PATH}. Run "pluxx init --from-mcp" first.`)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
const text = await Bun.file(metadataPath).text()
|
|
463
|
-
return JSON.parse(text) as McpScaffoldMetadata
|
|
464
|
-
} catch (error) {
|
|
465
|
-
throw new Error(
|
|
466
|
-
`Failed to parse ${MCP_SCAFFOLD_METADATA_PATH}: ${error instanceof Error ? error.message : String(error)}`,
|
|
467
|
-
)
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
async function planFile(rootDir: string, relativePath: string, content: string): Promise<AgentPreparePlannedFile> {
|
|
472
|
-
const filePath = resolve(rootDir, relativePath)
|
|
473
|
-
const action = existsSync(filePath)
|
|
474
|
-
? ((await Bun.file(filePath).text()) === content ? 'unchanged' : 'update')
|
|
475
|
-
: 'create'
|
|
476
|
-
return { relativePath, content, action }
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function buildAgentContext(
|
|
480
|
-
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
481
|
-
metadata: McpScaffoldMetadata,
|
|
482
|
-
lint: LintResult,
|
|
483
|
-
contextSources: AgentContextSource[],
|
|
484
|
-
overrides: AgentOverrides | null,
|
|
485
|
-
): string {
|
|
486
|
-
const serverEntry = Object.entries(config.mcp ?? {})[0]
|
|
487
|
-
const [serverName, server] = serverEntry ?? ['unknown', undefined]
|
|
488
|
-
const displayName = config.brand?.displayName ?? metadata.settings.displayName ?? config.name
|
|
489
|
-
const resourceByUri = new Map((metadata.resources ?? []).map((resource) => [resource.uri, resource]))
|
|
490
|
-
const resourceTemplateByUri = new Map((metadata.resourceTemplates ?? []).map((template) => [template.uriTemplate, template]))
|
|
491
|
-
const promptByName = new Map((metadata.prompts ?? []).map((prompt) => [prompt.name, prompt]))
|
|
492
|
-
const lines = [
|
|
493
|
-
'# Pluxx Agent Context',
|
|
494
|
-
'',
|
|
495
|
-
'## Plugin',
|
|
496
|
-
'',
|
|
497
|
-
`- Name: \`${config.name}\``,
|
|
498
|
-
`- Display name: ${displayName}`,
|
|
499
|
-
`- Targets: ${config.targets.join(', ')}`,
|
|
500
|
-
'',
|
|
501
|
-
'## MCP',
|
|
502
|
-
'',
|
|
503
|
-
`- Metadata source: \`${MCP_SCAFFOLD_METADATA_PATH}\``,
|
|
504
|
-
`- Semantic taxonomy: \`${MCP_TAXONOMY_PATH}\``,
|
|
505
|
-
`- Server name: \`${serverName}\``,
|
|
506
|
-
`- Transport: ${server?.transport ?? metadata.source.transport}`,
|
|
507
|
-
`- Auth: ${describeAuth(server ?? metadata.source)}`,
|
|
508
|
-
`- Tool count: ${metadata.tools.length}`,
|
|
509
|
-
`- Resource count: ${metadata.resources?.length ?? 0}`,
|
|
510
|
-
`- Prompt template count: ${metadata.prompts?.length ?? 0}`,
|
|
511
|
-
'',
|
|
512
|
-
'## Generated Skills',
|
|
513
|
-
'',
|
|
514
|
-
]
|
|
515
|
-
|
|
516
|
-
for (const skill of metadata.skills) {
|
|
517
|
-
const relatedResourceLabels = [
|
|
518
|
-
...(skill.resourceUris ?? []).map((uri) => {
|
|
519
|
-
const resource = resourceByUri.get(uri)
|
|
520
|
-
return resource ? `\`${resource.name ?? resource.title ?? resource.uri}\`` : null
|
|
521
|
-
}),
|
|
522
|
-
...(skill.resourceTemplateUris ?? []).map((uriTemplate) => {
|
|
523
|
-
const template = resourceTemplateByUri.get(uriTemplate)
|
|
524
|
-
return template ? `\`${template.name}\`` : null
|
|
525
|
-
}),
|
|
526
|
-
].filter((label): label is string => Boolean(label))
|
|
527
|
-
const relatedPromptLabels = (skill.promptNames ?? [])
|
|
528
|
-
.map((name) => promptByName.get(name)?.name ?? name)
|
|
529
|
-
.map((name) => `\`${name}\``)
|
|
530
|
-
|
|
531
|
-
lines.push(`### \`${skill.dirName}\``)
|
|
532
|
-
lines.push('')
|
|
533
|
-
lines.push(`- Title: ${skill.title}`)
|
|
534
|
-
lines.push(`- Tools: ${skill.toolNames.join(', ') || 'none'}`)
|
|
535
|
-
if (relatedResourceLabels.length > 0) {
|
|
536
|
-
lines.push(`- Related resources: ${relatedResourceLabels.join(', ')}`)
|
|
537
|
-
}
|
|
538
|
-
if (relatedPromptLabels.length > 0) {
|
|
539
|
-
lines.push(`- Related prompt templates: ${relatedPromptLabels.join(', ')}`)
|
|
540
|
-
}
|
|
541
|
-
lines.push('')
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if ((metadata.resources?.length ?? 0) > 0 || (metadata.resourceTemplates?.length ?? 0) > 0 || (metadata.prompts?.length ?? 0) > 0) {
|
|
545
|
-
lines.push('## MCP Discovery Surfaces')
|
|
546
|
-
lines.push('')
|
|
547
|
-
|
|
548
|
-
for (const resource of metadata.resources ?? []) {
|
|
549
|
-
const label = resource.name ?? resource.title ?? resource.uri
|
|
550
|
-
lines.push(`- Resource \`${label}\`: ${summarizeDiscoveryDescription(resource.description, `URI: ${resource.uri}`)}`)
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
for (const template of metadata.resourceTemplates ?? []) {
|
|
554
|
-
lines.push(`- Resource template \`${template.name}\`: ${summarizeDiscoveryDescription(template.description, `URI template: ${template.uriTemplate}`)}`)
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
for (const prompt of metadata.prompts ?? []) {
|
|
558
|
-
const args = prompt.arguments?.map((argument) => `\`${argument.name}\`${argument.required ? ' (required)' : ''}`).join(', ')
|
|
559
|
-
const trailing = args ? `Arguments: ${args}` : undefined
|
|
560
|
-
lines.push(`- Prompt \`${prompt.name}\`: ${summarizeDiscoveryDescription(prompt.description, trailing)}`)
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
lines.push('')
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
lines.push('## Lint Snapshot')
|
|
567
|
-
lines.push('')
|
|
568
|
-
lines.push(`- Errors: ${lint.errors}`)
|
|
569
|
-
lines.push(`- Warnings: ${lint.warnings}`)
|
|
570
|
-
lines.push('')
|
|
571
|
-
|
|
572
|
-
if (lint.issues.length > 0) {
|
|
573
|
-
lines.push('### Current Issues')
|
|
574
|
-
lines.push('')
|
|
575
|
-
for (const issue of lint.issues.slice(0, 20)) {
|
|
576
|
-
lines.push(`- [${issue.level}] ${issue.code}: ${issue.message}`)
|
|
577
|
-
}
|
|
578
|
-
lines.push('')
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
if (contextSources.length > 0) {
|
|
582
|
-
lines.push('## Additional Context')
|
|
583
|
-
lines.push('')
|
|
584
|
-
for (const source of contextSources) {
|
|
585
|
-
lines.push(`### ${source.kind === 'file' ? '`' + source.label + '`' : source.label}`)
|
|
586
|
-
lines.push('')
|
|
587
|
-
lines.push(source.summary)
|
|
588
|
-
lines.push('')
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
if (overrides) {
|
|
593
|
-
lines.push('## Project Overrides')
|
|
594
|
-
lines.push('')
|
|
595
|
-
lines.push(`- Source: \`${overrides.path}\``)
|
|
596
|
-
lines.push('')
|
|
597
|
-
|
|
598
|
-
appendOverrideSection(lines, 'Product Hints', overrides.productHints)
|
|
599
|
-
appendOverrideSection(lines, 'Setup/Auth Notes', overrides.setupAuthNotes)
|
|
600
|
-
appendOverrideSection(lines, 'Grouping Hints', overrides.groupingHints)
|
|
601
|
-
appendOverrideSection(lines, 'Taxonomy Guidance', overrides.taxonomyGuidance)
|
|
602
|
-
appendOverrideSection(lines, 'Instructions Guidance', overrides.instructionsGuidance)
|
|
603
|
-
appendOverrideSection(lines, 'Review Criteria', overrides.reviewCriteria)
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
lines.push('## Write Contract')
|
|
607
|
-
lines.push('')
|
|
608
|
-
lines.push('- Edit only Pluxx-managed generated sections.')
|
|
609
|
-
lines.push(`- Preserve custom sections marked by \`${PLUXX_CUSTOM_START}\` and \`${PLUXX_CUSTOM_END}\`.`)
|
|
610
|
-
lines.push('- Do not change auth wiring or target-platform config unless explicitly requested.')
|
|
611
|
-
lines.push('- Do not edit generated platform bundles in `dist/`.')
|
|
612
|
-
lines.push('')
|
|
613
|
-
lines.push('## Quality Bar')
|
|
614
|
-
lines.push('')
|
|
615
|
-
lines.push('- Each skill should represent a real user workflow or product surface.')
|
|
616
|
-
lines.push('- Setup, admin, account, and runtime workflows should be grouped intentionally.')
|
|
617
|
-
lines.push('- Prefer branded product language in user-facing content; avoid exposing raw MCP server identifiers unless they are operationally required.')
|
|
618
|
-
lines.push('- Avoid tiny singleton skills unless the surface is genuinely standalone.')
|
|
619
|
-
lines.push('- Examples should be concrete and specific, not generic placeholders.')
|
|
620
|
-
lines.push('- Weak MCP metadata (missing/generic tool descriptions) should be called out explicitly before publishing.')
|
|
621
|
-
lines.push('- The wording should match the MCP product narrative, not just raw tool names.')
|
|
622
|
-
lines.push('- Use discovered MCP resources and prompt templates when they clarify the real product surface.')
|
|
623
|
-
lines.push('- Respect the per-skill resource and prompt-template associations in the metadata/context unless stronger discovery evidence shows they are wrong.')
|
|
624
|
-
lines.push('- Keep INSTRUCTIONS.md as concise routing guidance; do not dump raw vendor documentation into generated sections.')
|
|
625
|
-
lines.push('')
|
|
626
|
-
|
|
627
|
-
return `${lines.join('\n')}\n`
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function buildAgentModePlanJson(
|
|
631
|
-
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
632
|
-
metadata: McpScaffoldMetadata,
|
|
633
|
-
lint: LintResult,
|
|
634
|
-
editableFiles: AgentPlanFile[],
|
|
635
|
-
protectedFiles: string[],
|
|
636
|
-
generatedFiles: string[],
|
|
637
|
-
contextSources: AgentContextSource[],
|
|
638
|
-
): string {
|
|
639
|
-
const serverEntry = Object.entries(config.mcp ?? {})[0]
|
|
640
|
-
const [serverName, server] = serverEntry ?? ['unknown', metadata.source]
|
|
641
|
-
const plan: AgentModePlanFile = {
|
|
642
|
-
version: 1,
|
|
643
|
-
plugin: {
|
|
644
|
-
name: config.name,
|
|
645
|
-
displayName: config.brand?.displayName ?? metadata.settings.displayName ?? config.name,
|
|
646
|
-
targets: [...config.targets],
|
|
647
|
-
},
|
|
648
|
-
mcp: {
|
|
649
|
-
metadataPath: MCP_SCAFFOLD_METADATA_PATH,
|
|
650
|
-
toolCount: metadata.tools.length,
|
|
651
|
-
serverName,
|
|
652
|
-
transport: server.transport,
|
|
653
|
-
auth: describeAuth(server),
|
|
654
|
-
},
|
|
655
|
-
contextInputs: contextSources.map((source) => source.label),
|
|
656
|
-
files: {
|
|
657
|
-
editable: editableFiles,
|
|
658
|
-
protected: protectedFiles,
|
|
659
|
-
generated: generatedFiles,
|
|
660
|
-
},
|
|
661
|
-
successCriteria: [
|
|
662
|
-
'Each skill represents a real user workflow or product surface.',
|
|
663
|
-
'Setup/admin/account tools are grouped intentionally.',
|
|
664
|
-
'Examples are concrete and realistic.',
|
|
665
|
-
'Weak MCP metadata is surfaced before publishing.',
|
|
666
|
-
'Only Pluxx-managed sections are modified.',
|
|
667
|
-
],
|
|
668
|
-
caveats: lint.issues.map((issue) => `[${issue.level}] ${issue.code}: ${issue.message}`),
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
return JSON.stringify(plan, null, 2)
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
async function collectAgentContextSources(
|
|
675
|
-
rootDir: string,
|
|
676
|
-
options: AgentPrepareOptions,
|
|
677
|
-
overrides: AgentOverrides | null,
|
|
678
|
-
): Promise<AgentContextSource[]> {
|
|
679
|
-
const sources: AgentContextSource[] = []
|
|
680
|
-
const seenFilePaths = new Set<string>()
|
|
681
|
-
|
|
682
|
-
if (options.websiteUrl) {
|
|
683
|
-
sources.push(await fetchContextSource(options.websiteUrl, 'website'))
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
if (options.docsUrl) {
|
|
687
|
-
sources.push(await fetchContextSource(options.docsUrl, 'docs'))
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
const contextPaths = [
|
|
691
|
-
...(overrides?.contextPaths ?? []),
|
|
692
|
-
...(options.contextPaths ?? []),
|
|
693
|
-
]
|
|
694
|
-
|
|
695
|
-
for (const relativePath of contextPaths) {
|
|
696
|
-
if (seenFilePaths.has(relativePath)) continue
|
|
697
|
-
seenFilePaths.add(relativePath)
|
|
698
|
-
const filePath = resolve(rootDir, relativePath)
|
|
699
|
-
if (!existsSync(filePath)) {
|
|
700
|
-
sources.push({
|
|
701
|
-
label: relativePath,
|
|
702
|
-
kind: 'file',
|
|
703
|
-
summary: `Unavailable: local file not found.`,
|
|
704
|
-
})
|
|
705
|
-
continue
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
const content = await Bun.file(filePath).text()
|
|
709
|
-
sources.push({
|
|
710
|
-
label: relativePath,
|
|
711
|
-
kind: 'file',
|
|
712
|
-
summary: summarizePlainText(content),
|
|
713
|
-
})
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
return sources
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
async function fetchContextSource(url: string, kind: 'website' | 'docs'): Promise<AgentContextSource> {
|
|
720
|
-
try {
|
|
721
|
-
const response = await fetch(url)
|
|
722
|
-
if (!response.ok) {
|
|
723
|
-
return {
|
|
724
|
-
label: url,
|
|
725
|
-
kind,
|
|
726
|
-
summary: `Unavailable: fetch failed with ${response.status} ${response.statusText}.`,
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
const contentType = response.headers.get('content-type') ?? ''
|
|
731
|
-
const body = await response.text()
|
|
732
|
-
|
|
733
|
-
return {
|
|
734
|
-
label: url,
|
|
735
|
-
kind,
|
|
736
|
-
summary: contentType.includes('html')
|
|
737
|
-
? summarizeHtml(body)
|
|
738
|
-
: summarizePlainText(body),
|
|
739
|
-
}
|
|
740
|
-
} catch (error) {
|
|
741
|
-
return {
|
|
742
|
-
label: url,
|
|
743
|
-
kind,
|
|
744
|
-
summary: `Unavailable: ${error instanceof Error ? error.message : String(error)}.`,
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
function summarizeHtml(html: string): string {
|
|
750
|
-
const title = matchHtmlTag(html, 'title')
|
|
751
|
-
const description = matchMetaDescription(html)
|
|
752
|
-
const headings = matchHtmlTags(html, ['h1', 'h2', 'h3']).slice(0, 5)
|
|
753
|
-
const paragraphs = matchHtmlTags(html, ['p']).slice(0, 3)
|
|
754
|
-
const lines: string[] = []
|
|
755
|
-
|
|
756
|
-
if (title) {
|
|
757
|
-
lines.push(`Title: ${title}`)
|
|
758
|
-
}
|
|
759
|
-
if (description) {
|
|
760
|
-
lines.push(`Description: ${description}`)
|
|
761
|
-
}
|
|
762
|
-
if (headings.length > 0) {
|
|
763
|
-
lines.push(`Headings: ${headings.join(' | ')}`)
|
|
764
|
-
}
|
|
765
|
-
if (paragraphs.length > 0) {
|
|
766
|
-
lines.push(`Excerpt: ${paragraphs.join(' ').slice(0, 900)}`)
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
return lines.join('\n')
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function summarizePlainText(content: string): string {
|
|
773
|
-
return content
|
|
774
|
-
.replace(/\s+/g, ' ')
|
|
775
|
-
.trim()
|
|
776
|
-
.slice(0, 1200)
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
function matchHtmlTag(html: string, tag: string): string | null {
|
|
780
|
-
const match = html.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, 'i'))
|
|
781
|
-
return match ? cleanHtmlText(match[1]) : null
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
function matchMetaDescription(html: string): string | null {
|
|
785
|
-
const match = html.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["'][^>]*>/i)
|
|
786
|
-
return match ? cleanHtmlText(match[1]) : null
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
function matchHtmlTags(html: string, tags: string[]): string[] {
|
|
790
|
-
const pattern = new RegExp(`<(?:${tags.join('|')})[^>]*>([\\s\\S]*?)</(?:${tags.join('|')})>`, 'ig')
|
|
791
|
-
const values: string[] = []
|
|
792
|
-
for (const match of html.matchAll(pattern)) {
|
|
793
|
-
const value = cleanHtmlText(match[1])
|
|
794
|
-
if (value) values.push(value)
|
|
795
|
-
}
|
|
796
|
-
return values
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
function cleanHtmlText(value: string): string {
|
|
800
|
-
return value
|
|
801
|
-
.replace(/<[^>]+>/g, ' ')
|
|
802
|
-
.replace(/ /g, ' ')
|
|
803
|
-
.replace(/&/g, '&')
|
|
804
|
-
.replace(/"/g, '"')
|
|
805
|
-
.replace(/'/g, "'")
|
|
806
|
-
.replace(/\s+/g, ' ')
|
|
807
|
-
.trim()
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
function buildAgentPrompt(
|
|
811
|
-
kind: AgentPromptKind,
|
|
812
|
-
input: {
|
|
813
|
-
pluginName: string
|
|
814
|
-
displayName: string
|
|
815
|
-
skillPaths: string[]
|
|
816
|
-
commandPaths: string[]
|
|
817
|
-
overrides: AgentOverrides | null
|
|
818
|
-
},
|
|
819
|
-
): string {
|
|
820
|
-
const sharedIntro = [
|
|
821
|
-
`# ${titleCase(kind)} Prompt`,
|
|
822
|
-
'',
|
|
823
|
-
`You are refining the Pluxx-generated plugin scaffold for \`${input.pluginName}\` (${input.displayName}).`,
|
|
824
|
-
'',
|
|
825
|
-
'Inputs:',
|
|
826
|
-
'- `.pluxx/agent/context.md`',
|
|
827
|
-
'- `.pluxx/agent/plan.json`',
|
|
828
|
-
`- \`${MCP_TAXONOMY_PATH}\``,
|
|
829
|
-
'- `INSTRUCTIONS.md`',
|
|
830
|
-
...input.skillPaths.map((path) => `- \`${path}\``),
|
|
831
|
-
...input.commandPaths.map((path) => `- \`${path}\``),
|
|
832
|
-
'',
|
|
833
|
-
'Rules:',
|
|
834
|
-
'- Only edit Pluxx-managed generated sections.',
|
|
835
|
-
`- Preserve all custom-note blocks between \`${PLUXX_CUSTOM_START}\` and \`${PLUXX_CUSTOM_END}\`.`,
|
|
836
|
-
'- Do not change auth wiring or target-platform config.',
|
|
837
|
-
'- Do not edit files under `dist/`.',
|
|
838
|
-
'- Treat discovered MCP resources, resource templates, and prompt templates as part of the product surface when they are present in the context and metadata.',
|
|
839
|
-
'- Treat per-skill related resources and prompt templates in the context as default evidence for workflow boundaries and examples unless stronger discovery evidence contradicts them.',
|
|
840
|
-
'',
|
|
841
|
-
]
|
|
842
|
-
|
|
843
|
-
if (kind === 'taxonomy') {
|
|
844
|
-
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 from tools, resources, resource templates, and prompt templates.\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.\n7. Use per-skill related resources and prompt templates as strong evidence for workflow shape, but correct them when broader discovery evidence shows a mismatch.\n8. Reject stale scaffold assumptions; if current files conflict with discovery context, prefer the discovery evidence and flag the mismatch.\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 and avoid weak command UX\n- per-skill resource and prompt-template associations remain coherent with the chosen taxonomy\n- taxonomy decisions are grounded in current discovery context, not stale scaffold assumptions\n`
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
if (kind === 'instructions') {
|
|
848
|
-
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. Use discovered tools, resources, resource templates, and prompt templates to produce short routing guidance, not a raw documentation dump.\n4. Keep wording aligned to the MCP's product narrative and branded language; avoid raw MCP server/tool identifiers except when technically required.\n5. Prefer the branded product name in user-facing copy; do not lead with internal MCP server identifiers.\n6. Replace stale scaffold claims with current discovery-backed language and keep command examples operational, concrete, and copy-paste runnable.\n7. When a workflow already has related resources or prompt templates in the context, keep the wording and examples aligned to that surfaced workflow evidence.\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 generated section reads like routing guidance, not pasted vendor docs\n- command examples use strong command UX (clear intent, realistic args, and runnable shapes)\n- workflow guidance stays coherent with related resource and prompt-template evidence in the context\n- the file remains safe for future \`pluxx sync --from-mcp\`\n`
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
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, raw documentation dumps, lexical skill names, stale scaffold assumptions, weak command UX, incoherent per-skill resource/prompt associations, 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- stale assumptions, incoherent per-skill discovery associations, and command-UX weaknesses are identified explicitly when present\n- suggested changes improve user-facing plugin quality\n- recommendations stay inside Pluxx-managed boundaries\n`
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function summarizeDiscoveryDescription(description: string | undefined, trailing?: string): string {
|
|
855
|
-
const base = description
|
|
856
|
-
?.replace(/\s+/g, ' ')
|
|
857
|
-
.trim()
|
|
858
|
-
.slice(0, 180)
|
|
859
|
-
return [base || 'Discovered during MCP introspection.', trailing].filter(Boolean).join(' ')
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
function buildAgentRunnerPrompt(kind: AgentPromptKind, promptPath: string): string {
|
|
863
|
-
const lines = [
|
|
864
|
-
'You are running inside a Pluxx-generated plugin scaffold.',
|
|
865
|
-
`Read and follow \`${AGENT_CONTEXT_PATH}\`, \`${AGENT_PLAN_PATH}\`, and \`${promptPath}\` before doing anything else.`,
|
|
866
|
-
'Use the prompt file as the task definition.',
|
|
867
|
-
'Respect the write contract in the plan file.',
|
|
868
|
-
`Preserve all custom-note blocks between \`${PLUXX_CUSTOM_START}\` and \`${PLUXX_CUSTOM_END}\`.`,
|
|
869
|
-
'Do not change auth wiring, target-platform config, or generated files under `dist/`.',
|
|
870
|
-
]
|
|
871
|
-
|
|
872
|
-
if (kind === 'review') {
|
|
873
|
-
lines.push('Do not edit files. Return findings only.')
|
|
874
|
-
} else {
|
|
875
|
-
lines.push('Edit only the Pluxx-managed generated sections allowed by the plan file.')
|
|
876
|
-
lines.push('Do not run lint, build, or tests; Pluxx will verify the result afterward.')
|
|
877
|
-
lines.push('When finished, provide a short summary of what you changed.')
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
return `${lines.join('\n')}\n`
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
function buildAgentRunnerCommand(
|
|
884
|
-
runner: AgentRunner,
|
|
885
|
-
kind: AgentPromptKind,
|
|
886
|
-
prompt: string,
|
|
887
|
-
options: {
|
|
888
|
-
model?: string
|
|
889
|
-
attach?: string
|
|
890
|
-
workspace?: string
|
|
891
|
-
} = {},
|
|
892
|
-
): string[] {
|
|
893
|
-
const binary = AGENT_RUNNER_BINARIES[runner]
|
|
894
|
-
|
|
895
|
-
if (runner === 'claude') {
|
|
896
|
-
const args = [binary]
|
|
897
|
-
if (options.model) {
|
|
898
|
-
args.push('--model', options.model)
|
|
899
|
-
}
|
|
900
|
-
args.push(
|
|
901
|
-
'--no-session-persistence',
|
|
902
|
-
'--verbose',
|
|
903
|
-
'--output-format',
|
|
904
|
-
'stream-json',
|
|
905
|
-
'--permission-mode',
|
|
906
|
-
kind === 'review' ? 'plan' : 'acceptEdits',
|
|
907
|
-
'-p',
|
|
908
|
-
prompt,
|
|
909
|
-
)
|
|
910
|
-
return args
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
if (runner === 'codex') {
|
|
914
|
-
// Codex headless edits can finish successfully and then stall during
|
|
915
|
-
// session persistence/finalization. Ephemeral mode keeps the non-interactive
|
|
916
|
-
// worker path stable for Pluxx agent/autopilot runs.
|
|
917
|
-
const args = [binary, 'exec', '--ephemeral', '--skip-git-repo-check']
|
|
918
|
-
if (options.model) {
|
|
919
|
-
args.push('--model', options.model)
|
|
920
|
-
}
|
|
921
|
-
if (kind !== 'review') {
|
|
922
|
-
args.push('--full-auto')
|
|
923
|
-
}
|
|
924
|
-
args.push(prompt)
|
|
925
|
-
return args
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
if (runner === 'cursor') {
|
|
929
|
-
if (!options.workspace) {
|
|
930
|
-
throw new Error('Cursor runner requires a workspace path.')
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
const args = [binary, '-p', '--trust', '--workspace', options.workspace]
|
|
934
|
-
if (kind !== 'review') {
|
|
935
|
-
args.push('--force')
|
|
936
|
-
}
|
|
937
|
-
if (options.model) {
|
|
938
|
-
args.push('--model', options.model)
|
|
939
|
-
}
|
|
940
|
-
args.push(prompt)
|
|
941
|
-
return args
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
const args = [binary, 'run']
|
|
945
|
-
if (options.model) {
|
|
946
|
-
args.push('--model', options.model)
|
|
947
|
-
}
|
|
948
|
-
if (options.attach) {
|
|
949
|
-
args.push('--attach', options.attach)
|
|
950
|
-
}
|
|
951
|
-
args.push(prompt)
|
|
952
|
-
return args
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
async function resolveAgentRunnerModel(
|
|
956
|
-
runner: AgentRunner,
|
|
957
|
-
explicitModel?: string,
|
|
958
|
-
): Promise<AgentRunnerModelSummary> {
|
|
959
|
-
if (explicitModel) {
|
|
960
|
-
return {
|
|
961
|
-
value: explicitModel,
|
|
962
|
-
source: 'explicit',
|
|
963
|
-
display: `${explicitModel} (explicit)`,
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
const detectedModel = runner === 'codex'
|
|
968
|
-
? await readCodexDefaultModel()
|
|
969
|
-
: runner === 'opencode'
|
|
970
|
-
? await readOpenCodeDefaultModel()
|
|
971
|
-
: runner === 'claude'
|
|
972
|
-
? await readClaudeDefaultModel()
|
|
973
|
-
: undefined
|
|
974
|
-
|
|
975
|
-
if (detectedModel) {
|
|
976
|
-
return {
|
|
977
|
-
value: detectedModel,
|
|
978
|
-
source: 'default',
|
|
979
|
-
display: `${detectedModel} (local default)`,
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
return {
|
|
984
|
-
source: 'unknown',
|
|
985
|
-
display: 'local default (CLI-managed)',
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
async function readCodexDefaultModel(): Promise<string | undefined> {
|
|
990
|
-
const codexHome = process.env.CODEX_HOME?.trim() || resolve(homedir(), '.codex')
|
|
991
|
-
return await readTomlStringValue(resolve(codexHome, 'config.toml'), 'model')
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
async function readOpenCodeDefaultModel(): Promise<string | undefined> {
|
|
995
|
-
const configHome = process.env.XDG_CONFIG_HOME?.trim() || resolve(homedir(), '.config')
|
|
996
|
-
const configPath = resolve(configHome, 'opencode', 'opencode.json')
|
|
997
|
-
const parsed = await readJsonFile(configPath)
|
|
998
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
999
|
-
return undefined
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
if (typeof parsed.model === 'string' && parsed.model.trim()) {
|
|
1003
|
-
return parsed.model.trim()
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
if (
|
|
1007
|
-
typeof parsed.default_agent === 'string'
|
|
1008
|
-
&& parsed.agent
|
|
1009
|
-
&& typeof parsed.agent === 'object'
|
|
1010
|
-
&& parsed.default_agent in parsed.agent
|
|
1011
|
-
) {
|
|
1012
|
-
const defaultAgent = parsed.agent[parsed.default_agent]
|
|
1013
|
-
if (
|
|
1014
|
-
defaultAgent
|
|
1015
|
-
&& typeof defaultAgent === 'object'
|
|
1016
|
-
&& 'model' in defaultAgent
|
|
1017
|
-
&& typeof defaultAgent.model === 'string'
|
|
1018
|
-
&& defaultAgent.model.trim()
|
|
1019
|
-
) {
|
|
1020
|
-
return defaultAgent.model.trim()
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
return undefined
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
async function readClaudeDefaultModel(): Promise<string | undefined> {
|
|
1028
|
-
for (const candidate of [
|
|
1029
|
-
resolve(homedir(), '.claude', 'settings.json'),
|
|
1030
|
-
resolve(homedir(), '.claude', 'settings.local.json'),
|
|
1031
|
-
resolve(homedir(), '.claude.json'),
|
|
1032
|
-
]) {
|
|
1033
|
-
const parsed = await readJsonFile(candidate)
|
|
1034
|
-
if (!parsed || typeof parsed !== 'object') continue
|
|
1035
|
-
for (const key of ['model', 'defaultModel', 'default_model']) {
|
|
1036
|
-
if (key in parsed && typeof parsed[key] === 'string' && parsed[key].trim()) {
|
|
1037
|
-
return parsed[key].trim()
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
return undefined
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
async function readTomlStringValue(filePath: string, key: string): Promise<string | undefined> {
|
|
1046
|
-
try {
|
|
1047
|
-
const raw = await readFile(filePath, 'utf8')
|
|
1048
|
-
const match = raw.match(new RegExp(`^\\s*${key}\\s*=\\s*"([^"]+)"\\s*$`, 'm'))
|
|
1049
|
-
return match?.[1]?.trim() || undefined
|
|
1050
|
-
} catch {
|
|
1051
|
-
return undefined
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
async function readJsonFile(filePath: string): Promise<Record<string, any> | undefined> {
|
|
1056
|
-
try {
|
|
1057
|
-
const raw = await readFile(filePath, 'utf8')
|
|
1058
|
-
return JSON.parse(raw) as Record<string, any>
|
|
1059
|
-
} catch {
|
|
1060
|
-
return undefined
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
async function ensureRunnerAvailable(runner: AgentRunner): Promise<void> {
|
|
1065
|
-
const binary = runner === 'cursor'
|
|
1066
|
-
? await resolveCursorBinary()
|
|
1067
|
-
: AGENT_RUNNER_BINARIES[runner]
|
|
1068
|
-
const available = binary ? await commandExists(binary) : false
|
|
1069
|
-
if (!available) {
|
|
1070
|
-
if (runner === 'cursor') {
|
|
1071
|
-
throw new Error('The cursor runner requires the Cursor CLI `agent` or `cursor-agent` binary on PATH. Install it with `curl https://cursor.com/install -fsS | bash` or choose a different runner.')
|
|
1072
|
-
}
|
|
1073
|
-
throw new Error(`The ${runner} runner is not available on PATH. Install \`${binary}\` or choose a different runner.`)
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
async function ensureRunnerAuthenticated(runner: AgentRunner): Promise<void> {
|
|
1078
|
-
if (runner !== 'cursor') return
|
|
1079
|
-
|
|
1080
|
-
if (process.env.CURSOR_API_KEY && process.env.CURSOR_API_KEY.trim().length > 0) {
|
|
1081
|
-
return
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
const binary = await resolveCursorBinary()
|
|
1085
|
-
const isAuthenticated = binary ? await commandSucceeds([binary, 'status']) : false
|
|
1086
|
-
if (!isAuthenticated) {
|
|
1087
|
-
throw new Error('Cursor CLI authentication is required. Run `agent login` (or `cursor-agent login`) or export `CURSOR_API_KEY` before running Pluxx with `--runner cursor`.')
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
async function resolveCursorBinary(): Promise<string | undefined> {
|
|
1092
|
-
for (const candidate of CURSOR_RUNNER_BINARIES) {
|
|
1093
|
-
if (await commandExists(candidate)) {
|
|
1094
|
-
return candidate
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
return undefined
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
async function commandExists(binary: string): Promise<boolean> {
|
|
1102
|
-
return await new Promise<boolean>((resolvePromise) => {
|
|
1103
|
-
const child = spawn('sh', ['-c', `command -v ${shellQuote(binary)} >/dev/null 2>&1`], {
|
|
1104
|
-
stdio: 'ignore',
|
|
1105
|
-
env: process.env,
|
|
1106
|
-
})
|
|
1107
|
-
child.on('close', (code) => resolvePromise(code === 0))
|
|
1108
|
-
child.on('error', () => resolvePromise(false))
|
|
1109
|
-
})
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
async function commandSucceeds(command: string[]): Promise<boolean> {
|
|
1113
|
-
return await new Promise<boolean>((resolvePromise) => {
|
|
1114
|
-
const child = spawn(command[0], command.slice(1), {
|
|
1115
|
-
stdio: 'ignore',
|
|
1116
|
-
env: process.env,
|
|
1117
|
-
})
|
|
1118
|
-
child.on('close', (code) => resolvePromise(code === 0))
|
|
1119
|
-
child.on('error', () => resolvePromise(false))
|
|
1120
|
-
})
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
async function executeCommand(
|
|
1124
|
-
command: string[],
|
|
1125
|
-
cwd: string,
|
|
1126
|
-
options: {
|
|
1127
|
-
streamOutput?: boolean
|
|
1128
|
-
env?: NodeJS.ProcessEnv
|
|
1129
|
-
} = {},
|
|
1130
|
-
): Promise<number> {
|
|
1131
|
-
const runtimeCommand = [...command]
|
|
1132
|
-
let codexOutputDir: string | null = null
|
|
1133
|
-
let codexLastMessagePath: string | null = null
|
|
1134
|
-
const isClaudeStreamJson = runtimeCommand[0] === 'claude'
|
|
1135
|
-
&& runtimeCommand.includes('--output-format')
|
|
1136
|
-
&& runtimeCommand.includes('stream-json')
|
|
1137
|
-
|
|
1138
|
-
if (runtimeCommand[0] === 'codex' && runtimeCommand[1] === 'exec') {
|
|
1139
|
-
codexOutputDir = await mkdtemp(resolve(tmpdir(), 'pluxx-codex-output-'))
|
|
1140
|
-
codexLastMessagePath = resolve(codexOutputDir, 'last-message.txt')
|
|
1141
|
-
runtimeCommand.splice(2, 0, '--json', '--output-last-message', codexLastMessagePath)
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
return await new Promise<number>((resolvePromise, reject) => {
|
|
1145
|
-
const child = spawn(runtimeCommand[0], runtimeCommand.slice(1), {
|
|
1146
|
-
cwd,
|
|
1147
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1148
|
-
env: options.env ?? process.env,
|
|
1149
|
-
})
|
|
1150
|
-
let killedAfterFinalMessage = false
|
|
1151
|
-
let sawFinalMessageAt: number | null = null
|
|
1152
|
-
let codexStdoutBuffer = ''
|
|
1153
|
-
let codexTurnCompleted = false
|
|
1154
|
-
let codexTurnFailed = false
|
|
1155
|
-
let claudeStdoutBuffer = ''
|
|
1156
|
-
let claudeTurnCompleted = false
|
|
1157
|
-
let claudeTurnFailed = false
|
|
1158
|
-
const sentinelInterval = (codexLastMessagePath || isClaudeStreamJson)
|
|
1159
|
-
? setInterval(() => {
|
|
1160
|
-
const sawCompletionSignal = codexTurnCompleted
|
|
1161
|
-
|| codexTurnFailed
|
|
1162
|
-
|| claudeTurnCompleted
|
|
1163
|
-
|| claudeTurnFailed
|
|
1164
|
-
|| (codexLastMessagePath ? existsSync(codexLastMessagePath) : false)
|
|
1165
|
-
if (!sawCompletionSignal) return
|
|
1166
|
-
if (sawFinalMessageAt == null) {
|
|
1167
|
-
sawFinalMessageAt = Date.now()
|
|
1168
|
-
return
|
|
1169
|
-
}
|
|
1170
|
-
if (!killedAfterFinalMessage && Date.now() - sawFinalMessageAt >= 1500) {
|
|
1171
|
-
killedAfterFinalMessage = true
|
|
1172
|
-
child.kill('SIGTERM')
|
|
1173
|
-
}
|
|
1174
|
-
}, 250)
|
|
1175
|
-
: null
|
|
1176
|
-
|
|
1177
|
-
const finalize = async (result: number, error?: Error): Promise<void> => {
|
|
1178
|
-
if (sentinelInterval) clearInterval(sentinelInterval)
|
|
1179
|
-
if (codexOutputDir) {
|
|
1180
|
-
await rm(codexOutputDir, { recursive: true, force: true })
|
|
1181
|
-
}
|
|
1182
|
-
if (error) {
|
|
1183
|
-
reject(error)
|
|
1184
|
-
return
|
|
1185
|
-
}
|
|
1186
|
-
resolvePromise(result)
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
child.stdout?.on('data', (chunk) => {
|
|
1190
|
-
const text = chunk.toString()
|
|
1191
|
-
if (codexLastMessagePath || isClaudeStreamJson) {
|
|
1192
|
-
const buffer = codexLastMessagePath ? codexStdoutBuffer + text : claudeStdoutBuffer + text
|
|
1193
|
-
const lines = buffer.split('\n')
|
|
1194
|
-
const remainder = lines.pop() ?? ''
|
|
1195
|
-
if (codexLastMessagePath) {
|
|
1196
|
-
codexStdoutBuffer = remainder
|
|
1197
|
-
} else {
|
|
1198
|
-
claudeStdoutBuffer = remainder
|
|
1199
|
-
}
|
|
1200
|
-
for (const line of lines) {
|
|
1201
|
-
const trimmed = line.trim()
|
|
1202
|
-
if (!trimmed) continue
|
|
1203
|
-
try {
|
|
1204
|
-
const event = JSON.parse(trimmed) as { type?: string; subtype?: string; is_error?: boolean }
|
|
1205
|
-
if (codexLastMessagePath) {
|
|
1206
|
-
if (event.type === 'turn.completed') {
|
|
1207
|
-
codexTurnCompleted = true
|
|
1208
|
-
} else if (event.type === 'turn.failed' || event.type === 'error') {
|
|
1209
|
-
codexTurnFailed = true
|
|
1210
|
-
}
|
|
1211
|
-
} else if (isClaudeStreamJson) {
|
|
1212
|
-
if (event.type === 'result') {
|
|
1213
|
-
if (event.is_error || event.subtype === 'error') {
|
|
1214
|
-
claudeTurnFailed = true
|
|
1215
|
-
} else {
|
|
1216
|
-
claudeTurnCompleted = true
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
} catch {
|
|
1221
|
-
// Ignore non-JSON lines. Codex still writes some human-readable output to stderr.
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
if (options.streamOutput) process.stdout.write(chunk)
|
|
1226
|
-
})
|
|
1227
|
-
child.stderr?.on('data', (chunk) => {
|
|
1228
|
-
if (options.streamOutput) process.stderr.write(chunk)
|
|
1229
|
-
})
|
|
1230
|
-
|
|
1231
|
-
child.on('error', (error) => {
|
|
1232
|
-
void finalize(1, error)
|
|
1233
|
-
})
|
|
1234
|
-
child.on('close', (code) => {
|
|
1235
|
-
const result = codexTurnFailed || claudeTurnFailed
|
|
1236
|
-
? 1
|
|
1237
|
-
: (killedAfterFinalMessage || codexTurnCompleted || claudeTurnCompleted ? 0 : (code ?? 1))
|
|
1238
|
-
void finalize(result)
|
|
1239
|
-
})
|
|
1240
|
-
})
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
async function prepareRunnerExecution(runner: AgentRunner): Promise<{
|
|
1244
|
-
env: NodeJS.ProcessEnv
|
|
1245
|
-
cleanup?: () => Promise<void>
|
|
1246
|
-
}> {
|
|
1247
|
-
if (runner === 'cursor') {
|
|
1248
|
-
const cursorBinary = await resolveCursorBinary()
|
|
1249
|
-
if (!cursorBinary || cursorBinary === AGENT_RUNNER_BINARIES.cursor) {
|
|
1250
|
-
return { env: process.env }
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
const shimDir = await mkdtemp(resolve(tmpdir(), 'pluxx-cursor-bin-'))
|
|
1254
|
-
const shimPath = resolve(shimDir, AGENT_RUNNER_BINARIES.cursor)
|
|
1255
|
-
await Bun.write(
|
|
1256
|
-
shimPath,
|
|
1257
|
-
`#!/bin/sh\nexec ${shellQuote(cursorBinary)} "$@"\n`,
|
|
1258
|
-
)
|
|
1259
|
-
await chmod(shimPath, 0o755)
|
|
1260
|
-
|
|
1261
|
-
return {
|
|
1262
|
-
env: {
|
|
1263
|
-
...process.env,
|
|
1264
|
-
PATH: `${shimDir}:${process.env.PATH ?? ''}`,
|
|
1265
|
-
},
|
|
1266
|
-
cleanup: async () => {
|
|
1267
|
-
await rm(shimDir, { recursive: true, force: true })
|
|
1268
|
-
},
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
if (runner !== 'codex') {
|
|
1273
|
-
return { env: process.env }
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
const currentCodexHome = process.env.CODEX_HOME?.trim() || resolve(homedir(), '.codex')
|
|
1277
|
-
const isolatedCodexHome = await mkdtemp(resolve(tmpdir(), 'pluxx-codex-home-'))
|
|
1278
|
-
await mkdir(resolve(isolatedCodexHome, 'memories'), { recursive: true })
|
|
1279
|
-
|
|
1280
|
-
for (const relativePath of ['auth.json', 'config.toml', 'hooks.json', 'installation_id']) {
|
|
1281
|
-
const sourcePath = resolve(currentCodexHome, relativePath)
|
|
1282
|
-
if (!existsSync(sourcePath)) continue
|
|
1283
|
-
await copyFile(sourcePath, resolve(isolatedCodexHome, relativePath))
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
const rulesSourceDir = resolve(currentCodexHome, 'rules')
|
|
1287
|
-
if (existsSync(rulesSourceDir)) {
|
|
1288
|
-
const rulesTargetDir = resolve(isolatedCodexHome, 'rules')
|
|
1289
|
-
await mkdir(rulesTargetDir, { recursive: true })
|
|
1290
|
-
const defaultRulesPath = resolve(rulesSourceDir, 'default.rules')
|
|
1291
|
-
if (existsSync(defaultRulesPath)) {
|
|
1292
|
-
await copyFile(defaultRulesPath, resolve(rulesTargetDir, 'default.rules'))
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
return {
|
|
1297
|
-
env: {
|
|
1298
|
-
...process.env,
|
|
1299
|
-
CODEX_HOME: isolatedCodexHome,
|
|
1300
|
-
},
|
|
1301
|
-
cleanup: async () => {
|
|
1302
|
-
await rm(isolatedCodexHome, { recursive: true, force: true })
|
|
1303
|
-
},
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
function shellQuote(value: string): string {
|
|
1308
|
-
if (/^[A-Za-z0-9_/:=.,-]+$/.test(value)) {
|
|
1309
|
-
return value
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
return `'${value.replace(/'/g, `'\"'\"'`)}'`
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
function describeAuth(server: { auth?: { type: string; envVar?: string; headerName?: string } }): string {
|
|
1316
|
-
const auth = server.auth
|
|
1317
|
-
if (!auth || auth.type === 'none') {
|
|
1318
|
-
return 'none'
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
if (auth.type === 'header') {
|
|
1322
|
-
return `header via ${auth.headerName ?? 'custom header'} from ${auth.envVar ?? 'env'}`
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
if (auth.type === 'platform') {
|
|
1326
|
-
return 'platform-managed auth'
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
return `bearer via ${auth.envVar ?? 'env'}`
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
function titleCase(value: string): string {
|
|
1333
|
-
return value.charAt(0).toUpperCase() + value.slice(1)
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
async function loadAgentOverrides(rootDir: string): Promise<AgentOverrides | null> {
|
|
1337
|
-
const overridesPath = resolve(rootDir, AGENT_OVERRIDES_PATH)
|
|
1338
|
-
if (!existsSync(overridesPath)) {
|
|
1339
|
-
return null
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
const content = await Bun.file(overridesPath).text()
|
|
1343
|
-
return parseAgentOverrides(content, AGENT_OVERRIDES_PATH)
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
function parseAgentOverrides(content: string, path: string): AgentOverrides {
|
|
1347
|
-
const sections = new Map<string, string[]>()
|
|
1348
|
-
let currentSection: string | null = null
|
|
1349
|
-
|
|
1350
|
-
for (const rawLine of content.split(/\r?\n/)) {
|
|
1351
|
-
const heading = rawLine.match(/^##\s+(.+?)\s*$/)
|
|
1352
|
-
if (heading) {
|
|
1353
|
-
currentSection = normalizeOverrideHeading(heading[1])
|
|
1354
|
-
if (currentSection && !sections.has(currentSection)) {
|
|
1355
|
-
sections.set(currentSection, [])
|
|
1356
|
-
}
|
|
1357
|
-
continue
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
if (!currentSection) continue
|
|
1361
|
-
sections.get(currentSection)?.push(rawLine)
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
const contextPaths = extractListItems(sections.get('context-paths') ?? [])
|
|
1365
|
-
|
|
1366
|
-
return {
|
|
1367
|
-
path,
|
|
1368
|
-
contextPaths,
|
|
1369
|
-
productHints: normalizeOverrideBody(sections.get('product-hints')),
|
|
1370
|
-
setupAuthNotes: normalizeOverrideBody(sections.get('setup-auth-notes')),
|
|
1371
|
-
groupingHints: normalizeOverrideBody(sections.get('grouping-hints')),
|
|
1372
|
-
taxonomyGuidance: normalizeOverrideBody(sections.get('taxonomy-guidance')),
|
|
1373
|
-
instructionsGuidance: normalizeOverrideBody(sections.get('instructions-guidance')),
|
|
1374
|
-
reviewCriteria: normalizeOverrideBody(sections.get('review-criteria')),
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
function normalizeOverrideHeading(value: string): string | null {
|
|
1379
|
-
const normalized = value
|
|
1380
|
-
.trim()
|
|
1381
|
-
.toLowerCase()
|
|
1382
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
1383
|
-
.replace(/^-+|-+$/g, '')
|
|
1384
|
-
|
|
1385
|
-
const aliases: Record<string, string> = {
|
|
1386
|
-
'context-paths': 'context-paths',
|
|
1387
|
-
'context-files': 'context-paths',
|
|
1388
|
-
'product-hints': 'product-hints',
|
|
1389
|
-
'setup-auth-notes': 'setup-auth-notes',
|
|
1390
|
-
'setup-and-auth-notes': 'setup-auth-notes',
|
|
1391
|
-
'setup-auth-guidance': 'setup-auth-notes',
|
|
1392
|
-
'grouping-hints': 'grouping-hints',
|
|
1393
|
-
'tool-grouping-hints': 'grouping-hints',
|
|
1394
|
-
'taxonomy-guidance': 'taxonomy-guidance',
|
|
1395
|
-
'instructions-guidance': 'instructions-guidance',
|
|
1396
|
-
'review-criteria': 'review-criteria',
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
return aliases[normalized] ?? null
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
function extractListItems(lines: string[]): string[] {
|
|
1403
|
-
return lines
|
|
1404
|
-
.map((line) => line.trim())
|
|
1405
|
-
.filter((line) => line.startsWith('- ') || line.startsWith('* '))
|
|
1406
|
-
.map((line) => line.slice(2).trim())
|
|
1407
|
-
.filter(Boolean)
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
function normalizeOverrideBody(lines: string[] | undefined): string | undefined {
|
|
1411
|
-
if (!lines) return undefined
|
|
1412
|
-
const value = lines.join('\n').trim()
|
|
1413
|
-
return value || undefined
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
function appendOverrideSection(lines: string[], heading: string, content: string | undefined): void {
|
|
1417
|
-
if (!content) return
|
|
1418
|
-
lines.push(`### ${heading}`)
|
|
1419
|
-
lines.push('')
|
|
1420
|
-
lines.push(content)
|
|
1421
|
-
lines.push('')
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
function buildPromptOverrideBlock(kind: AgentPromptKind, overrides: AgentOverrides | null): string {
|
|
1425
|
-
if (!overrides) return ''
|
|
1426
|
-
|
|
1427
|
-
const additions: string[] = []
|
|
1428
|
-
|
|
1429
|
-
if (overrides.productHints) {
|
|
1430
|
-
additions.push(`Product hints:\n${overrides.productHints}`)
|
|
1431
|
-
}
|
|
1432
|
-
if (overrides.setupAuthNotes) {
|
|
1433
|
-
additions.push(`Setup/auth notes:\n${overrides.setupAuthNotes}`)
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
if (kind === 'taxonomy') {
|
|
1437
|
-
if (overrides.groupingHints) {
|
|
1438
|
-
additions.push(`Grouping hints:\n${overrides.groupingHints}`)
|
|
1439
|
-
}
|
|
1440
|
-
if (overrides.taxonomyGuidance) {
|
|
1441
|
-
additions.push(`Taxonomy guidance:\n${overrides.taxonomyGuidance}`)
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
if (kind === 'instructions' && overrides.instructionsGuidance) {
|
|
1446
|
-
additions.push(`Instructions guidance:\n${overrides.instructionsGuidance}`)
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
if (kind === 'review' && overrides.reviewCriteria) {
|
|
1450
|
-
additions.push(`Additional review criteria:\n${overrides.reviewCriteria}`)
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
if (additions.length === 0) return ''
|
|
1454
|
-
return `\nProject overrides:\n${additions.map((block) => `- ${block.replace(/\n/g, '\n ')}`).join('\n')}\n`
|
|
1455
|
-
}
|