@yeaft/webchat-agent 0.1.306 → 0.1.307

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 (2) hide show
  1. package/conversation.js +74 -11
  2. package/package.json +1 -1
package/conversation.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync, readdirSync } from 'fs';
1
+ import { readFileSync, readdirSync, statSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir } from 'os';
4
4
  import ctx from './context.js';
@@ -25,9 +25,11 @@ const BUILTIN_COMMAND_DESCRIPTIONS = {
25
25
  };
26
26
 
27
27
  /**
28
- * Load command descriptions from installed plugin commands/*.md files.
28
+ * Load command and skill descriptions from installed plugins.
29
+ * Scans commands/*.md and skills/ ** /SKILL.md (recursively) for each plugin.
29
30
  * Parses YAML frontmatter to extract name and description fields.
30
31
  * Results are cached in ctx.slashCommandDescriptions.
32
+ * Also populates ctx.slashCommands from the loaded descriptions.
31
33
  */
32
34
  export function loadPluginCommandDescriptions() {
33
35
  if (Object.keys(ctx.slashCommandDescriptions).length > 0) return; // already loaded
@@ -40,11 +42,12 @@ export function loadPluginCommandDescriptions() {
40
42
  // pluginKey: "yeaft-skills@yeaft-skills-dev"
41
43
  const pluginName = pluginKey.split('@')[0]; // "yeaft-skills"
42
44
  for (const entry of entries) {
45
+ // --- Scan commands/*.md ---
43
46
  const commandsDir = join(entry.installPath, 'commands');
44
47
  let files;
45
48
  try {
46
49
  files = readdirSync(commandsDir).filter(f => f.endsWith('.md'));
47
- } catch { continue; }
50
+ } catch { files = []; }
48
51
 
49
52
  for (const file of files) {
50
53
  try {
@@ -59,22 +62,66 @@ export function loadPluginCommandDescriptions() {
59
62
  }
60
63
  } catch { /* skip unparseable files */ }
61
64
  }
65
+
66
+ // --- Scan skills/*/SKILL.md (recursive) ---
67
+ const skillsDir = join(entry.installPath, 'skills');
68
+ scanSkillsDir(skillsDir, pluginName);
62
69
  }
63
70
  }
64
71
 
65
- // Add builtin descriptions
72
+ // Add builtin descriptions as fallback
66
73
  for (const [name, desc] of Object.entries(BUILTIN_COMMAND_DESCRIPTIONS)) {
67
74
  if (!ctx.slashCommandDescriptions[name]) {
68
75
  ctx.slashCommandDescriptions[name] = desc;
69
76
  }
70
77
  }
71
78
 
72
- console.log(`[Preload] Loaded ${Object.keys(ctx.slashCommandDescriptions).length} command descriptions`);
79
+ // Build ctx.slashCommands from the loaded descriptions
80
+ // This populates the command list without needing a CLI spawn
81
+ if (ctx.slashCommands.length === 0) {
82
+ ctx.slashCommands = Object.keys(ctx.slashCommandDescriptions);
83
+ }
84
+
85
+ console.log(`[Preload] Loaded ${Object.keys(ctx.slashCommandDescriptions).length} command/skill descriptions from filesystem`);
73
86
  } catch (err) {
74
87
  console.warn('[Preload] Failed to load plugin command descriptions:', err.message);
75
88
  }
76
89
  }
77
90
 
