agileflow 2.96.2 → 2.96.4

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/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.96.4] - 2026-02-06
11
+
12
+ ### Added
13
+ - Command deduplication to fix duplicate slash commands in autocomplete
14
+
15
+ ## [2.96.3] - 2026-02-06
16
+
17
+ ### Fixed
18
+ - Codex CLI subcommand support for nested commands
19
+
10
20
  ## [2.96.2] - 2026-02-06
11
21
 
12
22
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "2.96.2",
3
+ "version": "2.96.4",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -208,6 +208,69 @@ class BaseIdeSetup {
208
208
  }
209
209
  }
210
210
  }
211
+
212
+ // Remove duplicate command files that exist outside the agileflow/ subfolder
213
+ // These cause "command 2" entries in Claude Code's autocomplete
214
+ await this.removeDuplicateCommands(projectDir);
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Remove duplicate AgileFlow command files from the parent commands directory.
220
+ * When commands exist both in .claude/commands/agileflow/foo.md AND .claude/commands/foo.md,
221
+ * Claude Code shows them as "foo" and "foo 2" in autocomplete. This removes the loose copies.
222
+ * @param {string} projectDir - Project directory
223
+ */
224
+ async removeDuplicateCommands(projectDir) {
225
+ const commandsDir = path.join(projectDir, this.configDir, 'commands');
226
+ const agileflowDir = path.join(commandsDir, 'agileflow');
227
+
228
+ if (!(await fs.pathExists(commandsDir))) return;
229
+
230
+ // Get list of AgileFlow command names (files and directories)
231
+ const agileflowNames = new Set();
232
+ if (await fs.pathExists(agileflowDir)) {
233
+ const entries = await fs.readdir(agileflowDir, { withFileTypes: true });
234
+ for (const entry of entries) {
235
+ agileflowNames.add(entry.name);
236
+ }
237
+ }
238
+
239
+ // Also collect names from the source commands to catch pre-existing stale copies
240
+ const coreCommandsDir = path.join(projectDir, this.agileflowFolder, 'commands');
241
+ if (await fs.pathExists(coreCommandsDir)) {
242
+ const entries = await fs.readdir(coreCommandsDir, { withFileTypes: true });
243
+ for (const entry of entries) {
244
+ agileflowNames.add(entry.name);
245
+ }
246
+ }
247
+
248
+ if (agileflowNames.size === 0) return;
249
+
250
+ // Scan the parent commands directory for loose duplicates
251
+ let removedCount = 0;
252
+ const entries = await fs.readdir(commandsDir, { withFileTypes: true });
253
+
254
+ for (const entry of entries) {
255
+ // Skip the agileflow subfolder itself and non-agileflow items
256
+ if (entry.name === 'agileflow' || entry.name === 'AgileFlow') continue;
257
+
258
+ // Check if this file/directory matches an AgileFlow command name
259
+ if (agileflowNames.has(entry.name)) {
260
+ const duplicatePath = path.join(commandsDir, entry.name);
261
+ try {
262
+ await fs.remove(duplicatePath);
263
+ removedCount++;
264
+ } catch {
265
+ // Best effort - don't fail setup over cleanup
266
+ }
267
+ }
268
+ }
269
+
270
+ if (removedCount > 0) {
271
+ console.log(
272
+ chalk.dim(` Removed ${removedCount} duplicate command(s) from ${this.displayName}`)
273
+ );
211
274
  }
212
275
  }
213
276
 
