@yuaone/core 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-loop.d.ts +62 -0
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +705 -18
- package/dist/agent-loop.js.map +1 -1
- package/dist/background-agent.d.ts +110 -0
- package/dist/background-agent.d.ts.map +1 -0
- package/dist/background-agent.js +255 -0
- package/dist/background-agent.js.map +1 -0
- package/dist/coding-standards.d.ts +45 -0
- package/dist/coding-standards.d.ts.map +1 -0
- package/dist/coding-standards.js +1152 -0
- package/dist/coding-standards.js.map +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -6
- package/dist/constants.js.map +1 -1
- package/dist/context-manager.d.ts +6 -0
- package/dist/context-manager.d.ts.map +1 -1
- package/dist/context-manager.js +23 -4
- package/dist/context-manager.js.map +1 -1
- package/dist/index.d.ts +28 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -2
- package/dist/index.js.map +1 -1
- package/dist/llm-client.d.ts +8 -3
- package/dist/llm-client.d.ts.map +1 -1
- package/dist/llm-client.js +64 -13
- package/dist/llm-client.js.map +1 -1
- package/dist/plugin-auto-loader.d.ts +108 -0
- package/dist/plugin-auto-loader.d.ts.map +1 -0
- package/dist/plugin-auto-loader.js +743 -0
- package/dist/plugin-auto-loader.js.map +1 -0
- package/dist/plugin-registry.d.ts +112 -0
- package/dist/plugin-registry.d.ts.map +1 -0
- package/dist/plugin-registry.js +319 -0
- package/dist/plugin-registry.js.map +1 -0
- package/dist/plugin-types.d.ts +388 -0
- package/dist/plugin-types.d.ts.map +1 -0
- package/dist/plugin-types.js +8 -0
- package/dist/plugin-types.js.map +1 -0
- package/dist/plugin-validator.d.ts +54 -0
- package/dist/plugin-validator.d.ts.map +1 -0
- package/dist/plugin-validator.js +129 -0
- package/dist/plugin-validator.js.map +1 -0
- package/dist/repo-knowledge-graph.d.ts +112 -0
- package/dist/repo-knowledge-graph.d.ts.map +1 -0
- package/dist/repo-knowledge-graph.js +561 -0
- package/dist/repo-knowledge-graph.js.map +1 -0
- package/dist/role-registry.js +1 -1
- package/dist/role-registry.js.map +1 -1
- package/dist/self-debug-loop.d.ts +257 -0
- package/dist/self-debug-loop.d.ts.map +1 -0
- package/dist/self-debug-loop.js +870 -0
- package/dist/self-debug-loop.js.map +1 -0
- package/dist/skill-learner.d.ts +136 -0
- package/dist/skill-learner.d.ts.map +1 -0
- package/dist/skill-learner.js +382 -0
- package/dist/skill-learner.js.map +1 -0
- package/dist/skill-loader.d.ts +90 -0
- package/dist/skill-loader.d.ts.map +1 -0
- package/dist/skill-loader.js +309 -0
- package/dist/skill-loader.js.map +1 -0
- package/dist/specialist-registry.d.ts +132 -0
- package/dist/specialist-registry.d.ts.map +1 -0
- package/dist/specialist-registry.js +413 -0
- package/dist/specialist-registry.js.map +1 -0
- package/dist/sub-agent-prompts.d.ts +45 -0
- package/dist/sub-agent-prompts.d.ts.map +1 -0
- package/dist/sub-agent-prompts.js +177 -0
- package/dist/sub-agent-prompts.js.map +1 -0
- package/dist/sub-agent-router.d.ts +75 -0
- package/dist/sub-agent-router.d.ts.map +1 -0
- package/dist/sub-agent-router.js +174 -0
- package/dist/sub-agent-router.js.map +1 -0
- package/dist/sub-agent.d.ts +48 -0
- package/dist/sub-agent.d.ts.map +1 -1
- package/dist/sub-agent.js +108 -5
- package/dist/sub-agent.js.map +1 -1
- package/dist/system-prompt.d.ts +26 -0
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +177 -7
- package/dist/system-prompt.js.map +1 -1
- package/dist/task-classifier.d.ts +25 -1
- package/dist/task-classifier.d.ts.map +1 -1
- package/dist/task-classifier.js +171 -1
- package/dist/task-classifier.js.map +1 -1
- package/dist/tool-planner.d.ts +160 -0
- package/dist/tool-planner.d.ts.map +1 -0
- package/dist/tool-planner.js +501 -0
- package/dist/tool-planner.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/world-state.d.ts.map +1 -1
- package/dist/world-state.js +8 -1
- package/dist/world-state.js.map +1 -1
- package/package.json +2 -1
- package/plugins/git/patterns/branch-patterns.json +101 -0
- package/plugins/git/patterns/commit-patterns.json +186 -0
- package/plugins/git/plugin.yaml +128 -0
- package/plugins/git/skills/branch-strategy.md +172 -0
- package/plugins/git/skills/commit-conv.md +178 -0
- package/plugins/git/skills/conflict-resolve.md +159 -0
- package/plugins/git/skills/history-clean.md +199 -0
- package/plugins/git/skills/pr-review.md +196 -0
- package/plugins/git/strategies/conflict-resolve.json +244 -0
- package/plugins/git/strategies/release-flow.json +292 -0
- package/plugins/git/validators/rules.json +348 -0
- package/plugins/react/patterns/anti-patterns.json +88 -0
- package/plugins/react/patterns/components.json +80 -0
- package/plugins/react/patterns/hooks.json +72 -0
- package/plugins/react/plugin.yaml +229 -0
- package/plugins/react/skills/bugfix.md +208 -0
- package/plugins/react/skills/component-gen.md +206 -0
- package/plugins/react/skills/hook-extract.md +208 -0
- package/plugins/react/skills/ssr.md +256 -0
- package/plugins/react/skills/test.md +273 -0
- package/plugins/react/strategies/build-fix.json +43 -0
- package/plugins/react/strategies/hook-loop-fix.json +36 -0
- package/plugins/react/strategies/hydration-fix.json +42 -0
- package/plugins/react/validators/rules.json +92 -0
- package/plugins/typescript/patterns/best-practices.json +25 -0
- package/plugins/typescript/patterns/common-errors.json +32 -0
- package/plugins/typescript/plugin.yaml +74 -0
- package/plugins/typescript/skills/debug.md +23 -0
- package/plugins/typescript/skills/migration.md +24 -0
- package/plugins/typescript/skills/refactor.md +22 -0
- package/plugins/typescript/skills/strict-mode.md +23 -0
- package/plugins/typescript/strategies/strict-migration.json +37 -0
- package/plugins/typescript/strategies/type-error-fix.json +37 -0
- package/plugins/typescript/validators/rules.json +28 -0
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PluginAutoLoader — Discovers and registers plugins at startup.
|
|
3
|
+
*
|
|
4
|
+
* Scan order:
|
|
5
|
+
* 1. Built-in plugins (packages/yuan-core/plugins/)
|
|
6
|
+
* 2. Project-local plugins (.yuan/plugins/)
|
|
7
|
+
* 3. User global plugins (~/.yuan/plugins/)
|
|
8
|
+
* 4. npm-installed plugins (node_modules/@yuaone/plugin-*, node_modules/yuan-plugin-*)
|
|
9
|
+
*
|
|
10
|
+
* Each plugin's detect conditions are checked against the project.
|
|
11
|
+
* Matching plugins are auto-registered.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
/**
|
|
17
|
+
* Minimal YAML parser that handles:
|
|
18
|
+
* - Key-value pairs
|
|
19
|
+
* - Arrays (with - prefix)
|
|
20
|
+
* - Nested objects (via indentation)
|
|
21
|
+
* - String values (quoted and unquoted)
|
|
22
|
+
* - Boolean and number values
|
|
23
|
+
*
|
|
24
|
+
* Does NOT handle multi-line strings, anchors, tags, or flow style.
|
|
25
|
+
*/
|
|
26
|
+
function parseSimpleYaml(text) {
|
|
27
|
+
const lines = text.split("\n");
|
|
28
|
+
return parseBlock(lines, 0, 0).value;
|
|
29
|
+
}
|
|
30
|
+
function parseBlock(lines, startIdx, parentIndent) {
|
|
31
|
+
const result = {};
|
|
32
|
+
let i = startIdx;
|
|
33
|
+
while (i < lines.length) {
|
|
34
|
+
const line = lines[i];
|
|
35
|
+
// Skip empty lines and comments
|
|
36
|
+
if (line.trim() === "" || line.trim().startsWith("#")) {
|
|
37
|
+
i++;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const indent = getIndent(line);
|
|
41
|
+
// If indentation drops below or to parent level, this block is done
|
|
42
|
+
if (indent < parentIndent) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
// If exactly at parent indent, also done (sibling of parent)
|
|
46
|
+
if (indent < parentIndent) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
// Only process lines at our expected indent level
|
|
50
|
+
if (i === startIdx) {
|
|
51
|
+
// First line sets the indent for this block
|
|
52
|
+
}
|
|
53
|
+
const trimmed = line.trim();
|
|
54
|
+
// Array item at this level: "- value" or "- key: value"
|
|
55
|
+
if (trimmed.startsWith("- ")) {
|
|
56
|
+
// This is an array — but arrays are handled within key parsing
|
|
57
|
+
// If we hit a bare array at block level, skip
|
|
58
|
+
i++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// Key-value pair
|
|
62
|
+
const colonIdx = trimmed.indexOf(":");
|
|
63
|
+
if (colonIdx === -1) {
|
|
64
|
+
i++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const key = trimmed.substring(0, colonIdx).trim();
|
|
68
|
+
const afterColon = trimmed.substring(colonIdx + 1).trim();
|
|
69
|
+
if (afterColon === "" || afterColon === "|" || afterColon === ">") {
|
|
70
|
+
// Value is a nested block (object or array) on next lines
|
|
71
|
+
const childIndent = findChildIndent(lines, i + 1);
|
|
72
|
+
if (childIndent <= indent) {
|
|
73
|
+
// Empty value
|
|
74
|
+
result[key] = null;
|
|
75
|
+
i++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Check if children are array items
|
|
79
|
+
const nextNonEmpty = findNextNonEmpty(lines, i + 1);
|
|
80
|
+
if (nextNonEmpty !== -1 && lines[nextNonEmpty].trim().startsWith("- ")) {
|
|
81
|
+
const arrayResult = parseArray(lines, i + 1, childIndent);
|
|
82
|
+
result[key] = arrayResult.value;
|
|
83
|
+
i = i + 1 + arrayResult.consumed;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Nested object
|
|
87
|
+
const blockResult = parseBlock(lines, i + 1, childIndent);
|
|
88
|
+
result[key] = blockResult.value;
|
|
89
|
+
i = i + 1 + blockResult.consumed;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// Inline value
|
|
94
|
+
result[key] = parseScalar(afterColon);
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { value: result, consumed: i - startIdx };
|
|
99
|
+
}
|
|
100
|
+
function parseArray(lines, startIdx, expectedIndent) {
|
|
101
|
+
const result = [];
|
|
102
|
+
let i = startIdx;
|
|
103
|
+
while (i < lines.length) {
|
|
104
|
+
const line = lines[i];
|
|
105
|
+
if (line.trim() === "" || line.trim().startsWith("#")) {
|
|
106
|
+
i++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const indent = getIndent(line);
|
|
110
|
+
if (indent < expectedIndent) {
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
const trimmed = line.trim();
|
|
114
|
+
if (!trimmed.startsWith("- ")) {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
const itemContent = trimmed.substring(2).trim();
|
|
118
|
+
// Check if the array item has a colon (object item)
|
|
119
|
+
const colonIdx = itemContent.indexOf(":");
|
|
120
|
+
if (colonIdx !== -1 && !isQuotedColon(itemContent, colonIdx)) {
|
|
121
|
+
// Could be an inline object like "- key: value"
|
|
122
|
+
// or start of a nested object block
|
|
123
|
+
const key = itemContent.substring(0, colonIdx).trim();
|
|
124
|
+
const afterColon = itemContent.substring(colonIdx + 1).trim();
|
|
125
|
+
// Build an object for this array item
|
|
126
|
+
const obj = {};
|
|
127
|
+
if (afterColon === "") {
|
|
128
|
+
// Nested block under this array item key
|
|
129
|
+
const childIndent = findChildIndent(lines, i + 1);
|
|
130
|
+
if (childIndent > indent) {
|
|
131
|
+
const blockResult = parseBlock(lines, i + 1, childIndent);
|
|
132
|
+
obj[key] = blockResult.value;
|
|
133
|
+
i = i + 1 + blockResult.consumed;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
obj[key] = null;
|
|
137
|
+
i++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
obj[key] = parseScalar(afterColon);
|
|
142
|
+
i++;
|
|
143
|
+
}
|
|
144
|
+
// Check for additional keys at deeper indent (part of same object)
|
|
145
|
+
const nextChildIndent = indent + 2;
|
|
146
|
+
while (i < lines.length) {
|
|
147
|
+
const nextLine = lines[i];
|
|
148
|
+
if (nextLine.trim() === "" || nextLine.trim().startsWith("#")) {
|
|
149
|
+
i++;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const nextIndent = getIndent(nextLine);
|
|
153
|
+
if (nextIndent < nextChildIndent)
|
|
154
|
+
break;
|
|
155
|
+
if (nextIndent !== nextChildIndent)
|
|
156
|
+
break;
|
|
157
|
+
const nextTrimmed = nextLine.trim();
|
|
158
|
+
if (nextTrimmed.startsWith("- "))
|
|
159
|
+
break;
|
|
160
|
+
const nextColonIdx = nextTrimmed.indexOf(":");
|
|
161
|
+
if (nextColonIdx === -1)
|
|
162
|
+
break;
|
|
163
|
+
const nextKey = nextTrimmed.substring(0, nextColonIdx).trim();
|
|
164
|
+
const nextAfterColon = nextTrimmed.substring(nextColonIdx + 1).trim();
|
|
165
|
+
if (nextAfterColon === "") {
|
|
166
|
+
const deepChildIndent = findChildIndent(lines, i + 1);
|
|
167
|
+
if (deepChildIndent > nextIndent) {
|
|
168
|
+
const nextNonEmpty = findNextNonEmpty(lines, i + 1);
|
|
169
|
+
if (nextNonEmpty !== -1 && lines[nextNonEmpty].trim().startsWith("- ")) {
|
|
170
|
+
const arrResult = parseArray(lines, i + 1, deepChildIndent);
|
|
171
|
+
obj[nextKey] = arrResult.value;
|
|
172
|
+
i = i + 1 + arrResult.consumed;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const blockResult = parseBlock(lines, i + 1, deepChildIndent);
|
|
176
|
+
obj[nextKey] = blockResult.value;
|
|
177
|
+
i = i + 1 + blockResult.consumed;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
obj[nextKey] = null;
|
|
182
|
+
i++;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
obj[nextKey] = parseScalar(nextAfterColon);
|
|
187
|
+
i++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
result.push(obj);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// Simple scalar item
|
|
194
|
+
result.push(parseScalar(itemContent));
|
|
195
|
+
i++;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { value: result, consumed: i - startIdx };
|
|
199
|
+
}
|
|
200
|
+
function parseScalar(value) {
|
|
201
|
+
if (value === "" || value === "null" || value === "~")
|
|
202
|
+
return null;
|
|
203
|
+
// Quoted strings
|
|
204
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
205
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
206
|
+
return value.slice(1, -1);
|
|
207
|
+
}
|
|
208
|
+
// Booleans
|
|
209
|
+
const lower = value.toLowerCase();
|
|
210
|
+
if (lower === "true" || lower === "yes" || lower === "on")
|
|
211
|
+
return true;
|
|
212
|
+
if (lower === "false" || lower === "no" || lower === "off")
|
|
213
|
+
return false;
|
|
214
|
+
// Numbers
|
|
215
|
+
if (/^-?\d+$/.test(value))
|
|
216
|
+
return parseInt(value, 10);
|
|
217
|
+
if (/^-?\d+\.\d+$/.test(value))
|
|
218
|
+
return parseFloat(value);
|
|
219
|
+
// Strip inline comments
|
|
220
|
+
const commentIdx = value.indexOf(" #");
|
|
221
|
+
if (commentIdx !== -1) {
|
|
222
|
+
return value.substring(0, commentIdx).trim();
|
|
223
|
+
}
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
function getIndent(line) {
|
|
227
|
+
const match = line.match(/^(\s*)/);
|
|
228
|
+
return match ? match[1].length : 0;
|
|
229
|
+
}
|
|
230
|
+
function findChildIndent(lines, startIdx) {
|
|
231
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
232
|
+
const line = lines[i];
|
|
233
|
+
if (line.trim() !== "" && !line.trim().startsWith("#")) {
|
|
234
|
+
return getIndent(line);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
function findNextNonEmpty(lines, startIdx) {
|
|
240
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
241
|
+
if (lines[i].trim() !== "" && !lines[i].trim().startsWith("#")) {
|
|
242
|
+
return i;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return -1;
|
|
246
|
+
}
|
|
247
|
+
function isQuotedColon(text, colonIdx) {
|
|
248
|
+
// Check if the colon is inside quotes
|
|
249
|
+
let inSingle = false;
|
|
250
|
+
let inDouble = false;
|
|
251
|
+
for (let i = 0; i < colonIdx; i++) {
|
|
252
|
+
if (text[i] === "'" && !inDouble)
|
|
253
|
+
inSingle = !inSingle;
|
|
254
|
+
if (text[i] === '"' && !inSingle)
|
|
255
|
+
inDouble = !inDouble;
|
|
256
|
+
}
|
|
257
|
+
return inSingle || inDouble;
|
|
258
|
+
}
|
|
259
|
+
export class PluginAutoLoader {
|
|
260
|
+
config;
|
|
261
|
+
/** Lifecycle hooks stored per plugin ID */
|
|
262
|
+
lifecycleHooks = new Map();
|
|
263
|
+
constructor(config) {
|
|
264
|
+
this.config = config;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Invoke a lifecycle hook for a plugin.
|
|
268
|
+
* Returns the shell command string if the hook exists, or null if not defined.
|
|
269
|
+
* The caller (agent) is responsible for executing the command — we don't
|
|
270
|
+
* auto-execute for security reasons.
|
|
271
|
+
*/
|
|
272
|
+
invokeHook(pluginId, hookName, _context) {
|
|
273
|
+
const hooks = this.lifecycleHooks.get(pluginId);
|
|
274
|
+
if (!hooks)
|
|
275
|
+
return null;
|
|
276
|
+
const command = hooks[hookName];
|
|
277
|
+
if (!command || typeof command !== "string")
|
|
278
|
+
return null;
|
|
279
|
+
return command;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Get all stored lifecycle hooks for a plugin.
|
|
283
|
+
*/
|
|
284
|
+
getLifecycleHooks(pluginId) {
|
|
285
|
+
return this.lifecycleHooks.get(pluginId);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Discover and register all matching plugins.
|
|
289
|
+
*/
|
|
290
|
+
async loadAll(registry) {
|
|
291
|
+
const result = { loaded: [], skipped: [], errors: [] };
|
|
292
|
+
// 1. Find all plugin directories
|
|
293
|
+
const pluginDirs = this.discoverPluginDirs();
|
|
294
|
+
// 2. Parse each plugin.yaml and register
|
|
295
|
+
for (const dir of pluginDirs) {
|
|
296
|
+
try {
|
|
297
|
+
const manifest = this.parsePluginYaml(dir);
|
|
298
|
+
if (!manifest) {
|
|
299
|
+
result.errors.push({
|
|
300
|
+
pluginId: dir,
|
|
301
|
+
error: "Invalid or missing plugin.yaml",
|
|
302
|
+
});
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
// 3. Check detect conditions
|
|
306
|
+
if (!this.config.loadAll && !this.matchesDetect(manifest)) {
|
|
307
|
+
result.skipped.push(manifest.id);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
// 4. Register plugin
|
|
311
|
+
registry.register(manifest);
|
|
312
|
+
result.loaded.push(manifest.id);
|
|
313
|
+
// 5. Store lifecycle hooks if defined in manifest YAML
|
|
314
|
+
this.storeLifecycleHooks(manifest, dir);
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
result.errors.push({
|
|
318
|
+
pluginId: dir,
|
|
319
|
+
error: err instanceof Error ? err.message : String(err),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Extract and store lifecycle hooks from a plugin's manifest directory.
|
|
327
|
+
* Looks for a "lifecycle" section in the parsed YAML (keyed by hook name → command string).
|
|
328
|
+
* If none found, checks for a lifecycle.yaml/yml file in the plugin directory.
|
|
329
|
+
*/
|
|
330
|
+
storeLifecycleHooks(manifest, pluginDir) {
|
|
331
|
+
// Try to read lifecycle hooks from the manifest YAML's raw "lifecycle" key
|
|
332
|
+
// Since our parsePluginYaml doesn't extract lifecycle, re-read it minimally
|
|
333
|
+
let lifecyclePath = path.join(pluginDir, "plugin.yaml");
|
|
334
|
+
if (!fs.existsSync(lifecyclePath)) {
|
|
335
|
+
lifecyclePath = path.join(pluginDir, "plugin.yml");
|
|
336
|
+
if (!fs.existsSync(lifecyclePath))
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
const content = fs.readFileSync(lifecyclePath, "utf-8");
|
|
341
|
+
// Quick check: does the file mention "lifecycle"?
|
|
342
|
+
if (!content.includes("lifecycle"))
|
|
343
|
+
return;
|
|
344
|
+
// Extract lifecycle section via simple line parsing
|
|
345
|
+
const hooks = {};
|
|
346
|
+
const lines = content.split("\n");
|
|
347
|
+
let inLifecycle = false;
|
|
348
|
+
let lifecycleIndent = -1;
|
|
349
|
+
for (const line of lines) {
|
|
350
|
+
const trimmed = line.trim();
|
|
351
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
352
|
+
continue;
|
|
353
|
+
const indent = line.length - line.trimStart().length;
|
|
354
|
+
if (trimmed.startsWith("lifecycle:")) {
|
|
355
|
+
inLifecycle = true;
|
|
356
|
+
lifecycleIndent = indent;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (inLifecycle) {
|
|
360
|
+
if (indent <= lifecycleIndent) {
|
|
361
|
+
// Exited lifecycle block
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
const colonIdx = trimmed.indexOf(":");
|
|
365
|
+
if (colonIdx > 0) {
|
|
366
|
+
const key = trimmed.substring(0, colonIdx).trim();
|
|
367
|
+
const value = trimmed.substring(colonIdx + 1).trim();
|
|
368
|
+
if (value) {
|
|
369
|
+
// Strip quotes
|
|
370
|
+
const unquoted = (value.startsWith('"') && value.endsWith('"')) ||
|
|
371
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
372
|
+
? value.slice(1, -1)
|
|
373
|
+
: value;
|
|
374
|
+
hooks[key] = unquoted;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (Object.keys(hooks).length > 0) {
|
|
380
|
+
this.lifecycleHooks.set(manifest.id, hooks);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// Failed to read lifecycle hooks — not critical
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Discover plugin directories from all search paths.
|
|
389
|
+
* Returns absolute paths to directories containing plugin.yaml.
|
|
390
|
+
*/
|
|
391
|
+
discoverPluginDirs() {
|
|
392
|
+
const searchPaths = [];
|
|
393
|
+
// 1. Built-in plugins: relative to this source file's package
|
|
394
|
+
const builtinDir = path.resolve(__dirname, "..", "plugins");
|
|
395
|
+
searchPaths.push(builtinDir);
|
|
396
|
+
// 2. Project-local plugins
|
|
397
|
+
const localDir = path.resolve(this.config.projectRoot, ".yuan", "plugins");
|
|
398
|
+
searchPaths.push(localDir);
|
|
399
|
+
// 3. User global plugins
|
|
400
|
+
const globalDir = path.resolve(os.homedir(), ".yuan", "plugins");
|
|
401
|
+
searchPaths.push(globalDir);
|
|
402
|
+
// 4. Extra paths from config
|
|
403
|
+
if (this.config.extraPaths) {
|
|
404
|
+
searchPaths.push(...this.config.extraPaths);
|
|
405
|
+
}
|
|
406
|
+
const pluginDirs = [];
|
|
407
|
+
for (const searchPath of searchPaths) {
|
|
408
|
+
if (!fs.existsSync(searchPath))
|
|
409
|
+
continue;
|
|
410
|
+
let stat;
|
|
411
|
+
try {
|
|
412
|
+
stat = fs.statSync(searchPath);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (!stat.isDirectory())
|
|
418
|
+
continue;
|
|
419
|
+
// Each subdirectory that contains a plugin.yaml is a plugin
|
|
420
|
+
let entries;
|
|
421
|
+
try {
|
|
422
|
+
entries = fs.readdirSync(searchPath, { withFileTypes: true });
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
for (const entry of entries) {
|
|
428
|
+
if (!entry.isDirectory())
|
|
429
|
+
continue;
|
|
430
|
+
const pluginDir = path.join(searchPath, entry.name);
|
|
431
|
+
const manifestPath = path.join(pluginDir, "plugin.yaml");
|
|
432
|
+
const manifestPathYml = path.join(pluginDir, "plugin.yml");
|
|
433
|
+
if (fs.existsSync(manifestPath) || fs.existsSync(manifestPathYml)) {
|
|
434
|
+
pluginDirs.push(pluginDir);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// 5. npm-installed plugins (node_modules)
|
|
439
|
+
if (this.config.scanNodeModules !== false) {
|
|
440
|
+
const nodeModulesDir = path.resolve(this.config.projectRoot, "node_modules");
|
|
441
|
+
if (fs.existsSync(nodeModulesDir)) {
|
|
442
|
+
// 5a. Scoped: @yuaone/plugin-*
|
|
443
|
+
const yuaoneScopeDir = path.join(nodeModulesDir, "@yuaone");
|
|
444
|
+
if (fs.existsSync(yuaoneScopeDir)) {
|
|
445
|
+
try {
|
|
446
|
+
const entries = fs.readdirSync(yuaoneScopeDir, { withFileTypes: true });
|
|
447
|
+
for (const entry of entries) {
|
|
448
|
+
if (!entry.isDirectory())
|
|
449
|
+
continue;
|
|
450
|
+
if (!entry.name.startsWith("plugin-"))
|
|
451
|
+
continue;
|
|
452
|
+
const pkgDir = path.join(yuaoneScopeDir, entry.name);
|
|
453
|
+
if (fs.existsSync(path.join(pkgDir, "plugin.yaml")) ||
|
|
454
|
+
fs.existsSync(path.join(pkgDir, "plugin.yml"))) {
|
|
455
|
+
pluginDirs.push(pkgDir);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
// Cannot read @yuaone scope — skip
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// 5b. Community: yuan-plugin-*
|
|
464
|
+
try {
|
|
465
|
+
const entries = fs.readdirSync(nodeModulesDir, { withFileTypes: true });
|
|
466
|
+
for (const entry of entries) {
|
|
467
|
+
if (!entry.isDirectory())
|
|
468
|
+
continue;
|
|
469
|
+
if (!entry.name.startsWith("yuan-plugin-"))
|
|
470
|
+
continue;
|
|
471
|
+
const pkgDir = path.join(nodeModulesDir, entry.name);
|
|
472
|
+
if (fs.existsSync(path.join(pkgDir, "plugin.yaml")) ||
|
|
473
|
+
fs.existsSync(path.join(pkgDir, "plugin.yml"))) {
|
|
474
|
+
pluginDirs.push(pkgDir);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
// Cannot read node_modules — skip
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return pluginDirs;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Parse a plugin.yaml file into a PluginManifest.
|
|
487
|
+
* Uses a simple YAML parser (no external deps).
|
|
488
|
+
*/
|
|
489
|
+
parsePluginYaml(pluginDir) {
|
|
490
|
+
let manifestPath = path.join(pluginDir, "plugin.yaml");
|
|
491
|
+
if (!fs.existsSync(manifestPath)) {
|
|
492
|
+
manifestPath = path.join(pluginDir, "plugin.yml");
|
|
493
|
+
if (!fs.existsSync(manifestPath)) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const content = fs.readFileSync(manifestPath, "utf-8");
|
|
498
|
+
const raw = parseSimpleYaml(content);
|
|
499
|
+
// Validate required fields
|
|
500
|
+
if (typeof raw["id"] !== "string" ||
|
|
501
|
+
typeof raw["name"] !== "string" ||
|
|
502
|
+
typeof raw["version"] !== "string") {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
const manifest = {
|
|
506
|
+
id: raw["id"],
|
|
507
|
+
name: raw["name"],
|
|
508
|
+
version: raw["version"],
|
|
509
|
+
description: raw["description"] ?? "",
|
|
510
|
+
author: raw["author"] ?? "unknown",
|
|
511
|
+
category: raw["category"] ?? "general",
|
|
512
|
+
trustLevel: raw["trustLevel"] ?? "community",
|
|
513
|
+
type: raw["type"] ?? "knowledge",
|
|
514
|
+
};
|
|
515
|
+
// Optional scalar fields
|
|
516
|
+
if (raw["sandbox"] != null)
|
|
517
|
+
manifest.sandbox = raw["sandbox"];
|
|
518
|
+
if (raw["triggerMode"] != null)
|
|
519
|
+
manifest.triggerMode = raw["triggerMode"];
|
|
520
|
+
if (raw["pluginApiVersion"] != null)
|
|
521
|
+
manifest.pluginApiVersion = raw["pluginApiVersion"];
|
|
522
|
+
if (raw["estimatedPromptTokens"] != null)
|
|
523
|
+
manifest.estimatedPromptTokens = raw["estimatedPromptTokens"];
|
|
524
|
+
if (raw["checksum"] != null)
|
|
525
|
+
manifest.checksum = raw["checksum"];
|
|
526
|
+
if (raw["license"] != null)
|
|
527
|
+
manifest.license = raw["license"];
|
|
528
|
+
if (raw["engineVersion"] != null)
|
|
529
|
+
manifest.engineVersion = raw["engineVersion"];
|
|
530
|
+
// Detect config
|
|
531
|
+
if (raw["detect"] != null && typeof raw["detect"] === "object") {
|
|
532
|
+
manifest.detect = this.parseDetectConfig(raw["detect"]);
|
|
533
|
+
}
|
|
534
|
+
// Skills array
|
|
535
|
+
if (Array.isArray(raw["skills"])) {
|
|
536
|
+
manifest.skills = this.parseSkillsArray(raw["skills"]);
|
|
537
|
+
}
|
|
538
|
+
// Tools array
|
|
539
|
+
if (Array.isArray(raw["tools"])) {
|
|
540
|
+
manifest.tools = this.parseToolsArray(raw["tools"]);
|
|
541
|
+
}
|
|
542
|
+
// Triggers array
|
|
543
|
+
if (Array.isArray(raw["triggers"])) {
|
|
544
|
+
manifest.triggers = this.parseTriggersArray(raw["triggers"]);
|
|
545
|
+
}
|
|
546
|
+
// Permissions
|
|
547
|
+
if (raw["permissions"] != null && typeof raw["permissions"] === "object") {
|
|
548
|
+
manifest.permissions = raw["permissions"];
|
|
549
|
+
}
|
|
550
|
+
// Dependencies (plugin deps)
|
|
551
|
+
if (raw["dependencies"] != null && typeof raw["dependencies"] === "object" && !Array.isArray(raw["dependencies"])) {
|
|
552
|
+
manifest.dependencies = raw["dependencies"];
|
|
553
|
+
}
|
|
554
|
+
// Config fields
|
|
555
|
+
if (raw["config"] != null && typeof raw["config"] === "object" && !Array.isArray(raw["config"])) {
|
|
556
|
+
manifest.config = raw["config"];
|
|
557
|
+
}
|
|
558
|
+
return manifest;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Parse detect config from raw YAML object.
|
|
562
|
+
*/
|
|
563
|
+
parseDetectConfig(raw) {
|
|
564
|
+
const detect = {};
|
|
565
|
+
if (Array.isArray(raw["files"])) {
|
|
566
|
+
detect.files = raw["files"].map(String);
|
|
567
|
+
}
|
|
568
|
+
if (Array.isArray(raw["dependencies"])) {
|
|
569
|
+
detect.dependencies = raw["dependencies"].map(String);
|
|
570
|
+
}
|
|
571
|
+
if (Array.isArray(raw["glob"])) {
|
|
572
|
+
detect.glob = raw["glob"].map(String);
|
|
573
|
+
}
|
|
574
|
+
if (Array.isArray(raw["env"])) {
|
|
575
|
+
detect.env = raw["env"].map(String);
|
|
576
|
+
}
|
|
577
|
+
return detect;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Parse skills array from raw YAML.
|
|
581
|
+
*/
|
|
582
|
+
parseSkillsArray(rawSkills) {
|
|
583
|
+
return rawSkills
|
|
584
|
+
.filter((s) => typeof s["id"] === "string" && typeof s["name"] === "string")
|
|
585
|
+
.map((s) => ({
|
|
586
|
+
id: s["id"],
|
|
587
|
+
name: s["name"],
|
|
588
|
+
description: s["description"] ?? "",
|
|
589
|
+
trigger: this.parseSkillTrigger(s["trigger"]),
|
|
590
|
+
template: s["template"] ?? "",
|
|
591
|
+
enabled: s["enabled"] !== false,
|
|
592
|
+
tags: Array.isArray(s["tags"]) ? s["tags"].map(String) : undefined,
|
|
593
|
+
}));
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Parse a skill trigger from raw YAML.
|
|
597
|
+
*/
|
|
598
|
+
parseSkillTrigger(raw) {
|
|
599
|
+
if (!raw) {
|
|
600
|
+
return { kind: "manual" };
|
|
601
|
+
}
|
|
602
|
+
const trigger = {
|
|
603
|
+
kind: raw["kind"] ?? "manual",
|
|
604
|
+
};
|
|
605
|
+
if (raw["pattern"] != null)
|
|
606
|
+
trigger.pattern = raw["pattern"];
|
|
607
|
+
if (raw["command"] != null)
|
|
608
|
+
trigger.command = raw["command"];
|
|
609
|
+
if (raw["confidence"] != null)
|
|
610
|
+
trigger.confidence = raw["confidence"];
|
|
611
|
+
if (Array.isArray(raw["requires"]))
|
|
612
|
+
trigger.requires = raw["requires"].map(String);
|
|
613
|
+
if (Array.isArray(raw["exclude"]))
|
|
614
|
+
trigger.exclude = raw["exclude"].map(String);
|
|
615
|
+
if (raw["cooldown"] != null)
|
|
616
|
+
trigger.cooldown = raw["cooldown"];
|
|
617
|
+
return trigger;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Parse tools array from raw YAML.
|
|
621
|
+
*/
|
|
622
|
+
parseToolsArray(rawTools) {
|
|
623
|
+
return rawTools
|
|
624
|
+
.filter((t) => typeof t["name"] === "string")
|
|
625
|
+
.map((t) => ({
|
|
626
|
+
name: t["name"],
|
|
627
|
+
description: t["description"] ?? "",
|
|
628
|
+
inputSchema: t["inputSchema"] ?? {},
|
|
629
|
+
requiresApproval: t["requiresApproval"],
|
|
630
|
+
riskLevel: t["riskLevel"],
|
|
631
|
+
sideEffectLevel: t["sideEffectLevel"],
|
|
632
|
+
}));
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Parse triggers array from raw YAML.
|
|
636
|
+
*/
|
|
637
|
+
parseTriggersArray(rawTriggers) {
|
|
638
|
+
return rawTriggers
|
|
639
|
+
.filter((t) => typeof t["pattern"] === "string" && typeof t["skill"] === "string")
|
|
640
|
+
.map((t) => ({
|
|
641
|
+
pattern: t["pattern"],
|
|
642
|
+
kind: t["kind"],
|
|
643
|
+
skill: t["skill"],
|
|
644
|
+
strategy: t["strategy"],
|
|
645
|
+
priority: t["priority"],
|
|
646
|
+
triggerMode: t["triggerMode"],
|
|
647
|
+
}));
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Check if a plugin's detect conditions match the current project.
|
|
651
|
+
*
|
|
652
|
+
* Logic: ANY file match OR ANY dependency match triggers activation.
|
|
653
|
+
* If no detect config exists, the plugin is always activated.
|
|
654
|
+
*/
|
|
655
|
+
matchesDetect(manifest) {
|
|
656
|
+
const detect = manifest.detect;
|
|
657
|
+
if (!detect)
|
|
658
|
+
return true;
|
|
659
|
+
const hasAnyCondition = (detect.files && detect.files.length > 0) ||
|
|
660
|
+
(detect.dependencies && detect.dependencies.length > 0) ||
|
|
661
|
+
(detect.glob && detect.glob.length > 0) ||
|
|
662
|
+
(detect.env && detect.env.length > 0);
|
|
663
|
+
if (!hasAnyCondition)
|
|
664
|
+
return true;
|
|
665
|
+
// Check file existence
|
|
666
|
+
if (detect.files) {
|
|
667
|
+
for (const file of detect.files) {
|
|
668
|
+
const filePath = path.resolve(this.config.projectRoot, file);
|
|
669
|
+
if (fs.existsSync(filePath))
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// Check dependencies in package.json
|
|
674
|
+
if (detect.dependencies && detect.dependencies.length > 0) {
|
|
675
|
+
const pkgJsonPath = path.resolve(this.config.projectRoot, "package.json");
|
|
676
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
677
|
+
try {
|
|
678
|
+
const pkgContent = fs.readFileSync(pkgJsonPath, "utf-8");
|
|
679
|
+
const pkg = JSON.parse(pkgContent);
|
|
680
|
+
const allDeps = new Set([
|
|
681
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
682
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
683
|
+
...Object.keys(pkg.peerDependencies ?? {}),
|
|
684
|
+
]);
|
|
685
|
+
for (const dep of detect.dependencies) {
|
|
686
|
+
if (allDeps.has(dep))
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
// Invalid package.json — skip dependency check
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// Check glob patterns
|
|
696
|
+
if (detect.glob) {
|
|
697
|
+
for (const pattern of detect.glob) {
|
|
698
|
+
if (this.globExistsInProject(pattern))
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
// Check environment variables
|
|
703
|
+
if (detect.env) {
|
|
704
|
+
for (const envVar of detect.env) {
|
|
705
|
+
if (process.env[envVar] != null)
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Check if any file matching a glob pattern exists in the project root.
|
|
713
|
+
* Uses a simple top-level check (does not recurse for **).
|
|
714
|
+
*/
|
|
715
|
+
globExistsInProject(pattern) {
|
|
716
|
+
// Convert glob to regex for matching
|
|
717
|
+
const regexStr = pattern
|
|
718
|
+
.replace(/\./g, "\\.")
|
|
719
|
+
.replace(/\*\*/g, "<<GLOBSTAR>>")
|
|
720
|
+
.replace(/\*/g, "[^/]*")
|
|
721
|
+
.replace(/<<GLOBSTAR>>/g, ".*");
|
|
722
|
+
let regex;
|
|
723
|
+
try {
|
|
724
|
+
regex = new RegExp(`^${regexStr}$`);
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
// For simple patterns like "*.ts" or "tsconfig.json", just check root
|
|
730
|
+
try {
|
|
731
|
+
const entries = fs.readdirSync(this.config.projectRoot);
|
|
732
|
+
for (const entry of entries) {
|
|
733
|
+
if (regex.test(entry))
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// Cannot read directory
|
|
739
|
+
}
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
//# sourceMappingURL=plugin-auto-loader.js.map
|