@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.
Files changed (129) hide show
  1. package/dist/agent-loop.d.ts +62 -0
  2. package/dist/agent-loop.d.ts.map +1 -1
  3. package/dist/agent-loop.js +705 -18
  4. package/dist/agent-loop.js.map +1 -1
  5. package/dist/background-agent.d.ts +110 -0
  6. package/dist/background-agent.d.ts.map +1 -0
  7. package/dist/background-agent.js +255 -0
  8. package/dist/background-agent.js.map +1 -0
  9. package/dist/coding-standards.d.ts +45 -0
  10. package/dist/coding-standards.d.ts.map +1 -0
  11. package/dist/coding-standards.js +1152 -0
  12. package/dist/coding-standards.js.map +1 -0
  13. package/dist/constants.d.ts.map +1 -1
  14. package/dist/constants.js +2 -6
  15. package/dist/constants.js.map +1 -1
  16. package/dist/context-manager.d.ts +6 -0
  17. package/dist/context-manager.d.ts.map +1 -1
  18. package/dist/context-manager.js +23 -4
  19. package/dist/context-manager.js.map +1 -1
  20. package/dist/index.d.ts +28 -4
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +26 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/llm-client.d.ts +8 -3
  25. package/dist/llm-client.d.ts.map +1 -1
  26. package/dist/llm-client.js +64 -13
  27. package/dist/llm-client.js.map +1 -1
  28. package/dist/plugin-auto-loader.d.ts +108 -0
  29. package/dist/plugin-auto-loader.d.ts.map +1 -0
  30. package/dist/plugin-auto-loader.js +743 -0
  31. package/dist/plugin-auto-loader.js.map +1 -0
  32. package/dist/plugin-registry.d.ts +112 -0
  33. package/dist/plugin-registry.d.ts.map +1 -0
  34. package/dist/plugin-registry.js +319 -0
  35. package/dist/plugin-registry.js.map +1 -0
  36. package/dist/plugin-types.d.ts +388 -0
  37. package/dist/plugin-types.d.ts.map +1 -0
  38. package/dist/plugin-types.js +8 -0
  39. package/dist/plugin-types.js.map +1 -0
  40. package/dist/plugin-validator.d.ts +54 -0
  41. package/dist/plugin-validator.d.ts.map +1 -0
  42. package/dist/plugin-validator.js +129 -0
  43. package/dist/plugin-validator.js.map +1 -0
  44. package/dist/repo-knowledge-graph.d.ts +112 -0
  45. package/dist/repo-knowledge-graph.d.ts.map +1 -0
  46. package/dist/repo-knowledge-graph.js +561 -0
  47. package/dist/repo-knowledge-graph.js.map +1 -0
  48. package/dist/role-registry.js +1 -1
  49. package/dist/role-registry.js.map +1 -1
  50. package/dist/self-debug-loop.d.ts +257 -0
  51. package/dist/self-debug-loop.d.ts.map +1 -0
  52. package/dist/self-debug-loop.js +870 -0
  53. package/dist/self-debug-loop.js.map +1 -0
  54. package/dist/skill-learner.d.ts +136 -0
  55. package/dist/skill-learner.d.ts.map +1 -0
  56. package/dist/skill-learner.js +382 -0
  57. package/dist/skill-learner.js.map +1 -0
  58. package/dist/skill-loader.d.ts +90 -0
  59. package/dist/skill-loader.d.ts.map +1 -0
  60. package/dist/skill-loader.js +309 -0
  61. package/dist/skill-loader.js.map +1 -0
  62. package/dist/specialist-registry.d.ts +132 -0
  63. package/dist/specialist-registry.d.ts.map +1 -0
  64. package/dist/specialist-registry.js +413 -0
  65. package/dist/specialist-registry.js.map +1 -0
  66. package/dist/sub-agent-prompts.d.ts +45 -0
  67. package/dist/sub-agent-prompts.d.ts.map +1 -0
  68. package/dist/sub-agent-prompts.js +177 -0
  69. package/dist/sub-agent-prompts.js.map +1 -0
  70. package/dist/sub-agent-router.d.ts +75 -0
  71. package/dist/sub-agent-router.d.ts.map +1 -0
  72. package/dist/sub-agent-router.js +174 -0
  73. package/dist/sub-agent-router.js.map +1 -0
  74. package/dist/sub-agent.d.ts +48 -0
  75. package/dist/sub-agent.d.ts.map +1 -1
  76. package/dist/sub-agent.js +108 -5
  77. package/dist/sub-agent.js.map +1 -1
  78. package/dist/system-prompt.d.ts +26 -0
  79. package/dist/system-prompt.d.ts.map +1 -1
  80. package/dist/system-prompt.js +177 -7
  81. package/dist/system-prompt.js.map +1 -1
  82. package/dist/task-classifier.d.ts +25 -1
  83. package/dist/task-classifier.d.ts.map +1 -1
  84. package/dist/task-classifier.js +171 -1
  85. package/dist/task-classifier.js.map +1 -1
  86. package/dist/tool-planner.d.ts +160 -0
  87. package/dist/tool-planner.d.ts.map +1 -0
  88. package/dist/tool-planner.js +501 -0
  89. package/dist/tool-planner.js.map +1 -0
  90. package/dist/types.d.ts +1 -1
  91. package/dist/types.d.ts.map +1 -1
  92. package/dist/world-state.d.ts.map +1 -1
  93. package/dist/world-state.js +8 -1
  94. package/dist/world-state.js.map +1 -1
  95. package/package.json +2 -1
  96. package/plugins/git/patterns/branch-patterns.json +101 -0
  97. package/plugins/git/patterns/commit-patterns.json +186 -0
  98. package/plugins/git/plugin.yaml +128 -0
  99. package/plugins/git/skills/branch-strategy.md +172 -0
  100. package/plugins/git/skills/commit-conv.md +178 -0
  101. package/plugins/git/skills/conflict-resolve.md +159 -0
  102. package/plugins/git/skills/history-clean.md +199 -0
  103. package/plugins/git/skills/pr-review.md +196 -0
  104. package/plugins/git/strategies/conflict-resolve.json +244 -0
  105. package/plugins/git/strategies/release-flow.json +292 -0
  106. package/plugins/git/validators/rules.json +348 -0
  107. package/plugins/react/patterns/anti-patterns.json +88 -0
  108. package/plugins/react/patterns/components.json +80 -0
  109. package/plugins/react/patterns/hooks.json +72 -0
  110. package/plugins/react/plugin.yaml +229 -0
  111. package/plugins/react/skills/bugfix.md +208 -0
  112. package/plugins/react/skills/component-gen.md +206 -0
  113. package/plugins/react/skills/hook-extract.md +208 -0
  114. package/plugins/react/skills/ssr.md +256 -0
  115. package/plugins/react/skills/test.md +273 -0
  116. package/plugins/react/strategies/build-fix.json +43 -0
  117. package/plugins/react/strategies/hook-loop-fix.json +36 -0
  118. package/plugins/react/strategies/hydration-fix.json +42 -0
  119. package/plugins/react/validators/rules.json +92 -0
  120. package/plugins/typescript/patterns/best-practices.json +25 -0
  121. package/plugins/typescript/patterns/common-errors.json +32 -0
  122. package/plugins/typescript/plugin.yaml +74 -0
  123. package/plugins/typescript/skills/debug.md +23 -0
  124. package/plugins/typescript/skills/migration.md +24 -0
  125. package/plugins/typescript/skills/refactor.md +22 -0
  126. package/plugins/typescript/skills/strict-mode.md +23 -0
  127. package/plugins/typescript/strategies/strict-migration.json +37 -0
  128. package/plugins/typescript/strategies/type-error-fix.json +37 -0
  129. 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