Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"@clack/prompts": "^0.11.0",
"@types/bun": "latest",
"@types/node": "^22.10.0",
"@vercel/detect-agent": "^1.2.1",
"gray-matter": "^4.0.3",
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions skills-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"version": 1,
"skills": {
"vercel-react-best-practices": {
"source": "vercel-labs/agent-skills",
"sourceType": "github",
"computedHash": "afeca00ac2f492d88fdd010e73d91719a5b57c564bb4a73325b0bd77e11323ab"
}
}
}
26 changes: 24 additions & 2 deletions src/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
type AuditResponse,
type PartnerAudit,
} from './telemetry.ts';
import { detectAgent, getAgentType } from './detect-agent.ts';
import { wellKnownProvider, type WellKnownSkill } from './providers/index.ts';
import {
addSkillToLock,
Expand Down Expand Up @@ -915,10 +916,31 @@ export async function runAdd(args: string[], options: AddOptions = {}): Promise<
options.yes = true;
}

// Auto-enable non-interactive mode when running inside an AI agent
const agentResult = await detectAgent();
if (agentResult.isAgent) {
options.yes = true;
// Auto-select the detected agent + universal agents (unless user explicitly specified agents)
if (!options.agent || options.agent.length === 0) {
const mappedAgent = getAgentType(agentResult.agent.name);
if (mappedAgent) {
options.agent = ensureUniversalAgents([mappedAgent]);
}
}
}

console.log();
p.intro(pc.bgCyan(pc.black(' skills ')));
if (!agentResult.isAgent) {
p.intro(pc.bgCyan(pc.black(' skills ')));
}