@@ -269,9 +332,10 @@ class BaseIdeSetup {
269
332
  * Scan a directory for files with a specific extension
270
333
  * @param {string} dirPath - Directory path
271
334
  * @param {string} extension - File extension (e.g., '.md')
272
- * @returns {Promise<Array>} List of files
335
+ * @param {string} [prefix=''] - Prefix for nested names (used for recursion)
336
+ * @returns {Promise<Array>} List of files with name, path, filename
273
337
  */
274
- async scanDirectory(dirPath, extension) {
338
+ async scanDirectory(dirPath, extension, prefix = '') {
275
339
  const results = [];
276
340
 
277
341
  if (!(await this.exists(dirPath))) {
@@ -281,12 +345,23 @@ class BaseIdeSetup {
281
345
  const entries = await fs.readdir(dirPath, { withFileTypes: true });
282
346
 
283
347
  for (const entry of entries) {
348
+ const fullPath = path.join(dirPath, entry.name);
349
+
284
350
  if (entry.isFile() && entry.name.endsWith(extension)) {
351
+ // Build name with prefix for subcommands (e.g., ideate-new from ideate/new.md)
352
+ const baseName = entry.name.replace(extension, '');
353
+ const fullName = prefix ? `${prefix}-${baseName}` : baseName;
354
+
285
355
  results.push({
286
- name: entry.name.replace(extension, ''),
287
- path: path.join(dirPath, entry.name),
356
+ name: fullName,
357
+ path: fullPath,
288
358
  filename: entry.name,
289
359
  });
360
+ } else if (entry.isDirectory()) {
361
+ // Recursively scan subdirectories with prefix
362
+ const subPrefix = prefix ? `${prefix}-${entry.name}` : entry.name;
363
+ const subResults = await this.scanDirectory(fullPath, extension, subPrefix);
364
+ results.push(...subResults);
290
365
  }
291
366
  }
292
367
 
@@ -35,6 +35,11 @@ class ClaudeCodeSetup extends BaseIdeSetup {
35
35
  const { ideDir, agileflowTargetDir } = result;
36
36
  const agentsSource = path.join(agileflowDir, 'agents');
37
37
 
38
+ // Claude Code specific: Check for duplicates in user-level ~/.claude/commands/
39
+ // Commands in both ~/.claude/commands/ and .claude/commands/agileflow/ cause
40
+ // "command 2" entries in autocomplete
41
+ await this.removeUserLevelDuplicates(agileflowDir);
42
+
38
43
  // Claude Code specific: Install agents as spawnable subagents (.claude/agents/agileflow/)
39
44
  // This allows Task tool to spawn them with subagent_type: "agileflow-ui"
40
45
  const spawnableAgentsDir = path.join(ideDir, 'agents', 'agileflow');
@@ -282,6 +287,71 @@ class ClaudeCodeSetup extends BaseIdeSetup {
282
287
  await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
283
288
  console.log(chalk.dim(` - SessionStart hooks: welcome, archive, context-loader`));
284
289
  }
290
+
291
+ /**
292
+ * Remove AgileFlow command duplicates from user-level ~/.claude/commands/
293
+ * When the same command exists in both ~/.claude/commands/ and
294
+ * <project>/.claude/commands/agileflow/, Claude Code shows duplicates.
295
+ * @param {string} agileflowDir - AgileFlow installation directory (to get command names)
296
+ */
297
+ async removeUserLevelDuplicates(agileflowDir) {
298
+ const os = require('os');
299
+ const userCommandsDir = path.join(os.homedir(), '.claude', 'commands');
300
+
301
+ if (!(await fs.pathExists(userCommandsDir))) return;
302
+
303
+ // Collect AgileFlow command names from source
304
+ const agileflowNames = new Set();
305
+ const commandsSource = path.join(agileflowDir, 'commands');
306
+ const agentsSource = path.join(agileflowDir, 'agents');
307
+
308
+ for (const sourceDir of [commandsSource, agentsSource]) {
309
+ if (await fs.pathExists(sourceDir)) {
310
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
311
+ for (const entry of entries) {
312
+ agileflowNames.add(entry.name);
313
+ }
314
+ }
315
+ }
316
+
317
+ if (agileflowNames.size === 0) return;
318
+
319
+ // Check for agileflow-related items in user-level commands
320
+ let removedCount = 0;
321
+ const entries = await fs.readdir(userCommandsDir, { withFileTypes: true });
322
+
323
+ for (const entry of entries) {
324
+ // Only remove items that match AgileFlow command names
325
+ if (agileflowNames.has(entry.name)) {
326
+ const duplicatePath = path.join(userCommandsDir, entry.name);
327
+ try {
328
+ await fs.remove(duplicatePath);
329
+ removedCount++;
330
+ } catch {
331
+ // Best effort - don't fail setup over cleanup
332
+ }
333
+ }
334
+ }
335
+
336
+ // Also remove any agileflow/ subfolder from user-level commands
337
+ for (const folderName of ['agileflow', 'AgileFlow']) {
338
+ const userAgileflowDir = path.join(userCommandsDir, folderName);
339
+ if (await fs.pathExists(userAgileflowDir)) {
340
+ try {
341
+ await fs.remove(userAgileflowDir);
342
+ removedCount++;
343
+ } catch {
344
+ // Best effort
345
+ }
346
+ }
347
+ }
348
+
349
+ if (removedCount > 0) {
350
+ console.log(
351
+ chalk.dim(` - Removed ${removedCount} duplicate(s) from ~/.claude/commands/`)
352
+ );
353
+ }
354
+ }
285
355
  }
286
356
 
287
357
  module.exports = { ClaudeCodeSetup };