skillfish 1.0.21 → 1.0.23

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.
@@ -9,7 +9,7 @@ import pc from 'picocolors';
9
9
  import { printBanner } from '../lib/banner.js';
10
10
  import { trackInstall } from '../telemetry.js';
11
11
  import { isValidPath, parseFrontmatter, deriveSkillName, toTitleCase, truncate, batchMap, createJsonOutput, isInputTTY, isTTY, } from '../utils.js';
12
- import { getDetectedAgents, AGENT_CONFIGS } from '../lib/agents.js';
12
+ import { getDetectedAgentsForLocation, AGENT_CONFIGS, } from '../lib/agents.js';
13
13
  import { findAllSkillMdFiles, fetchSkillMdContent, fetchDefaultBranch, fetchTreeSha, getSkillSha, SKILL_FILENAME, RateLimitError, RepoNotFoundError, NetworkError, GitHubApiError, } from '../lib/github.js';
14
14
  import { installSkill } from '../lib/installer.js';
15
15
  import { EXIT_CODES, isValidName } from '../lib/constants.js';
@@ -77,6 +77,10 @@ Examples:
77
77
  const projectFlag = options.project ?? false;
78
78
  const globalFlag = options.global ?? false;
79
79
  let explicitPath = options.path ?? null;
80
+ // Validate flag conflicts
81
+ if (projectFlag && globalFlag) {
82
+ exitWithError('Cannot use both --project and --global. Choose one.', EXIT_CODES.INVALID_ARGS);
83
+ }
80
84
  // Validate --path if provided
81
85
  if (explicitPath !== null) {
82
86
  if (!isValidPath(explicitPath)) {
@@ -144,17 +148,26 @@ Examples:
144
148
  }
145
149
  const { paths: skillPaths, branch: discoveredBranch, sha: discoveredSha, tree: discoveredTree, } = discoveryResult;
146
150
  // 2. Determine install location (global vs project)
147
- const baseDir = await selectInstallLocation(projectFlag, globalFlag, jsonMode);
148
- // 3. Select agents to install to
149
- const detected = getDetectedAgents();
151
+ const { baseDir, location } = await selectInstallLocation(projectFlag, globalFlag, jsonMode);
152
+ const isLocal = location === 'project';
153
+ // 3. Select agents to install to (location-aware detection)
154
+ const detected = getDetectedAgentsForLocation(location, process.cwd());
150
155
  if (detected.length === 0) {
151
- const errorMsg = 'No agents detected. Install Claude Code, Cursor, or another supported agent first.';
156
+ // No agents detected for this location - provide helpful guidance
157
+ const locationLabel = isLocal ? 'this project' : 'your system';
158
+ const errorMsg = `No agents detected in ${locationLabel}.`;
159
+ const hint = isLocal
160
+ ? 'Create an agent directory (e.g., .claude/) or use --global to install globally.'
161
+ : 'Install Claude Code, Cursor, or another supported agent first.';
152
162
  if (jsonMode) {
153
- addError(errorMsg);
163
+ addError(`${errorMsg} ${hint}`);
154
164
  outputJsonAndExit(EXIT_CODES.GENERAL_ERROR);
155
165
  }
156
166
  p.log.error(errorMsg);
157
- p.outro(pc.dim('https://skill.fish/agents'));
167
+ p.log.info(pc.dim(hint));
168
+ if (!isLocal) {
169
+ p.outro(pc.dim('https://skill.fish/agents'));
170
+ }
158
171
  process.exit(EXIT_CODES.GENERAL_ERROR);
159
172
  }
160
173
  let targetAgents;
@@ -167,7 +180,6 @@ Examples:
167
180
  }
168
181
  else {
169
182
  // Interactive: let user choose from detected agents
170
- const isLocal = baseDir !== homedir();
171
183
  targetAgents = await selectAgents(detected, isLocal, jsonMode);
172
184
  }
173
185
  // Install each selected skill
@@ -226,7 +238,6 @@ Examples:
226
238
  continue;
227
239
  }
