@thelogicatelier/sylva 1.0.11 → 1.0.13

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.
@@ -1,7 +1,21 @@
1
1
  "use strict";
2
2
  /**
3
- * OpenClaw manifest parser.
4
- * Parses openclaw.json to extract orchestrator configuration.
3
+ * OpenClaw manifest parser — deep extraction.
4
+ * Parses openclaw.json / .openclaw.json to extract:
5
+ * - Orchestrator signal with version from meta
6
+ * - Agent config (models, workspace, concurrency)
7
+ * - Hooks (internal event scripts with paths + descriptions)
8
+ * - Plugins (enabled extensions)
9
+ * - Gateway config (port, auth, denied commands)
10
+ * - Channels (policies, stream modes)
11
+ * - Tools (web search, fetch)
12
+ * - Commands / messages config
13
+ *
14
+ * Also scans the OpenClaw workspace directory for:
15
+ * - Skills (reusable .md workflows)
16
+ * - Subagents (background agent directories)
17
+ * - Workspace .md files (AGENTS.md, IDENTITY.md, HEARTBEAT.md, etc.)
18
+ * - Hook scripts
5
19
  */
6
20
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
21
  if (k2 === undefined) k2 = k;
@@ -40,6 +54,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
40
54
  exports.parseOpenclawJson = parseOpenclawJson;
41
55
  const fs = __importStar(require("fs"));
42
56
  const path = __importStar(require("path"));