if (!process.stdin.isTTY) {
if (agentResult.isAgent) {
p.log.info(
pc.bgCyan(pc.black(pc.bold(` ${agentResult.agent.name} `))) +
' ' +
'Agent detected — installing non-interactively'
);
} else if (!process.stdin.isTTY) {
showInstallTip();
}

Expand Down
16 changes: 10 additions & 6 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { runList } from './list.ts';
import { removeCommand, parseRemoveOptions } from './remove.ts';
import { runSync, parseSyncOptions } from './sync.ts';
import { track } from './telemetry.ts';
import { isRunningInAgent } from './detect-agent.ts';
import { fetchSkillFolderHash, getGitHubToken } from './skill-lock.ts';

const __dirname = dirname(fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -607,9 +608,12 @@ async function runUpdate(): Promise<void> {

async function main(): Promise<void> {
const args = process.argv.slice(2);
const inAgent = await isRunningInAgent();

if (args.length === 0) {
showBanner();
if (!inAgent) {
showBanner();
}
return;
}

Expand All @@ -621,25 +625,25 @@ async function main(): Promise<void> {
case 'search':
case 'f':
case 's':
showLogo();
if (!inAgent) showLogo();
console.log();
await runFind(restArgs);
break;
case 'init':
showLogo();
if (!inAgent) showLogo();
console.log();
runInit(restArgs);
break;
case 'experimental_install': {
showLogo();
if (!inAgent) showLogo();
await runInstallFromLock(restArgs);
break;
}
case 'i':
case 'install':
case 'a':
case 'add': {
showLogo();
if (!inAgent) showLogo();
const { source: addSource, options: addOpts } = parseAddOptions(restArgs);
await runAdd(addSource, addOpts);
break;
Expand All @@ -656,7 +660,7 @@ async function main(): Promise<void> {
await removeCommand(skills, removeOptions);
break;
case 'experimental_sync': {
showLogo();
if (!inAgent) showLogo();
const { options: syncOptions } = parseSyncOptions(restArgs);
await runSync(restArgs, syncOptions);
break;
Expand Down
62 changes: 62 additions & 0 deletions src/detect-agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { determineAgent, type AgentResult } from '@vercel/detect-agent';
import { setDetectedAgent } from './telemetry.ts';
import type { AgentType } from './types.ts';

let cachedResult: AgentResult | null = null;

/**
* Map from @vercel/detect-agent names to skills-cli AgentType identifiers.
* Only includes agents that exist in both systems.
*/
const agentNameToType: Record<string, AgentType> = {
cursor: 'cursor',
'cursor-cli': 'cursor',
claude: 'claude-code',
cowork: 'claude-code',
devin: 'universal', // Devin not in skills-cli agent list, use universal
replit: 'replit',
gemini: 'gemini-cli',
codex: 'codex',
antigravity: 'antigravity',
'augment-cli': 'augment',
opencode: 'opencode',
'github-copilot': 'github-copilot',
};

/**
* Detect if the CLI is being run inside an AI agent environment.
* Results are cached after the first call. Also updates telemetry with the agent name.
*/
export async function detectAgent(): Promise<AgentResult> {
if (cachedResult) return cachedResult;
cachedResult = await determineAgent();
if (cachedResult.isAgent) {
setDetectedAgent(cachedResult.agent.name);
}
return cachedResult;
}

/**
* Returns true if the CLI is running inside a detected AI agent.
* When true, the CLI should skip interactive prompts and use sensible defaults.
*/
export async function isRunningInAgent(): Promise<boolean> {
const result = await detectAgent();
return result.isAgent;
}

/**
* Returns the name of the detected agent, or null if not running in an agent.
*/
export async function getAgentName(): Promise<string | null> {
const result = await detectAgent();
return result.isAgent ? result.agent.name : null;
}

/**
* Maps a detected agent name to the corresponding skills-cli AgentType.
* Returns null if the agent can't be mapped to a specific skills-cli agent.
*/
export function getAgentType(agentName: string): AgentType | null {
return agentNameToType[agentName] ?? null;
}
8 changes: 6 additions & 2 deletions src/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as readline from 'readline';
import { runAdd, parseAddOptions } from './add.ts';
import { track } from './telemetry.ts';
import { isRepoPrivate } from './source-parser.ts';
import { isRunningInAgent } from './detect-agent.ts';

const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
Expand Down Expand Up @@ -304,11 +305,14 @@ ${DIM} 2) npx skills add <owner/repo@skill>${RESET}`;
return;
}

// Interactive mode - show tip only if running non-interactively (likely in a coding agent)
if (isNonInteractive) {
// Skip interactive search when running inside an AI agent or non-TTY
if (isNonInteractive || (await isRunningInAgent())) {
console.log(agentTip);
console.log();
console.log(`${DIM}Usage: npx skills find <query>${RESET}`);
return;
}

const selected = await runSearchPrompt();

// Track telemetry for interactive search
Expand Down
12 changes: 12 additions & 0 deletions src/remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { readdir, rm, lstat } from 'fs/promises';
import { join } from 'path';
import { agents, detectInstalledAgents } from './agents.ts';
import { track } from './telemetry.ts';
import { detectAgent } from './detect-agent.ts';
import { removeSkillFromLock, getSkillFromLock } from './skill-lock.ts';
import type { AgentType } from './types.ts';
import {
Expand All @@ -21,6 +22,17 @@ export interface RemoveOptions {
}

export async function removeCommand(skillNames: string[], options: RemoveOptions) {
// Auto-enable non-interactive mode when running inside an AI agent
const agentResult = await detectAgent();
if (agentResult.isAgent) {
options.yes = true;
p.log.info(
pc.bgCyan(pc.black(pc.bold(` ${agentResult.agent.name} `))) +
' ' +
'Agent detected — removing non-interactively'
);
}

const isGlobal = options.global ?? false;
const cwd = process.cwd();

Expand Down
29 changes: 28 additions & 1 deletion src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { searchMultiselect } from './prompts/search-multiselect.ts';
import { addSkillToLocalLock, computeSkillFolderHash, readLocalLock } from './local-lock.ts';
import type { Skill, AgentType } from './types.ts';
import { track } from './telemetry.ts';
import { detectAgent, getAgentType } from './detect-agent.ts';

const isCancelled = (value: unknown): value is symbol => typeof value === 'symbol';

Expand Down Expand Up @@ -132,8 +133,34 @@ async function discoverNodeModuleSkills(
export async function runSync(args: string[], options: SyncOptions = {}): Promise<void> {
const cwd = process.cwd();

// Auto-enable non-interactive mode when running inside an AI agent
const agentResult = await detectAgent();
if (agentResult.isAgent) {
options.yes = true;
if (!options.agent || options.agent.length === 0) {
const mappedAgent = getAgentType(agentResult.agent.name);
if (mappedAgent) {
const agentList: AgentType[] = [mappedAgent];
for (const ua of getUniversalAgents()) {
if (!agentList.includes(ua)) agentList.push(ua);
}
options.agent = agentList;
}
}
}

console.log();
p.intro(pc.bgCyan(pc.black(' skills experimental_sync ')));
if (!agentResult.isAgent) {
p.intro(pc.bgCyan(pc.black(' skills experimental_sync ')));
}

if (agentResult.isAgent) {
p.log.info(
pc.bgCyan(pc.black(pc.bold(` ${agentResult.agent.name} `))) +
' ' +
'Agent detected — installing non-interactively'
);
}

const spinner = p.spinner();

Expand Down
14 changes: 14 additions & 0 deletions src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ type TelemetryData =
| SyncTelemetryData;

let cliVersion: string | null = null;
let detectedAgentName: string | null = null;

/**
* Set the detected AI agent name for telemetry tracking.
* Called once during agent detection, then included in all telemetry events.
*/
export function setDetectedAgent(agentName: string | null): void {
detectedAgentName = agentName;
}

function isCI(): boolean {
return !!(
Expand Down Expand Up @@ -144,6 +153,11 @@ export function track(data: TelemetryData): void {
params.set('ci', '1');
}

// Add detected AI agent name
if (detectedAgentName) {
params.set('agent', detectedAgentName);
}

// Add event data
for (const [key, value] of Object.entries(data)) {
if (value !== undefined && value !== null) {
Expand Down