91
+ /**
92
+ * Recursively scan a skills directory for SKILL.md files.
93
+ * Handles nested structures like skills/personas/pm-jobs/SKILL.md.
94
+ */
95
+ function scanSkillsDir(dir, pluginName) {
96
+ let entries;
97
+ try {
98
+ entries = readdirSync(dir);
99
+ } catch { return; }
100
+
101
+ for (const entry of entries) {
102
+ const fullPath = join(dir, entry);
103
+ let stat;
104
+ try { stat = statSync(fullPath); } catch { continue; }
105
+
106
+ if (stat.isDirectory()) {
107
+ // Check for SKILL.md in this subdirectory
108
+ const skillFile = join(fullPath, 'SKILL.md');
109
+ try {
110
+ const content = readFileSync(skillFile, 'utf-8');
111
+ const fm = parseFrontmatter(content);
112
+ if (fm.name && fm.description) {
113
+ const cliName = `${pluginName}:${fm.name}`;
114
+ const desc = fm.description.split('\n')[0].trim();
115
+ ctx.slashCommandDescriptions[cliName] = desc;
116
+ }
117
+ } catch { /* no SKILL.md or unparseable — continue into subdirectories */ }
118
+
119
+ // Recurse into subdirectories (handles nested skills like personas/pm-jobs/)
120
+ scanSkillsDir(fullPath, pluginName);
121
+ }
122
+ }
123
+ }
124
+
78
125
  /**
79
126
  * Minimal YAML frontmatter parser for commands/*.md files.
80
127
  * Extracts name and description from --- delimited frontmatter.
@@ -133,10 +180,13 @@ function prestartClaude(conversationId, workDir, resumeSessionId) {
133
180
  }
134
181
 
135
182
  /**
136
- * Preload slash commands from Claude CLI (fire-and-forget).
137
- * Spawns a minimal CLI process with a cheap built-in command (/cost)
138
- * to trigger the system init message, captures slash_commands,
139
- * then aborts immediately (no API tokens consumed).
183
+ * Preload slash commands — filesystem-first, CLI-spawn as optional fallback.
184
+ *
185
+ * 1. Load command/skill descriptions from plugin filesystem (instant, no process spawn).
186
+ * This also populates ctx.slashCommands from the loaded descriptions.
187
+ * 2. Send slash_commands_update to frontend immediately.
188
+ * 3. If ctx.slashCommands was already populated by filesystem, skip CLI spawn entirely.
189
+ * Otherwise fall back to spawning a CLI process to get commands from system init.
140
190
  *
141
191
  * @param {string} [workDir] - Project directory (default: agent workDir)
142
192
  * @param {string} [targetId] - conversationId to key the update to
@@ -145,9 +195,22 @@ function prestartClaude(conversationId, workDir, resumeSessionId) {
145
195
  export async function preloadSlashCommands(workDir, targetId = '__preload__') {
146
196
  const effectiveWorkDir = workDir || ctx.CONFIG.workDir;
147
197
 
148
- // Eagerly load plugin command descriptions (cached, runs once)
198
+ // Step 1: Load from filesystem (cached, runs once)
149
199
  loadPluginCommandDescriptions();
150
200
 
201
+ // Step 2: If filesystem loaded commands, send update immediately (no CLI needed)
202
+ if (ctx.slashCommands.length > 0) {
203
+ ctx.sendToServer({
204
+ type: 'slash_commands_update',
205
+ conversationId: targetId,
206
+ slashCommands: ctx.slashCommands,
207
+ slashCommandDescriptions: ctx.slashCommandDescriptions
208
+ });
209
+ console.log(`[Preload] ${targetId}: ${ctx.slashCommands.length} slash commands ready from filesystem (no CLI spawn needed)`);
210
+ return;
211
+ }
212
+
213
+ // Step 3: Fallback — spawn CLI to get slash commands from system init
151
214
  try {
152
215
  const abortController = new AbortController();
153
216
  // Use --print with a cheap built-in command to trigger system init.
@@ -177,7 +240,7 @@ export async function preloadSlashCommands(workDir, targetId = '__preload__') {
177
240
  slashCommands,
178
241
  slashCommandDescriptions: ctx.slashCommandDescriptions
179
242
  });
180
- console.log(`[Preload] ${targetId}: loaded ${slashCommands.length} slash commands from ${effectiveWorkDir}`);
243
+ console.log(`[Preload] ${targetId}: loaded ${slashCommands.length} slash commands from CLI fallback`);
181
244
  }
182
245
  abortController.abort();
183
246
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.306",
3
+ "version": "0.1.307",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",