228
240
  // Log installed/skipped for this skill
229
- const isLocal = baseDir !== homedir();
230
241
  const pathPrefix = isLocal ? '.' : '~';
231
242
  for (const installed of result.installed) {
232
243
  if (!jsonMode) {
@@ -275,27 +286,26 @@ Examples:
275
286
  }
276
287
  process.exit(EXIT_CODES.SUCCESS);
277
288
  });
278
- // === Helper Functions ===
279
289
  async function selectInstallLocation(projectFlag, globalFlag, jsonMode) {
280
290
  // If flag specified, use it
281
291
  if (projectFlag) {
282
292
  if (!jsonMode) {
283
293
  p.log.info(`Location: ${pc.cyan('Project')} ${pc.dim('(./')}${pc.dim(AGENT_CONFIGS[0].dir)}${pc.dim(')')}`);
284
294
  }
285
- return process.cwd();
295
+ return { baseDir: process.cwd(), location: 'project' };
286
296
  }
287
297
  if (globalFlag) {
288
298
  if (!jsonMode) {
289
299
  p.log.info(`Location: ${pc.cyan('Global')} ${pc.dim('(~/')}${pc.dim(AGENT_CONFIGS[0].dir)}${pc.dim(')')}`);
290
300
  }
291
- return homedir();
301
+ return { baseDir: homedir(), location: 'global' };
292
302
  }
293
303
  // Non-TTY or JSON mode defaults to global
294
304
  if (!isInputTTY() || jsonMode) {
295
- return homedir();
305
+ return { baseDir: homedir(), location: 'global' };
296
306
  }
297
307
  // Interactive selection
298
- const location = await p.select({
308
+ const locationChoice = await p.select({
299
309
  message: 'Install location',
300
310
  options: [
301
311
  {
@@ -310,11 +320,13 @@ async function selectInstallLocation(projectFlag, globalFlag, jsonMode) {
310
320
  },
311
321
  ],
312
322
  });
313
- if (p.isCancel(location)) {
323
+ if (p.isCancel(locationChoice)) {
314
324
  p.cancel('Cancelled');
315
325
  process.exit(EXIT_CODES.SUCCESS);
316
326
  }
317
- return location === 'project' ? process.cwd() : homedir();
327
+ return locationChoice === 'project'
328
+ ? { baseDir: process.cwd(), location: 'project' }
329
+ : { baseDir: homedir(), location: 'global' };
318
330
  }
319
331
  async function selectAgents(agents, isLocal, jsonMode) {
320
332
  const pathPrefix = isLocal ? '.' : '~';
@@ -8,7 +8,7 @@ import { existsSync, mkdirSync, writeFileSync } from 'fs';
8
8
  import * as p from '@clack/prompts';
9
9
  import pc from 'picocolors';
10
10
  import { printBanner } from '../lib/banner.js';
11
- import { getDetectedAgents, getAgentSkillDir } from '../lib/agents.js';
11
+ import { getDetectedAgentsForLocation, getAgentSkillDir, } from '../lib/agents.js';
12
12
  import { SKILL_FILENAME } from '../lib/github.js';
13
13
  import { EXIT_CODES } from '../lib/constants.js';
14
14
  import { isInputTTY, isTTY } from '../utils.js';
@@ -294,17 +294,26 @@ Examples:
294
294
  }
295
295
  }
296
296
  // 5. Determine install location (global vs project)
297
- const baseDir = await selectInstallLocation(projectFlag, globalFlag, jsonMode);
298
- // 6. Select agents to create skill for
299
- const detected = getDetectedAgents();
297
+ const { baseDir, location } = await selectInstallLocation(projectFlag, globalFlag, jsonMode);
298
+ const isLocal = location === 'project';
299
+ // 6. Select agents to create skill for (location-aware detection)
300
+ const detected = getDetectedAgentsForLocation(location, process.cwd());
300
301
  if (detected.length === 0) {
301
- const errorMsg = 'No agents detected. Install Claude Code, Cursor, or another supported agent first.';
302
+ // No agents detected for this location - provide helpful guidance
303
+ const locationLabel = isLocal ? 'this project' : 'your system';
304
+ const errorMsg = `No agents detected in ${locationLabel}.`;
305
+ const hint = isLocal
306
+ ? 'Create an agent directory (e.g., .claude/) or use --global to create globally.'
307
+ : 'Install Claude Code, Cursor, or another supported agent first.';
302
308
  if (jsonMode) {
303
- addError(errorMsg);
309
+ addError(`${errorMsg} ${hint}`);
304
310
  outputJsonAndExit(EXIT_CODES.GENERAL_ERROR);
305
311
  }
306
312
  p.log.error(errorMsg);
307
- p.outro(pc.dim('https://skill.fish/agents'));
313
+ p.log.info(pc.dim(hint));
314
+ if (!isLocal) {
315
+ p.outro(pc.dim('https://skill.fish/agents'));
316
+ }
308
317
  process.exit(EXIT_CODES.GENERAL_ERROR);
309
318
  }
310
319
  let targetAgents;
@@ -317,7 +326,6 @@ Examples:
317
326
  }
318
327
  else {
319
328
  // Interactive: let user choose from detected agents
320
- const isLocal = baseDir !== homedir();
321
329
  targetAgents = await selectAgents(detected, isLocal, jsonMode);
322
330
  }
323
331
  // 7. Create the skill
@@ -329,7 +337,6 @@ Examples:
329
337
  license,
330
338
  };
331
339
  const skillContent = generateSkillMd(skillMeta, optionalDirs);
332
- const isLocal = baseDir !== homedir();
333
340
  const pathPrefix = isLocal ? '.' : '~';
334
341
  if (!jsonMode) {
335
342
  p.log.step(`Creating ${pc.bold(skillName)}`);
@@ -419,27 +426,26 @@ Examples:
419
426
  }
420
427
  process.exit(created > 0 || skipped > 0 ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERAL_ERROR);
421
428
  });