57
+ // ---------------------
58
+ // Deep JSON extraction
59
+ // ---------------------
43
60
  function parseOpenclawJson(manifest) {
44
61
  const signals = [];
45
62
  const content = fs.readFileSync(manifest.absolutePath, "utf-8");
@@ -54,50 +71,546 @@ function parseOpenclawJson(manifest) {
54
71
  return signals;
55
72
  }
56
73
  const rootPath = path.dirname(manifest.relativePath) || ".";
57
- // Primary orchestrator signal
74
+ // --- Extract OpenClaw version from meta ---
75
+ const meta = config.meta;
76
+ const openclawVersion = meta?.lastTouchedVersion
77
+ ? {
78
+ value: String(meta.lastTouchedVersion),
79
+ certainty: "exact",
80
+ sourceFile: manifest.relativePath,
81
+ notes: "From meta.lastTouchedVersion in openclaw.json",
82
+ }
83
+ : undefined;
84
+ // --- Primary orchestrator signal ---
58
85
  signals.push({
59
86
  kind: "orchestrator",
60
87
  frameworkId: "openclaw",
61
88
  frameworkName: "OpenClaw",
89
+ ...(openclawVersion ? { version: openclawVersion } : {}),
62
90
  evidence: {
63
91
  file: manifest.relativePath,
64
92
  reason: "openclaw.json configuration file found",
65
- excerpt: content.length > 500 ? content.substring(0, 500) + "..." : content,
93
+ excerpt: openclawVersion
94
+ ? `OpenClaw version ${openclawVersion.value}`
95
+ : "OpenClaw orchestrator (version not specified in config)",
96
+ },
97
+ scope: { pathRoot: rootPath },
98
+ });
99
+ // --- Agent config ---
100
+ extractAgentConfig(config, manifest, rootPath, signals);
101
+ // --- Hooks ---
102
+ extractHooks(config, manifest, rootPath, signals);
103
+ // --- Plugins ---
104
+ extractPlugins(config, manifest, rootPath, signals);
105
+ // --- Gateway ---
106
+ extractGateway(config, manifest, rootPath, signals);
107
+ // --- Channels (expanded) ---
108
+ extractChannels(config, manifest, rootPath, signals);
109
+ // --- Tools (expanded) ---
110
+ extractTools(config, manifest, rootPath, signals);
111
+ // --- Commands + messages ---
112
+ extractCommandsAndMessages(config, manifest, rootPath, signals);
113
+ // --- Workspace scanning ---
114
+ scanWorkspace(config, manifest, rootPath, signals);
115
+ return signals;
116
+ }
117
+ // ---------------------
118
+ // Section extractors
119
+ // ---------------------
120
+ function extractAgentConfig(config, manifest, rootPath, signals) {
121
+ const agents = config.agents;
122
+ if (!agents || typeof agents !== "object")
123
+ return;
124
+ const defaults = agents.defaults;
125
+ if (!defaults || typeof defaults !== "object")
126
+ return;
127
+ // Primary model
128
+ const model = defaults.model;
129
+ const primaryModel = model?.primary ? String(model.primary) : undefined;
130
+ // Available models
131
+ const models = defaults.models;
132
+ const modelCatalog = models ? Object.keys(models) : [];
133
+ // Workspace path
134
+ const workspace = defaults.workspace ? String(defaults.workspace) : undefined;
135
+ // Concurrency
136
+ const maxConcurrent = typeof defaults.maxConcurrent === "number" ? defaults.maxConcurrent : undefined;
137
+ const subagentConfig = defaults.subagents;
138
+ const subagentMaxConcurrent = subagentConfig && typeof subagentConfig.maxConcurrent === "number"
139
+ ? subagentConfig.maxConcurrent
140
+ : undefined;
141
+ // Compaction
142
+ const compaction = defaults.compaction;
143
+ const compactionMode = compaction?.mode ? String(compaction.mode) : undefined;
144
+ const details = [];
145
+ if (primaryModel)
146
+ details.push(`primary model: ${primaryModel}`);
147
+ if (modelCatalog.length > 0)
148
+ details.push(`${modelCatalog.length} model(s) available`);
149
+ if (workspace)
150
+ details.push(`workspace: ${workspace}`);
151
+ if (maxConcurrent)
152
+ details.push(`maxConcurrent: ${maxConcurrent}`);
153
+ if (subagentMaxConcurrent)
154
+ details.push(`subagent maxConcurrent: ${subagentMaxConcurrent}`);
155
+ if (compactionMode)
156
+ details.push(`compaction: ${compactionMode}`);
157
+ signals.push({
158
+ kind: "agent",
159
+ frameworkId: "openclaw-agent-config",
160
+ frameworkName: "OpenClaw Agent Config",
161
+ evidence: {
162
+ file: manifest.relativePath,
163
+ reason: "Agent configuration in openclaw.json",
164
+ excerpt: details.join("; "),
66
165
  },
67
166
  scope: { pathRoot: rootPath },
68
167
  });
69
- // Extract tool signals
168
+ // Emit individual model entries for the LLM to know available models
169
+ if (modelCatalog.length > 0) {
170
+ signals.push({
171
+ kind: "agent",
172
+ frameworkId: "openclaw-model-catalog",
173
+ frameworkName: "OpenClaw Model Catalog",
174
+ evidence: {
175
+ file: manifest.relativePath,
176
+ reason: "Available LLM models configured in openclaw.json",
177
+ excerpt: modelCatalog.join(", "),
178
+ },
179
+ scope: { pathRoot: rootPath },
180
+ });
181
+ }
182
+ }
183
+ function extractHooks(config, manifest, rootPath, signals) {
184
+ const hooks = config.hooks;
185
+ if (!hooks || typeof hooks !== "object")
186
+ return;
187
+ const internal = hooks.internal;
188
+ if (!internal || typeof internal !== "object")
189
+ return;
190
+ if (internal.enabled === false)
191
+ return;
192
+ const entries = internal.entries;
193
+ if (!entries || typeof entries !== "object")
194
+ return;
195
+ for (const [hookName, hookConfig] of Object.entries(entries)) {
196
+ if (!hookConfig || typeof hookConfig !== "object")
197
+ continue;
198
+ const enabled = hookConfig.enabled !== false; // default true
199
+ const hookPath = hookConfig.path ? String(hookConfig.path) : undefined;
200
+ const description = hookConfig.description ? String(hookConfig.description) : undefined;
201
+ const details = [`enabled: ${enabled}`];
202
+ if (hookPath)
203
+ details.push(`path: ${hookPath}`);
204
+ if (description)
205
+ details.push(`desc: ${description}`);
206
+ signals.push({
207
+ kind: "hook",
208
+ frameworkId: `openclaw-hook-${hookName}`,
209
+ frameworkName: `OpenClaw Hook: ${hookName}`,
210
+ evidence: {
211
+ file: manifest.relativePath,
212
+ reason: description || `Hook '${hookName}' configured in openclaw.json`,
213
+ excerpt: details.join("; "),
214
+ },
215
+ scope: { pathRoot: rootPath },
216
+ });
217
+ }
218
+ }
219
+ function extractPlugins(config, manifest, rootPath, signals) {
220
+ const plugins = config.plugins;
221
+ if (!plugins || typeof plugins !== "object")
222
+ return;
223
+ const entries = plugins.entries;
224
+ if (!entries || typeof entries !== "object")
225
+ return;
226
+ for (const [pluginName, pluginConfig] of Object.entries(entries)) {
227
+ if (!pluginConfig || typeof pluginConfig !== "object")
228
+ continue;
229
+ const enabled = pluginConfig.enabled !== false;
230
+ signals.push({
231
+ kind: "plugin",
232
+ frameworkId: `openclaw-plugin-${pluginName}`,
233
+ frameworkName: `OpenClaw Plugin: ${pluginName}`,
234
+ evidence: {
235
+ file: manifest.relativePath,
236
+ reason: `Plugin '${pluginName}' ${enabled ? "enabled" : "disabled"} in openclaw.json`,
237
+ excerpt: `enabled: ${enabled}`,
238
+ },
239
+ scope: { pathRoot: rootPath },
240
+ });
241
+ }
242
+ }
243
+ function extractGateway(config, manifest, rootPath, signals) {
244
+ const gateway = config.gateway;
245
+ if (!gateway || typeof gateway !== "object")
246
+ return;
247
+ const details = [];
248
+ if (gateway.port)
249
+ details.push(`port: ${gateway.port}`);
250
+ if (gateway.mode)
251
+ details.push(`mode: ${gateway.mode}`);
252
+ if (gateway.bind)
253
+ details.push(`bind: ${gateway.bind}`);
254
+ const auth = gateway.auth;
255
+ if (auth?.mode)
256
+ details.push(`auth: ${auth.mode}`);
257
+ const tailscale = gateway.tailscale;
258
+ if (tailscale?.mode)
259
+ details.push(`tailscale: ${tailscale.mode}`);
260
+ const nodes = gateway.nodes;
261
+ const denyCommands = nodes?.denyCommands;
262
+ if (denyCommands && Array.isArray(denyCommands)) {
263
+ details.push(`denied commands: ${denyCommands.join(", ")}`);
264
+ }
265
+ signals.push({
266
+ kind: "tooling",
267
+ frameworkId: "openclaw-gateway",
268
+ frameworkName: "OpenClaw Gateway",
269
+ evidence: {
270
+ file: manifest.relativePath,
271
+ reason: "Gateway configuration in openclaw.json",
272
+ excerpt: details.join("; "),
273
+ },
274
+ scope: { pathRoot: rootPath },
275
+ });
276
+ }
277
+ function extractChannels(config, manifest, rootPath, signals) {
278
+ const channels = config.channels;
279
+ if (!channels || typeof channels !== "object")
280
+ return;
281
+ for (const [channelName, channelConfig] of Object.entries(channels)) {
282
+ if (!channelConfig || typeof channelConfig !== "object")
283
+ continue;
284
+ const ch = channelConfig;
285
+ const details = [];
286
+ if (ch.enabled === false)
287
+ details.push("disabled");
288
+ if (ch.dmPolicy)
289
+ details.push(`dmPolicy: ${ch.dmPolicy}`);
290
+ if (ch.groupPolicy)
291
+ details.push(`groupPolicy: ${ch.groupPolicy}`);
292
+ if (ch.streamMode)
293
+ details.push(`streamMode: ${ch.streamMode}`);
294
+ if (ch.selfChatMode !== undefined)
295
+ details.push(`selfChat: ${ch.selfChatMode}`);
296
+ if (ch.debounceMs !== undefined)
297
+ details.push(`debounce: ${ch.debounceMs}ms`);
298
+ if (ch.mediaMaxMb)
299
+ details.push(`mediaMax: ${ch.mediaMaxMb}MB`);
300
+ signals.push({
301
+ kind: "tooling",
302
+ frameworkId: `openclaw-channel-${channelName}`,
303
+ frameworkName: `OpenClaw Channel: ${channelName}`,
304
+ evidence: {
305
+ file: manifest.relativePath,
306
+ reason: `Channel '${channelName}' configured in openclaw.json`,
307
+ excerpt: details.length > 0 ? details.join("; ") : `channel ${channelName} configured`,
308
+ },
309
+ scope: { pathRoot: rootPath },
310
+ });
311
+ }
312
+ }
313
+ function extractTools(config, manifest, rootPath, signals) {
70
314
  const tools = config.tools;
71
- if (tools && typeof tools === "object") {
72
- for (const [toolName, toolConfig] of Object.entries(tools)) {
315
+ if (!tools || typeof tools !== "object")
316
+ return;
317
+ for (const [toolName, toolConfig] of Object.entries(tools)) {
318
+ if (!toolConfig || typeof toolConfig !== "object")
319
+ continue;
320
+ // Build a summary of the tool's sub-capabilities
321
+ const subCapabilities = [];
322
+ for (const [subName, subConfig] of Object.entries(toolConfig)) {
323
+ if (subConfig && typeof subConfig === "object") {
324
+ const sc = subConfig;
325
+ const enabled = sc.enabled !== false;
326
+ subCapabilities.push(`${subName}: ${enabled ? "enabled" : "disabled"}`);
327
+ }
328
+ }
329
+ signals.push({
330
+ kind: "tooling",
331
+ frameworkId: `openclaw-tool-${toolName}`,
332
+ frameworkName: `OpenClaw Tool: ${toolName}`,
333
+ evidence: {
334
+ file: manifest.relativePath,
335
+ reason: `Tool '${toolName}' configured in openclaw.json`,
336
+ excerpt: subCapabilities.length > 0 ? subCapabilities.join("; ") : `tool ${toolName} configured`,
337
+ },
338
+ scope: { pathRoot: rootPath },
339
+ });
340
+ }
341
+ }
342
+ function extractCommandsAndMessages(config, manifest, rootPath, signals) {
343
+ const commands = config.commands;
344
+ if (commands && typeof commands === "object") {
345
+ const details = [];
346
+ if (commands.native)
347
+ details.push(`native: ${commands.native}`);
348
+ if (commands.nativeSkills)
349
+ details.push(`nativeSkills: ${commands.nativeSkills}`);
350
+ if (details.length > 0) {
73
351
  signals.push({
74
352
  kind: "tooling",
75
- frameworkId: `openclaw-tool-${toolName}`,
76
- frameworkName: `OpenClaw Tool: ${toolName}`,
353
+ frameworkId: "openclaw-commands",
354
+ frameworkName: "OpenClaw Commands",
77
355
  evidence: {
78
356
  file: manifest.relativePath,
79
- reason: `Tool '${toolName}' configured in openclaw.json`,
80
- excerpt: JSON.stringify(toolConfig, null, 2).substring(0, 200),
357
+ reason: "Command configuration in openclaw.json",
358
+ excerpt: details.join("; "),
81
359
  },
82
360
  scope: { pathRoot: rootPath },
83
361
  });
84
362
  }
85
363
  }
86
- // Extract channel signals
87
- const channels = config.channels;
88
- if (channels && typeof channels === "object") {
89
- for (const [channelName] of Object.entries(channels)) {
364
+ const messages = config.messages;
365
+ if (messages && typeof messages === "object") {
366
+ const details = [];
367
+ if (messages.ackReactionScope)
368
+ details.push(`ackReactionScope: ${messages.ackReactionScope}`);
369
+ if (details.length > 0) {
90
370
  signals.push({
91
371
  kind: "tooling",
92
- frameworkId: `openclaw-channel-${channelName}`,
93
- frameworkName: `OpenClaw Channel: ${channelName}`,
372
+ frameworkId: "openclaw-messages",
373
+ frameworkName: "OpenClaw Messages",
94
374
  evidence: {
95
375
  file: manifest.relativePath,
96
- reason: `Channel '${channelName}' configured in openclaw.json`,
376
+ reason: "Message configuration in openclaw.json",
377
+ excerpt: details.join("; "),
97
378
  },
98
379
  scope: { pathRoot: rootPath },
99
380
  });
100
381
  }
101
382
  }
102
- return signals;
383
+ }
384
+ // ---------------------
385
+ // Workspace scanner
386
+ // ---------------------
387
+ function scanWorkspace(config, manifest, rootPath, signals) {
388
+ // Resolve workspace directory:
389
+ // 1. From agents.defaults.workspace in config
390
+ // 2. Fallback to sibling "workspace/" directory relative to the config file
391
+ const agents = config.agents;
392
+ const defaults = agents?.defaults;
393
+ const configuredWorkspace = defaults?.workspace ? String(defaults.workspace) : undefined;
394
+ const configDir = path.dirname(manifest.absolutePath);
395
+ let workspaceDir;
396
+ if (configuredWorkspace) {
397
+ // Could be absolute or relative — try both
398
+ const candidate = path.isAbsolute(configuredWorkspace)
399
+ ? configuredWorkspace
400
+ : path.resolve(configDir, configuredWorkspace);
401
+ if (existsAndIsDir(candidate)) {
402
+ workspaceDir = candidate;
403
+ }
404
+ }
405
+ if (!workspaceDir) {
406
+ // Fallback: sibling "workspace/" directory
407
+ const fallback = path.join(configDir, "workspace");
408
+ if (existsAndIsDir(fallback)) {
409
+ workspaceDir = fallback;
410
+ }
411
+ }
412
+ if (!workspaceDir)
413
+ return;
414
+ const workspaceRel = path.relative(path.dirname(manifest.absolutePath), workspaceDir);
415
+ // Scan workspace .md files (identity layer)
416
+ scanWorkspaceMdFiles(workspaceDir, workspaceRel, manifest, rootPath, signals);
417
+ // Scan skills
418
+ scanSkills(workspaceDir, workspaceRel, manifest, rootPath, signals);
419
+ // Scan subagents
420
+ scanSubagents(workspaceDir, workspaceRel, manifest, rootPath, signals);
421
+ }
422
+ function scanWorkspaceMdFiles(workspaceDir, workspaceRel, manifest, rootPath, signals) {
423
+ const WORKSPACE_FILES = {
424
+ "AGENTS.md": "Agent behavioral instructions and skill routing",
425
+ "IDENTITY.md": "Agent identity, personality, and avatar",
426
+ "HEARTBEAT.md": "Periodic awareness checklist (cron-like proactive checks)",
427
+ "MEMORY.md": "Long-term curated memory store",
428
+ "SOUL.md": "Agent personality and core values",
429
+ "USER.md": "User profile and preferences",
430
+ "TOOLS.md": "Tool configuration and capabilities",
431
+ "OPTIMIZATION.md": "Performance and cost optimization rules",
432
+ };
433
+ for (const [filename, description] of Object.entries(WORKSPACE_FILES)) {
434
+ const filePath = path.join(workspaceDir, filename);
435
+ if (!existsAndIsFile(filePath))
436
+ continue;
437
+ // For HEARTBEAT.md, check if it has actual tasks (non-empty, non-comment content)
438
+ if (filename === "HEARTBEAT.md") {
439
+ const heartbeatContent = safeReadFile(filePath);
440
+ const hasActiveTasks = heartbeatContent
441
+ ? heartbeatContent.split("\n").some((line) => line.trim() && !line.trim().startsWith("#"))
442
+ : false;
443
+ signals.push({
444
+ kind: "heartbeat",
445
+ frameworkId: "openclaw-heartbeat",
446
+ frameworkName: "OpenClaw Heartbeat",
447
+ evidence: {
448
+ file: path.join(workspaceRel, filename),
449
+ reason: hasActiveTasks
450
+ ? "HEARTBEAT.md found with active periodic tasks"
451
+ : "HEARTBEAT.md found but no active tasks configured",
452
+ excerpt: hasActiveTasks ? "Status: ACTIVE" : "Status: INACTIVE (empty/comments only)",
453
+ },
454
+ scope: { pathRoot: rootPath },
455
+ });
456
+ continue;
457
+ }
458
+ // For IDENTITY.md, extract the agent name if present
459
+ let excerpt = description;
460
+ if (filename === "IDENTITY.md") {
461
+ const identityContent = safeReadFile(filePath);
462
+ if (identityContent) {
463
+ const nameMatch = identityContent.match(/\*\*Name:\*\*\s*(.+)/);
464
+ if (nameMatch) {
465
+ excerpt = `Agent name: ${nameMatch[1].trim()} — ${description}`;
466
+ }
467
+ }
468
+ }
469
+ signals.push({
470
+ kind: "agent",
471
+ frameworkId: `openclaw-workspace-${filename.replace(".md", "").toLowerCase()}`,
472
+ frameworkName: `OpenClaw Workspace: ${filename}`,
473
+ evidence: {
474
+ file: path.join(workspaceRel, filename),
475
+ reason: `Workspace file ${filename} found`,
476
+ excerpt,
477
+ },
478
+ scope: { pathRoot: rootPath },
479
+ });
480
+ }
481
+ }
482
+ function scanSkills(workspaceDir, workspaceRel, manifest, rootPath, signals) {
483
+ const skillsDir = path.join(workspaceDir, "skills");
484
+ if (!existsAndIsDir(skillsDir))
485
+ return;
486
+ let entries;
487
+ try {
488
+ entries = fs.readdirSync(skillsDir);
489
+ }
490
+ catch {
491
+ return;
492
+ }
493
+ for (const entry of entries) {
494
+ if (!entry.endsWith(".md"))
495
+ continue;
496
+ if (entry === "SKILL_INDEX.md")
497
+ continue; // Index file, not a skill itself
498
+ const skillPath = path.join(skillsDir, entry);
499
+ if (!existsAndIsFile(skillPath))
500
+ continue;
501
+ // Read first meaningful line for description
502
+ const content = safeReadFile(skillPath);
503
+ let description = `Skill workflow: ${entry.replace(".md", "")}`;
504
+ if (content) {
505
+ const lines = content.split("\n").filter((l) => l.trim());
506
+ const firstLine = lines[0] || "";
507
+ if (firstLine.startsWith("#")) {
508
+ description = firstLine.replace(/^#+\s*/, "").trim();
509
+ }
510
+ }
511
+ const skillName = entry
512
+ .replace(".md", "")
513
+ .replace(/-/g, " ")
514
+ .replace(/\b\w/g, (c) => c.toUpperCase());
515
+ signals.push({
516
+ kind: "skill",
517
+ frameworkId: `openclaw-skill-${entry.replace(".md", "")}`,
518
+ frameworkName: `OpenClaw Skill: ${skillName}`,
519
+ evidence: {
520
+ file: path.join(workspaceRel, "skills", entry),
521
+ reason: description,
522
+ excerpt: `Skill file: ${entry}`,
523
+ },
524
+ scope: { pathRoot: rootPath },
525
+ });
526
+ }
527
+ }
528
+ function scanSubagents(workspaceDir, workspaceRel, manifest, rootPath, signals) {
529
+ const subagentsDir = path.join(workspaceDir, "subagents");
530
+ if (!existsAndIsDir(subagentsDir))
531
+ return;
532
+ let entries;
533
+ try {
534
+ entries = fs.readdirSync(subagentsDir);
535
+ }
536
+ catch {
537
+ return;
538
+ }
539
+ for (const entry of entries) {
540
+ const entryPath = path.join(subagentsDir, entry);
541
+ if (!existsAndIsDir(entryPath))
542
+ continue;
543
+ // List files in the subagent directory
544
+ let subFiles;
545
+ try {
546
+ subFiles = fs.readdirSync(entryPath).filter((f) => {
547
+ const fp = path.join(entryPath, f);
548
+ try {
549
+ return fs.statSync(fp).isFile() && !f.startsWith(".");
550
+ }
551
+ catch {
552
+ return false;
553
+ }
554
+ });
555
+ }
556
+ catch {
557
+ continue;
558
+ }
559
+ // Filter out __pycache__ etc
560
+ const meaningfulFiles = subFiles.filter((f) => !f.endsWith(".pyc") && f !== "__pycache__");
561
+ // Try to read protocol.md for description
562
+ let description = `Subagent: ${entry}`;
563
+ const protocolPath = path.join(entryPath, "protocol.md");
564
+ if (existsAndIsFile(protocolPath)) {
565
+ const content = safeReadFile(protocolPath);
566
+ if (content) {
567
+ const lines = content.split("\n").filter((l) => l.trim());
568
+ const firstLine = lines[0] || "";
569
+ if (firstLine.startsWith("#")) {
570
+ description = firstLine.replace(/^#+\s*/, "").trim();
571
+ }
572
+ }
573
+ }
574
+ const subagentName = entry.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
575
+ signals.push({
576
+ kind: "subagent",
577
+ frameworkId: `openclaw-subagent-${entry}`,
578
+ frameworkName: `OpenClaw Subagent: ${subagentName}`,
579
+ evidence: {
580
+ file: path.join(workspaceRel, "subagents", entry),
581
+ reason: description,
582
+ excerpt: meaningfulFiles.length > 0
583
+ ? `Files: ${meaningfulFiles.join(", ")}`
584
+ : "Empty subagent directory",
585
+ },
586
+ scope: { pathRoot: rootPath },
587
+ });
588
+ }
589
+ }
590
+ // ---------------------
591
+ // Helpers
592
+ // ---------------------
593
+ function existsAndIsDir(p) {
594
+ try {
595
+ return fs.statSync(p).isDirectory();
596
+ }
597
+ catch {
598
+ return false;
599
+ }
600
+ }
601
+ function existsAndIsFile(p) {
602
+ try {
603
+ return fs.statSync(p).isFile();
604
+ }
605
+ catch {
606
+ return false;
607
+ }
608
+ }
609
+ function safeReadFile(p) {
610
+ try {
611
+ return fs.readFileSync(p, "utf-8");
612
+ }
613
+ catch {
614
+ return null;
615
+ }
103
616
  }
@@ -41,29 +41,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
41
41
  exports.parsePackageJson = parsePackageJson;
42
42
  const fs = __importStar(require("fs"));
43
43
  const path = __importStar(require("path"));
44
- /** Framework detection rules: dep name patterns → framework info */
45
- const FRAMEWORK_DETECTION_RULES = [
46
- { depPattern: "@angular/core", frameworkId: "angular", frameworkName: "Angular" },
47
- { depPattern: "react", frameworkId: "react", frameworkName: "React" },
48
- { depPattern: "react-dom", frameworkId: "react-dom", frameworkName: "React DOM" },
49
- { depPattern: "next", frameworkId: "nextjs", frameworkName: "Next.js" },
50
- { depPattern: "vue", frameworkId: "vue", frameworkName: "Vue.js" },
51
- { depPattern: "nuxt", frameworkId: "nuxt", frameworkName: "Nuxt" },
52
- { depPattern: "@sveltejs/kit", frameworkId: "sveltekit", frameworkName: "SvelteKit" },
53
- { depPattern: "svelte", frameworkId: "svelte", frameworkName: "Svelte" },
54
- { depPattern: "express", frameworkId: "express", frameworkName: "Express" },
55
- { depPattern: "@nestjs/core", frameworkId: "nestjs", frameworkName: "NestJS" },
56
- { depPattern: "fastify", frameworkId: "fastify", frameworkName: "Fastify" },
57
- { depPattern: "koa", frameworkId: "koa", frameworkName: "Koa" },
58
- { depPattern: "typescript", frameworkId: "typescript", frameworkName: "TypeScript" },
59
- { depPattern: "@ax-llm/ax", frameworkId: "ax-llm", frameworkName: "Ax-LLM" },
60
- { depPattern: "electron", frameworkId: "electron", frameworkName: "Electron" },
61
- { depPattern: "react-native", frameworkId: "react-native", frameworkName: "React Native" },
62
- { depPattern: "tailwindcss", frameworkId: "tailwindcss", frameworkName: "Tailwind CSS" },
63
- { depPattern: "vite", frameworkId: "vite", frameworkName: "Vite" },
64
- { depPattern: "webpack", frameworkId: "webpack", frameworkName: "Webpack" },
65
- { depPattern: "esbuild", frameworkId: "esbuild", frameworkName: "esbuild" },
66
- ];
44
+ const constants_1 = require("../../constants");
67
45
  /**
68
46
  * Determine version certainty from a semver-like string.
69
47
  */
@@ -126,7 +104,7 @@ function parsePackageJson(manifest) {
126
104
  scope: { pathRoot: rootPath },
127
105
  });
128
106
  // Detect frameworks from dependencies
129
- for (const rule of FRAMEWORK_DETECTION_RULES) {
107
+ for (const rule of constants_1.NPM_FRAMEWORKS) {
130
108
  const depName = typeof rule.depPattern === "string" ? rule.depPattern : undefined;
131
109
  let matchedDep;
132
110
  let matchedVersion;