422
- // === Helper Functions ===
423
429
  async function selectInstallLocation(projectFlag, globalFlag, jsonMode) {
424
430
  // If flag specified, use it
425
431
  if (projectFlag) {
426
432
  if (!jsonMode) {
427
433
  p.log.info(`Location: ${pc.cyan('Project')} ${pc.dim('(current directory)')}`);
428
434
  }
429
- return process.cwd();
435
+ return { baseDir: process.cwd(), location: 'project' };
430
436
  }
431
437
  if (globalFlag) {
432
438
  if (!jsonMode) {
433
439
  p.log.info(`Location: ${pc.cyan('Global')} ${pc.dim('(home directory)')}`);
434
440
  }
435
- return homedir();
441
+ return { baseDir: homedir(), location: 'global' };
436
442
  }
437
443
  // Non-TTY or JSON mode defaults to project (more common for init)
438
444
  if (!isInputTTY() || jsonMode) {
439
- return process.cwd();
445
+ return { baseDir: process.cwd(), location: 'project' };
440
446
  }
441
447
  // Interactive selection
442
- const location = await p.select({
448
+ const locationChoice = await p.select({
443
449
  message: 'Install location',
444
450
  options: [
445
451
  {
@@ -454,11 +460,13 @@ async function selectInstallLocation(projectFlag, globalFlag, jsonMode) {
454
460
  },
455
461
  ],
456
462
  });
457
- if (p.isCancel(location)) {
463
+ if (p.isCancel(locationChoice)) {
458
464
  p.cancel('Cancelled');
459
465
  process.exit(EXIT_CODES.SUCCESS);
460
466
  }
461
- return location === 'project' ? process.cwd() : homedir();
467
+ return locationChoice === 'project'
468
+ ? { baseDir: process.cwd(), location: 'project' }
469
+ : { baseDir: homedir(), location: 'global' };
462
470
  }
463
471
  async function selectAgents(agents, isLocal, jsonMode) {
464
472
  const pathPrefix = isLocal ? '.' : '~';
@@ -7,7 +7,7 @@ import { join } from 'path';
7
7
  import * as p from '@clack/prompts';
8
8
  import pc from 'picocolors';
9
9
  import { printBanner } from '../lib/banner.js';
10
- import { getDetectedAgents, getAgentSkillDir } from '../lib/agents.js';
10
+ import { getDetectedAgentsForLocation, getAgentSkillDir, } from '../lib/agents.js';
11
11
  import { listInstalledSkillsInDir } from '../lib/installer.js';
12
12
  import { EXIT_CODES } from '../lib/constants.js';
13
13
  import { isTTY, isInputTTY } from '../utils.js';
@@ -56,14 +56,38 @@ Examples:
56
56
  p.log.error(message);
57
57
  process.exit(exitCode);
58
58
  }
59
+ // Validate flag conflicts
60
+ if (projectFlag && globalFlag) {
61
+ exitWithError('Cannot use both --project and --global. Choose one.', EXIT_CODES.INVALID_ARGS, {
62
+ installed: [],
63
+ agents_detected: [],
64
+ });
65
+ }
59
66
  // Determine which locations to check
60
67
  // By default, check both global and project. Flags narrow it down.
61
68
  const checkGlobal = !projectFlag; // Check global unless --project is set
62
69
  const checkProject = !globalFlag; // Check project unless --global is set
63
- // Detect agents
64
- const detected = getDetectedAgents();
70
+ // Determine detection location based on flags
71
+ const detectionLocation = projectFlag && !globalFlag ? 'project' : globalFlag && !projectFlag ? 'global' : 'both';
72
+ // Detect agents for the appropriate location
73
+ const detected = getDetectedAgentsForLocation(detectionLocation, process.cwd());
74
+ // When a specific location flag is used and no agents found, return empty results (not an error)
75
+ // Only error when no flags specified (location='both') and no agents found anywhere
65
76
  if (detected.length === 0) {
66
- exitWithError('No agents detected. Install Claude Code, Cursor, or another supported agent first.', EXIT_CODES.GENERAL_ERROR, { installed: [], agents_detected: [] });
77
+ if (detectionLocation === 'both') {
78
+ exitWithError('No agents detected. Install Claude Code, Cursor, or another supported agent first.', EXIT_CODES.GENERAL_ERROR, { installed: [], agents_detected: [] });
79
+ }
80
+ // For --project or --global with no agents, return success with empty list
81
+ if (jsonMode) {
82
+ outputJsonAndExit(EXIT_CODES.SUCCESS, { installed: [], agents_detected: [] });
83
+ }
84
+ if (isTTY()) {
85
+ printBanner();
86
+ }
87
+ p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim('Installed skills')}`);
88
+ const locationLabel = detectionLocation === 'project' ? 'project' : 'globally';
89
+ p.outro(pc.dim(`No agents configured ${locationLabel}`));
90
+ process.exit(EXIT_CODES.SUCCESS);
67
91
  }
68
92
  // Helper to collect skills for given agents
69
93
  function collectSkills(agents) {
@@ -185,76 +209,85 @@ Examples:
185
209
  agents_detected: detected.map((a) => a.name),
186
210
  });
187
211
  }
188
- // Interactive mode: show agent selector
212
+ // Interactive mode: location-first flow
189
213
  if (isInputTTY()) {
190
214
  if (isTTY() && !jsonMode) {
191
215
  printBanner();
192
216
  }
193
217
  p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim('Installed skills')}`);
194
- // Step 1: Build options with skill counts in label (always visible)
195
- const agentOptions = detected.map((agent) => {
196
- const { installed } = collectSkills([agent]);
197
- const count = installed.length;
198
- return {
199
- value: agent.name,
200
- label: `${agent.name} ${pc.dim(`(${count})`)}`,
201
- };
202
- });
203
- const selected = await p.select({
204
- message: 'Select an agent',
205
- options: agentOptions,
218
+ // Step 1: Choose location first
219
+ const locationChoice = await p.select({
220
+ message: 'View skills in',
221
+ options: [
222
+ { value: 'global', label: 'Global', hint: 'Skills in ~/' },
223
+ { value: 'project', label: 'Project', hint: 'Skills in ./' },
224
+ ],
206
225
  });
207
- if (p.isCancel(selected)) {
226
+ if (p.isCancel(locationChoice)) {
208
227
  p.cancel('Cancelled');
209
228
  process.exit(EXIT_CODES.SUCCESS);
210
229
  }
211
- const selectedAgent = detected.find((a) => a.name === selected);
212
- if (!selectedAgent) {
230
+ // Step 2: Detect agents for the chosen location
231
+ const locationAgents = getDetectedAgentsForLocation(locationChoice, process.cwd());
232
+ const isLocal = locationChoice === 'project';
233
+ const locationLabel = isLocal ? 'Project (./)' : 'Global (~/)';
234
+ if (locationAgents.length === 0) {
235
+ const hint = isLocal
236
+ ? 'No agents configured in this project.'
237
+ : 'No agents installed globally.';
238
+ p.log.info(pc.dim(hint));
239
+ p.outro(pc.dim('Done'));
213
240
  process.exit(EXIT_CODES.SUCCESS);
214
241
  }
215
- // Step 2: Get skills for selected agent
216
- const { globalSkills, projectSkills } = collectSkills([selectedAgent]);
217
- const hasBothLocations = globalSkills.length > 0 && projectSkills.length > 0;
218
- if (globalSkills.length === 0 && projectSkills.length === 0) {
219
- p.log.info(`No skills installed for ${pc.cyan(selectedAgent.name)}`);
242
+ // Step 3: Collect skills for this location only
243
+ const collectSkillsForLocation = (agents) => {
244
+ const skills = [];
245
+ const baseDir = isLocal ? process.cwd() : homedir();
246
+ for (const agent of agents) {
247
+ const skillDir = getAgentSkillDir(agent, baseDir);
248
+ const installed = listInstalledSkillsInDir(skillDir);
249
+ for (const skill of installed) {
250
+ skills.push({
251
+ agent: agent.name,
252
+ skill,
253
+ path: join(skillDir, skill),
254
+ location: locationChoice,
255
+ });
256
+ }
257
+ }
258
+ return skills;
259
+ };
260
+ const allSkills = collectSkillsForLocation(locationAgents);
261
+ if (allSkills.length === 0) {
262
+ p.log.info(`No skills installed in ${locationLabel}`);
220
263
  p.outro(pc.dim('Done'));
221
264
  process.exit(EXIT_CODES.SUCCESS);
222
265
  }
223
- // Step 3: If both locations have skills, let user choose location
224
- let skillsToShow;
225
- let locationLabel;
226
- if (hasBothLocations) {
227
- const locationOptions = [
228
- { value: 'global', label: `Global (~/) ${pc.dim(`(${globalSkills.length})`)}` },
229
- {
230
- value: 'project',
231
- label: `Project (./) ${pc.dim(`(${projectSkills.length})`)}`,
232
- },
233
- ];
234
- const selectedLocation = await p.select({
235
- message: 'Select location',
236
- options: locationOptions,
237
- });
238
- if (p.isCancel(selectedLocation)) {
239
- p.cancel('Cancelled');
240
- process.exit(EXIT_CODES.SUCCESS);
241
- }
242
- skillsToShow = selectedLocation === 'global' ? globalSkills : projectSkills;
243
- locationLabel = selectedLocation === 'global' ? 'Global (~/)' : 'Project (./)';
266
+ // Step 4: If multiple agents have skills, let user select one (or show all)
267
+ const agentSkillCounts = new Map();
268
+ for (const skill of allSkills) {
269
+ agentSkillCounts.set(skill.agent, (agentSkillCounts.get(skill.agent) || 0) + 1);
244
270
  }
245
- else {
246
- // Only one location has skills, use that
247
- if (globalSkills.length > 0) {
248
- skillsToShow = globalSkills;
249
- locationLabel = 'Global (~/)';
250
- }
251
- else {
252
- skillsToShow = projectSkills;
253
- locationLabel = 'Project (./)';
254
- }
271
+ if (agentSkillCounts.size === 1) {
272
+ // Only one agent has skills, show them directly
273
+ displaySkillsForLocation(allSkills, locationLabel);
274
+ process.exit(EXIT_CODES.SUCCESS);
275
+ }
276
+ // Multiple agents - let user choose
277
+ const agentOptions = Array.from(agentSkillCounts.entries()).map(([name, count]) => ({
278
+ value: name,
279
+ label: `${name} ${pc.dim(`(${count})`)}`,
280
+ }));
281
+ const selectedAgent = await p.select({
282
+ message: 'Select an agent',
283
+ options: agentOptions,
284
+ });
285
+ if (p.isCancel(selectedAgent)) {
286
+ p.cancel('Cancelled');
287
+ process.exit(EXIT_CODES.SUCCESS);
255
288
  }
256
- // Step 4: Display skills for selected location
257
- displaySkillsForLocation(skillsToShow, locationLabel);
289
+ const filteredSkills = allSkills.filter((s) => s.agent === selectedAgent);
290
+ displaySkillsForLocation(filteredSkills, locationLabel);
258
291
  process.exit(EXIT_CODES.SUCCESS);
259
292
  }
260
293
  // Non-interactive mode: display all agents with skills
@@ -8,7 +8,7 @@ import { existsSync, rmSync } from 'fs';
8
8
  import * as p from '@clack/prompts';
9
9
  import pc from 'picocolors';
10
10
  import { printBanner } from '../lib/banner.js';
11
- import { getDetectedAgents, getAgentSkillDir } from '../lib/agents.js';
11
+ import { getDetectedAgentsForLocation, getAgentSkillDir, } from '../lib/agents.js';
12
12
  import { listInstalledSkillsInDir } from '../lib/installer.js';
13
13
  import { isTTY, isInputTTY } from '../utils.js';
14
14
  import { EXIT_CODES } from '../lib/constants.js';
@@ -72,14 +72,28 @@ Examples:
72
72
  const projectFlag = options.project ?? false;
73
73
  const globalFlag = options.global ?? false;
74
74
  const targetAgentName = options.agent;
75
+ // Validate flag conflicts
76
+ if (projectFlag && globalFlag) {
77
+ exitWithError('Cannot use both --project and --global. Choose one.', EXIT_CODES.INVALID_ARGS, true);
78
+ }
75
79
  // Determine which locations to check
76
80
  // By default, check both global and project. Flags narrow it down.
77
81
  const checkGlobal = !projectFlag; // Check global unless --project is set
78
82
  const checkProject = !globalFlag; // Check project unless --global is set
79
- // Detect agents
80
- const detected = getDetectedAgents();
83
+ // Determine detection location based on flags
84
+ const detectionLocation = projectFlag && !globalFlag ? 'project' : globalFlag && !projectFlag ? 'global' : 'both';
85
+ // Detect agents for the appropriate location
86
+ const detected = getDetectedAgentsForLocation(detectionLocation, process.cwd());
81
87
  if (detected.length === 0) {
82
- exitWithError('No agents detected. Install Claude Code, Cursor, or another supported agent first.', EXIT_CODES.GENERAL_ERROR, true);
88
+ const locationHint = detectionLocation === 'project'
89
+ ? 'No agents configured in this project.'
90
+ : detectionLocation === 'global'
91
+ ? 'No agents installed globally.'
92
+ : 'No agents detected.';
93
+ const suggestion = detectionLocation === 'project'
94
+ ? ' Create an agent directory (e.g., .claude/) or use --global.'
95
+ : ' Install Claude Code, Cursor, or another supported agent first.';
96
+ exitWithError(locationHint + suggestion, EXIT_CODES.GENERAL_ERROR, true);
83
97
  }
84
98
  // Filter to target agent if specified
85
99
  let targetAgents = detected;
@@ -7,7 +7,7 @@ import { join } from 'path';
7
7
  import * as p from '@clack/prompts';
8
8
  import pc from 'picocolors';
9
9
  import { printBanner } from '../lib/banner.js';
10
- import { getDetectedAgents, getAgentSkillDir } from '../lib/agents.js';
10
+ import { getDetectedAgentsForLocation, getAgentSkillDir } from '../lib/agents.js';
11
11
  import { listInstalledSkillsInDir, installSkill } from '../lib/installer.js';
12
12
  import { readManifest } from '../lib/manifest.js';
13
13
  import { fetchRecursiveTree, getSkillSha, RateLimitError, RepoNotFoundError, NetworkError, GitHubApiError, } from '../lib/github.js';
@@ -58,8 +58,8 @@ Examples:
58
58
  printBanner();
59
59
  p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim(`v${version}`)}`);
60
60
  }
61
- // Detect agents
62
- const detected = getDetectedAgents();
61
+ // Detect agents (check both global and project for updates)
62
+ const detected = getDetectedAgentsForLocation('both', process.cwd());
63
63
  if (detected.length === 0) {
64
64
  exitWithError('No agents detected. Install Claude Code, Cursor, or another supported agent first.', EXIT_CODES.GENERAL_ERROR);
65
65
  }
@@ -14,10 +14,18 @@ export interface AgentConfig {
14
14
  }
15
15
  export declare const AGENT_CONFIGS: readonly AgentConfig[];
16
16
  /**
17
- * Check if an agent is detected on the system.
18
- * Checks home directory paths first, then current working directory paths.
17
+ * Check if an agent is installed globally (in home directory).
19
18
  */
20
- export declare function detectAgent(config: AgentConfig, baseDir?: string): boolean;
19
+ export declare function detectAgentGlobally(config: AgentConfig): boolean;
20
+ /**
21
+ * Check if an agent is configured in a specific project directory.
22
+ */
23
+ export declare function detectAgentInProject(config: AgentConfig, projectDir: string): boolean;
24
+ /**
25
+ * Check if an agent is detected (globally OR in project).
26
+ * Use this for combined detection when location hasn't been chosen yet.
27
+ */
28
+ export declare function detectAgent(config: AgentConfig, projectDir?: string): boolean;
21
29
  /**
22
30
  * Runtime agent type with detect function.
23
31
  */
@@ -27,13 +35,16 @@ export interface Agent {
27
35
  readonly detect: () => boolean;
28
36
  }
29
37
  /**
30
- * Build AGENTS array from config (preserves existing API).
38
+ * Location context for agent detection.
31
39
  */
32
- export declare function buildAgents(baseDir?: string): readonly Agent[];
40
+ export type DetectionLocation = 'global' | 'project' | 'both';
33
41
  /**
34
- * Get all detected agents.
42
+ * Get detected agents for a specific location context.
43
+ * - 'global': Only agents installed globally (homePaths)
44
+ * - 'project': Only agents configured in the project (cwdPaths)
45
+ * - 'both': Agents detected in either location (current behavior)
35
46
  */
36
- export declare function getDetectedAgents(baseDir?: string): readonly Agent[];
47
+ export declare function getDetectedAgentsForLocation(location: DetectionLocation, projectDir?: string): readonly Agent[];
37
48
  /**
38
49
  * Get the skill directory path for an agent.
39
50
  */
@@ -113,30 +113,48 @@ export const AGENT_CONFIGS = [
113
113
  },
114
114
  ];
115
115
  /**
116
- * Check if an agent is detected on the system.
117
- * Checks home directory paths first, then current working directory paths.
116
+ * Check if an agent is installed globally (in home directory).
118
117
  */
119
- export function detectAgent(config, baseDir) {
118
+ export function detectAgentGlobally(config) {
120
119
  const home = homedir();
121
- const cwd = baseDir ?? process.cwd();
122
- return (config.homePaths.some((p) => existsSync(join(home, p))) ||
123
- config.cwdPaths.some((p) => existsSync(join(cwd, p))));
120
+ return config.homePaths.some((p) => existsSync(join(home, p)));
124
121
  }
125
122
  /**
126
- * Build AGENTS array from config (preserves existing API).
123
+ * Check if an agent is configured in a specific project directory.
127
124
  */
128
- export function buildAgents(baseDir) {
129
- return AGENT_CONFIGS.map((config) => ({
130
- name: config.name,
131
- dir: config.dir,
132
- detect: () => detectAgent(config, baseDir),
133
- }));
125
+ export function detectAgentInProject(config, projectDir) {
126
+ return config.cwdPaths.some((p) => existsSync(join(projectDir, p)));
134
127
  }
135
128
  /**
136
- * Get all detected agents.
129
+ * Check if an agent is detected (globally OR in project).
130
+ * Use this for combined detection when location hasn't been chosen yet.
137
131
  */
138
- export function getDetectedAgents(baseDir) {
139
- return buildAgents(baseDir).filter((a) => a.detect());
132
+ export function detectAgent(config, projectDir) {
133
+ const cwd = projectDir ?? process.cwd();
134
+ return detectAgentGlobally(config) || detectAgentInProject(config, cwd);
135
+ }
136
+ /**
137
+ * Get detected agents for a specific location context.
138
+ * - 'global': Only agents installed globally (homePaths)
139
+ * - 'project': Only agents configured in the project (cwdPaths)
140
+ * - 'both': Agents detected in either location (current behavior)
141
+ */
142
+ export function getDetectedAgentsForLocation(location, projectDir) {
143
+ const cwd = projectDir ?? process.cwd();
144
+ return AGENT_CONFIGS.filter((config) => {
145
+ switch (location) {
146
+ case 'global':
147
+ return detectAgentGlobally(config);
148
+ case 'project':
149
+ return detectAgentInProject(config, cwd);
150
+ case 'both':
151
+ return detectAgent(config, cwd);
152
+ }
153
+ }).map((config) => ({
154
+ name: config.name,
155
+ dir: config.dir,
156
+ detect: () => detectAgent(config, cwd),
157
+ }));
140
158
  }
141
159
  /**
142
160
  * Get the skill directory path for an agent.
@@ -105,6 +105,7 @@ export async function searchSkillsInRegistry(query, limit = 5) {
105
105
  try {
106
106
  const params = new URLSearchParams({
107
107
  q: query,
108
+ type: 'skills',
108
109
  limit: String(limit),
109
110
  });
110
111
  const res = await fetchWithRetry(`${REGISTRY_SEARCH_URL}?${params.toString()}`, {
@@ -134,7 +135,7 @@ export async function searchSkillsInRegistry(query, limit = 5) {
134
135
  error: data.error || `Registry error: ${res.status}`,
135
136
  };
136
137
  }
137
- if (!data.tools || !Array.isArray(data.tools)) {
138
+ if (!data.skills || !Array.isArray(data.skills)) {
138
139
  return {
139
140
  success: true,
140
141
  results: [],
@@ -143,16 +144,16 @@ export async function searchSkillsInRegistry(query, limit = 5) {
143
144
  }
144
145
  // Validate and transform API response to our SearchResult format
145
146
  // Filter out malformed items that are missing required string fields
146
- const results = data.tools
147
+ const results = data.skills
147
148
  .filter((item) => typeof item.name === 'string' &&
148
149
  typeof item.slug === 'string' &&
149
150
  typeof item.github === 'string')
150
151
  .map((item) => ({
151
152
  name: item.name,
152
153
  slug: item.slug,
153
- type: item.type,
154
- owner: item.owner_name ?? '',
155
- ownerUrl: item.owner_url ?? '',
154
+ type: 'skill',
155
+ owner: item.owner?.name ?? '',
156
+ ownerUrl: item.owner?.url ?? '',
156
157
  github: item.github,
157
158
  description: item.description ?? '',
158
159
  stars: item.github_stars ?? 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillfish",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "All in one Skill manager for AI coding agents. Install, update, and sync Skills across Claude Code, Cursor, Copilot + more.",
5
5
  "type": "module",
6
6
  "bin": {