opencode-agents 1.0.7
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/bin/agents.js +19 -0
- package/build.mjs +49 -0
- package/dist/agents.js +19 -0
- package/dist/index.cjs +26268 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.js +26273 -0
- package/dist/index.js.map +7 -0
- package/dist/templates/default-agent.md +35 -0
- package/package.json +38 -0
- package/src/commands/add.ts +44 -0
- package/src/commands/check.ts +11 -0
- package/src/commands/find.ts +25 -0
- package/src/commands/init.ts +84 -0
- package/src/commands/list.ts +42 -0
- package/src/commands/remove.ts +42 -0
- package/src/commands/update.ts +12 -0
- package/src/core/discover.ts +129 -0
- package/src/core/installer.ts +169 -0
- package/src/core/parser.ts +55 -0
- package/src/index.ts +64 -0
- package/src/types/index.ts +78 -0
- package/src/utils/filesystem.ts +177 -0
- package/src/utils/logger.ts +13 -0
- package/templates/default-agent.md +35 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: my-agent - Add your agent description here
|
|
3
|
+
mode: subagent
|
|
4
|
+
model: anthropic/claude-3-5-sonnet-20241022
|
|
5
|
+
temperature: 0.7
|
|
6
|
+
tools:
|
|
7
|
+
read: true
|
|
8
|
+
write: true
|
|
9
|
+
edit: true
|
|
10
|
+
bash: false
|
|
11
|
+
glob: true
|
|
12
|
+
grep: true
|
|
13
|
+
task: false
|
|
14
|
+
skill: false
|
|
15
|
+
hidden: false
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# my-agent
|
|
19
|
+
|
|
20
|
+
You are a custom AI agent. Describe your purpose and behavior here.
|
|
21
|
+
|
|
22
|
+
## When to Use
|
|
23
|
+
|
|
24
|
+
Describe when this agent should be invoked.
|
|
25
|
+
|
|
26
|
+
## Workflow
|
|
27
|
+
|
|
28
|
+
1. First step...
|
|
29
|
+
2. Second step...
|
|
30
|
+
3. Third step...
|
|
31
|
+
|
|
32
|
+
## Guidelines
|
|
33
|
+
|
|
34
|
+
- Add your specific guidelines here
|
|
35
|
+
- Be clear and concise
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-agents",
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"description": "CLI for managing AI coding agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agents": "./bin/agents.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "node build.mjs",
|
|
11
|
+
"dev": "node --watch src/index.ts",
|
|
12
|
+
"test": "vitest",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"opencode",
|
|
17
|
+
"claude",
|
|
18
|
+
"agent",
|
|
19
|
+
"cli",
|
|
20
|
+
"skills"
|
|
21
|
+
],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"commander": "^12.1.0",
|
|
26
|
+
"degit": "^2.8.4",
|
|
27
|
+
"gray-matter": "^4.0.3",
|
|
28
|
+
"ora": "^5.4.1",
|
|
29
|
+
"chalk": "^5.3.0",
|
|
30
|
+
"yaml": "^2.3.4"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^20.10.0",
|
|
34
|
+
"typescript": "^5.3.0",
|
|
35
|
+
"esbuild": "^0.24.0",
|
|
36
|
+
"vitest": "^1.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import { installAgent } from '../core/installer.js';
|
|
3
|
+
import { detectPlatforms } from '../utils/filesystem.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import type { AgentPlatform, InstallOptions } from '../types/index.js';
|
|
6
|
+
|
|
7
|
+
interface AddCommandOptions {
|
|
8
|
+
global: boolean;
|
|
9
|
+
agent: string[] | undefined;
|
|
10
|
+
agentName: string | undefined;
|
|
11
|
+
yes: boolean;
|
|
12
|
+
copy: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function addCommand(source: string, options: AddCommandOptions): Promise<void> {
|
|
16
|
+
const spinner = ora('Installing agent...').start();
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
let platforms: AgentPlatform[];
|
|
20
|
+
|
|
21
|
+
if (options.agent && options.agent.length > 0) {
|
|
22
|
+
platforms = options.agent as AgentPlatform[];
|
|
23
|
+
} else {
|
|
24
|
+
platforms = ['opencode'];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const installOptions: InstallOptions = {
|
|
28
|
+
source,
|
|
29
|
+
global: options.global,
|
|
30
|
+
platforms,
|
|
31
|
+
agentName: options.agentName,
|
|
32
|
+
copy: options.copy,
|
|
33
|
+
yes: options.yes,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
await installAgent(installOptions);
|
|
37
|
+
|
|
38
|
+
spinner.succeed(`Successfully installed agent from ${source}`);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
spinner.fail(`Failed to install agent: ${err instanceof Error ? err.message : String(err)}`);
|
|
41
|
+
logger.error(String(err));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
export async function checkCommand(): Promise<void> {
|
|
5
|
+
logger.info('Checking for agent updates...');
|
|
6
|
+
|
|
7
|
+
console.log(chalk.yellow('\nNote: Update checking requires version tracking.'));
|
|
8
|
+
console.log('To update agents, use: npx agents add <source> --force\n');
|
|
9
|
+
|
|
10
|
+
console.log(chalk.dim('Add version info to your agents to enable update checking.'));
|
|
11
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
interface FindCommandOptions {
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function findCommand(query?: string, _options?: FindCommandOptions): Promise<void> {
|
|
8
|
+
if (!query) {
|
|
9
|
+
console.log(chalk.bold('\nSearch for agents\n'));
|
|
10
|
+
console.log('Usage: npx agents find <query>');
|
|
11
|
+
console.log('\nExample:');
|
|
12
|
+
console.log(' npx agents find code-review');
|
|
13
|
+
console.log(' npx agents find testing');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
logger.info(`Searching for agents matching "${query}"...`);
|
|
18
|
+
|
|
19
|
+
console.log(chalk.yellow('\nNote: Agent search functionality requires integration with skills.sh API.'));
|
|
20
|
+
console.log('For now, you can browse agents at: https://skills.sh\n');
|
|
21
|
+
|
|
22
|
+
console.log(chalk.dim('Alternatively, you can:'));
|
|
23
|
+
console.log(chalk.dim(' 1. Check GitHub for agent repositories'));
|
|
24
|
+
console.log(chalk.dim(' 2. Use "npx agents add <owner/repo>" to install directly'));
|
|
25
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { ensureDir, detectPlatforms } from '../utils/filesystem.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname } from 'path';
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
interface InitCommandOptions {
|
|
11
|
+
global?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function initCommand(name?: string, _options?: InitCommandOptions): Promise<void> {
|
|
15
|
+
try {
|
|
16
|
+
const targetName = name || 'my-agent';
|
|
17
|
+
const targetDir = name ? join(process.cwd(), name) : process.cwd();
|
|
18
|
+
const targetPath = join(targetDir, `${targetName}.md`);
|
|
19
|
+
|
|
20
|
+
if (existsSync(targetPath)) {
|
|
21
|
+
logger.error(`Agent file already exists at ${targetPath}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
ensureDir(targetDir);
|
|
26
|
+
|
|
27
|
+
let template: string;
|
|
28
|
+
const templatePath = join(__dirname, '..', '..', 'templates', 'default-agent.md');
|
|
29
|
+
|
|
30
|
+
if (existsSync(templatePath)) {
|
|
31
|
+
template = readFileSync(templatePath, 'utf-8');
|
|
32
|
+
} else {
|
|
33
|
+
template = getDefaultTemplate(targetName);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const fs = await import('fs');
|
|
37
|
+
fs.writeFileSync(targetPath, template, 'utf-8');
|
|
38
|
+
|
|
39
|
+
logger.success(`Created agent template at ${targetPath}`);
|
|
40
|
+
console.log(`\nYou can now edit ${targetPath} to customize your agent.`);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
logger.error(`Failed to create agent: ${err instanceof Error ? err.message : String(err)}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getDefaultTemplate(name: string): string {
|
|
48
|
+
return `---
|
|
49
|
+
description: ${name} - Add your agent description here
|
|
50
|
+
mode: subagent
|
|
51
|
+
model: anthropic/claude-3-5-sonnet-20241022
|
|
52
|
+
temperature: 0.7
|
|
53
|
+
tools:
|
|
54
|
+
read: true
|
|
55
|
+
write: true
|
|
56
|
+
edit: true
|
|
57
|
+
bash: false
|
|
58
|
+
glob: true
|
|
59
|
+
grep: true
|
|
60
|
+
task: false
|
|
61
|
+
skill: false
|
|
62
|
+
hidden: false
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
# ${name}
|
|
66
|
+
|
|
67
|
+
You are a custom AI agent. Describe your purpose and behavior here.
|
|
68
|
+
|
|
69
|
+
## When to Use
|
|
70
|
+
|
|
71
|
+
Describe when this agent should be invoked.
|
|
72
|
+
|
|
73
|
+
## Workflow
|
|
74
|
+
|
|
75
|
+
1. First step...
|
|
76
|
+
2. Second step...
|
|
77
|
+
3. Third step...
|
|
78
|
+
|
|
79
|
+
## Guidelines
|
|
80
|
+
|
|
81
|
+
- Add your specific guidelines here
|
|
82
|
+
- Be clear and concise
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { listInstalledAgents } from '../core/installer.js';
|
|
3
|
+
import { detectPlatforms } from '../utils/filesystem.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import type { AgentPlatform } from '../types/index.js';
|
|
6
|
+
|
|
7
|
+
interface ListCommandOptions {
|
|
8
|
+
global: boolean;
|
|
9
|
+
agent: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function listCommand(options: ListCommandOptions): Promise<void> {
|
|
13
|
+
let platforms: AgentPlatform[];
|
|
14
|
+
|
|
15
|
+
if (options.agent) {
|
|
16
|
+
platforms = [options.agent as AgentPlatform];
|
|
17
|
+
} else {
|
|
18
|
+
platforms = ['opencode'];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const scope = options.global ? 'global' : 'project';
|
|
22
|
+
|
|
23
|
+
for (const platform of platforms) {
|
|
24
|
+
const agents = listInstalledAgents(platform, options.global);
|
|
25
|
+
|
|
26
|
+
if (agents.length === 0) {
|
|
27
|
+
logger.info(`No agents found for ${platform} (${scope})`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(chalk.bold(`\n${platform} agents (${scope}):\n`));
|
|
32
|
+
|
|
33
|
+
for (const agent of agents) {
|
|
34
|
+
const name = agent.agent.name || agent.path.split(/[/\\]/).pop()?.replace('.md', '') || 'unknown';
|
|
35
|
+
const mode = agent.agent.mode || 'subagent';
|
|
36
|
+
const description = agent.agent.description || 'No description';
|
|
37
|
+
|
|
38
|
+
console.log(` ${chalk.cyan(name)} ${chalk.gray(`[${mode}]`)}`);
|
|
39
|
+
console.log(` ${chalk.dim(description)}\n`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import { removeAgent } from '../core/installer.js';
|
|
3
|
+
import { detectPlatforms } from '../utils/filesystem.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import type { AgentPlatform } from '../types/index.js';
|
|
6
|
+
|
|
7
|
+
interface RemoveCommandOptions {
|
|
8
|
+
global: boolean;
|
|
9
|
+
agent: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function removeCommand(name: string, options: RemoveCommandOptions): Promise<void> {
|
|
13
|
+
const spinner = ora(`Removing agent "${name}"...`).start();
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
let platforms: AgentPlatform[];
|
|
17
|
+
|
|
18
|
+
if (options.agent) {
|
|
19
|
+
platforms = [options.agent as AgentPlatform];
|
|
20
|
+
} else {
|
|
21
|
+
platforms = detectPlatforms();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let removed = false;
|
|
25
|
+
|
|
26
|
+
for (const platform of platforms) {
|
|
27
|
+
const result = removeAgent(name, platform, options.global);
|
|
28
|
+
if (result) {
|
|
29
|
+
removed = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (removed) {
|
|
34
|
+
spinner.succeed(`Removed agent "${name}"`);
|
|
35
|
+
} else {
|
|
36
|
+
spinner.warn(`Agent "${name}" not found`);
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
spinner.fail(`Failed to remove agent: ${err instanceof Error ? err.message : String(err)}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
export async function updateCommand(): Promise<void> {
|
|
5
|
+
logger.info('Updating agents...');
|
|
6
|
+
|
|
7
|
+
console.log(chalk.yellow('\nNote: Auto-update functionality coming soon.\n'));
|
|
8
|
+
|
|
9
|
+
console.log(chalk.dim('To update an agent, remove it and reinstall:'));
|
|
10
|
+
console.log(chalk.dim(' npx agents remove <agent-name>'));
|
|
11
|
+
console.log(chalk.dim(' npx agents add <source>'));
|
|
12
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { join, basename } from 'path';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
3
|
+
import { parseAgentFile } from './parser.js';
|
|
4
|
+
import { findFiles, isDirectory } from '../utils/filesystem.js';
|
|
5
|
+
import type { AgentFile, Agent } from '../types/index.js';
|
|
6
|
+
|
|
7
|
+
const SEARCH_DIRECTORIES = [
|
|
8
|
+
'',
|
|
9
|
+
'agents',
|
|
10
|
+
'.agents',
|
|
11
|
+
'.opencode/agents',
|
|
12
|
+
'src/agents',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const PRIORITY_FILES = [
|
|
16
|
+
'AGENTS.md',
|
|
17
|
+
'SKILL.md',
|
|
18
|
+
'AGENT.md',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export async function discoverFromDirectory(dir: string): Promise<AgentFile[]> {
|
|
22
|
+
const agents: AgentFile[] = [];
|
|
23
|
+
|
|
24
|
+
for (const subDir of SEARCH_DIRECTORIES) {
|
|
25
|
+
const searchDir = subDir ? join(dir, subDir) : dir;
|
|
26
|
+
if (!existsSync(searchDir)) continue;
|
|
27
|
+
|
|
28
|
+
const mdFiles = findFiles(searchDir, /\.md$/);
|
|
29
|
+
|
|
30
|
+
for (const file of mdFiles) {
|
|
31
|
+
const fileName = basename(file);
|
|
32
|
+
if (fileName.startsWith('.')) continue;
|
|
33
|
+
if (fileName === 'README.md') continue;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const content = readFileSync(file, 'utf-8');
|
|
37
|
+
const agentFile = parseAgentFile(content, file);
|
|
38
|
+
agents.push(agentFile);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const fileName of PRIORITY_FILES) {
|
|
46
|
+
const priorityFile = join(dir, fileName);
|
|
47
|
+
if (existsSync(priorityFile)) {
|
|
48
|
+
try {
|
|
49
|
+
const content = readFileSync(priorityFile, 'utf-8');
|
|
50
|
+
const agentFile = parseAgentFile(content, priorityFile);
|
|
51
|
+
const exists = agents.some(a => a.path === agentFile.path);
|
|
52
|
+
if (!exists) {
|
|
53
|
+
agents.unshift(agentFile);
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return agents;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function discoverFromRepo(owner: string, repo: string, ref?: string): Promise<AgentFile[]> {
|
|
65
|
+
const degit = await import('degit');
|
|
66
|
+
const tempDir = await createTempDir();
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const source = ref ? `${owner}/${repo}#${ref}` : `${owner}/${repo}`;
|
|
70
|
+
await degit.default(source).clone(tempDir);
|
|
71
|
+
|
|
72
|
+
return discoverFromDirectory(tempDir);
|
|
73
|
+
} finally {
|
|
74
|
+
// Cleanup temp directory would happen here in production
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function createTempDir(): Promise<string> {
|
|
79
|
+
const { mkdtempSync } = await import('fs');
|
|
80
|
+
const { tmpdir } = await import('os');
|
|
81
|
+
return mkdtempSync(join(tmpdir(), 'npx-agents-'));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function discoverLocal(dir: string): AgentFile[] {
|
|
85
|
+
const agents: AgentFile[] = [];
|
|
86
|
+
|
|
87
|
+
if (!existsSync(dir)) {
|
|
88
|
+
return agents;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const mdFiles = findFiles(dir, /\.md$/);
|
|
92
|
+
|
|
93
|
+
for (const file of mdFiles) {
|
|
94
|
+
const fileName = basename(file);
|
|
95
|
+
if (fileName.startsWith('.') || fileName === 'README.md') continue;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const content = readFileSync(file, 'utf-8');
|
|
99
|
+
const agentFile = parseAgentFile(content, file);
|
|
100
|
+
agents.push(agentFile);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return agents;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function parseSource(source: string): { type: 'github' | 'local'; owner?: string; repo?: string; path: string } {
|
|
110
|
+
if (source.startsWith('.') || source.startsWith('/') || /^[a-zA-Z]:\\/.test(source)) {
|
|
111
|
+
return { type: 'local', path: source };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (source.includes('/') && !source.startsWith('http')) {
|
|
115
|
+
const parts = source.split('/');
|
|
116
|
+
if (parts.length >= 2) {
|
|
117
|
+
return { type: 'github', owner: parts[0], repo: parts[1].replace(/#.+$/, '') };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (source.startsWith('http://') || source.startsWith('https://')) {
|
|
122
|
+
const urlMatch = source.match(/github\.com[/:]([^/]+)\/([^/]+)/);
|
|
123
|
+
if (urlMatch) {
|
|
124
|
+
return { type: 'github', owner: urlMatch[1], repo: urlMatch[2].replace(/#.+$/, '') };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { type: 'local', path: source };
|
|
129
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { join, basename } from 'path';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, mkdirSync, copyFileSync, symlinkSync, rmSync } from 'fs';
|
|
3
|
+
import { tmpdir, homedir } from 'os';
|
|
4
|
+
import { mkdtempSync, rmSync as rmSyncFs } from 'fs';
|
|
5
|
+
import { parseAgentFile } from './parser.js';
|
|
6
|
+
import { discoverFromDirectory } from './discover.js';
|
|
7
|
+
import { ensureDir, getPlatformPaths, isDirectory, findFiles } from '../utils/filesystem.js';
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
import type { AgentPlatform, AgentFile, InstallOptions } from '../types/index.js';
|
|
10
|
+
|
|
11
|
+
export async function installAgent(options: InstallOptions): Promise<void> {
|
|
12
|
+
const { source, global, platforms, agentName, copy } = options;
|
|
13
|
+
|
|
14
|
+
logger.info(`Installing agent from: ${source}`);
|
|
15
|
+
|
|
16
|
+
const tempDir = await fetchSource(source);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const agents = await discoverFromDirectory(tempDir);
|
|
20
|
+
|
|
21
|
+
if (agents.length === 0) {
|
|
22
|
+
throw new Error('No agents found in the source');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const platform of platforms) {
|
|
26
|
+
const targetPaths = getPlatformPaths(platform, global);
|
|
27
|
+
|
|
28
|
+
for (const targetPath of targetPaths) {
|
|
29
|
+
ensureDir(targetPath);
|
|
30
|
+
|
|
31
|
+
for (const agentFile of agents) {
|
|
32
|
+
const name = agentName || agentFile.agent.name || basename(agentFile.path, '.md');
|
|
33
|
+
const finalPath = join(targetPath, `${name}.md`);
|
|
34
|
+
|
|
35
|
+
if (existsSync(finalPath) && !options.yes) {
|
|
36
|
+
logger.warn(`Agent "${name}" already exists at ${finalPath}`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (copy) {
|
|
41
|
+
const sourcePath = agentFile.path;
|
|
42
|
+
if (isDirectory(sourcePath)) {
|
|
43
|
+
copyDirectory(sourcePath, join(targetPath, name));
|
|
44
|
+
} else {
|
|
45
|
+
copyFileSync(sourcePath, finalPath);
|
|
46
|
+
}
|
|
47
|
+
logger.success(`Copied agent "${name}" to ${finalPath}`);
|
|
48
|
+
} else {
|
|
49
|
+
const sourceDir = tempDir;
|
|
50
|
+
try {
|
|
51
|
+
symlinkSync(sourceDir, finalPath, 'dir');
|
|
52
|
+
logger.success(`Symlinked agent "${name}" to ${finalPath}`);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
logger.warn(`Failed to create symlink, copying instead: ${err}`);
|
|
55
|
+
copyDirectory(sourceDir, join(targetPath, name));
|
|
56
|
+
logger.success(`Copied agent "${name}" to ${targetPath}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} finally {
|
|
63
|
+
if (existsSync(tempDir) && tempDir.startsWith(tmpdir())) {
|
|
64
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function fetchSource(source: string): Promise<string> {
|
|
70
|
+
if (source.startsWith('.') || source.startsWith('/') || /^[a-zA-Z]:\\/.test(source)) {
|
|
71
|
+
return source;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const degit = await import('degit');
|
|
75
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'npx-agents-'));
|
|
76
|
+
|
|
77
|
+
const parts = source.split('/');
|
|
78
|
+
let owner = parts[0];
|
|
79
|
+
let repo = parts[1]?.replace(/#.+$/, '') || '';
|
|
80
|
+
let ref = '';
|
|
81
|
+
|
|
82
|
+
if (source.includes('#')) {
|
|
83
|
+
const [repoPart, refPart] = source.split('#');
|
|
84
|
+
repo = repoPart.split('/')[1];
|
|
85
|
+
ref = refPart;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const target = ref ? `${owner}/${repo}#${ref}` : `${owner}/${repo}`;
|
|
90
|
+
await degit.default(target).clone(tempDir);
|
|
91
|
+
return tempDir;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
94
|
+
throw new Error(`Failed to fetch from ${source}: ${err}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function copyDirectory(source: string, target: string): void {
|
|
99
|
+
ensureDir(target);
|
|
100
|
+
const files = readdirSync(source);
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
const srcPath = join(source, file);
|
|
103
|
+
const destPath = join(target, file);
|
|
104
|
+
if (isDirectory(srcPath)) {
|
|
105
|
+
copyDirectory(srcPath, destPath);
|
|
106
|
+
} else {
|
|
107
|
+
copyFileSync(srcPath, destPath);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function listInstalledAgents(platform: AgentPlatform, global: boolean): AgentFile[] {
|
|
113
|
+
const agents: AgentFile[] = [];
|
|
114
|
+
const paths = getPlatformPaths(platform, global);
|
|
115
|
+
|
|
116
|
+
for (const path of paths) {
|
|
117
|
+
if (!existsSync(path)) continue;
|
|
118
|
+
|
|
119
|
+
const mdFiles = findFiles(path, /\.md$/);
|
|
120
|
+
for (const file of mdFiles) {
|
|
121
|
+
try {
|
|
122
|
+
const content = readFileSync(file, 'utf-8');
|
|
123
|
+
const agentFile = parseAgentFile(content, file);
|
|
124
|
+
agents.push(agentFile);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return agents;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function removeAgent(name: string, platform: AgentPlatform, global: boolean): boolean {
|
|
135
|
+
const paths = getPlatformPaths(platform, global);
|
|
136
|
+
|
|
137
|
+
for (const path of paths) {
|
|
138
|
+
const agentPath = join(path, `${name}.md`);
|
|
139
|
+
if (existsSync(agentPath)) {
|
|
140
|
+
rmSync(agentPath, { recursive: true, force: true });
|
|
141
|
+
logger.success(`Removed agent "${name}" from ${path}`);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (existsSync(path)) {
|
|
146
|
+
const entries = readdirSync(path);
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
const entryPath = join(path, entry);
|
|
149
|
+
const linkTarget = existsSync(entryPath) ? entryPath : null;
|
|
150
|
+
if (linkTarget && readlinkCheck(entryPath, name)) {
|
|
151
|
+
rmSync(entryPath, { recursive: true, force: true });
|
|
152
|
+
logger.success(`Removed agent "${name}" from ${path}`);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readlinkCheck(path: string, name: string): boolean {
|
|
163
|
+
try {
|
|
164
|
+
const stat = require('fs').lstatSync(path);
|
|
165
|
+
return stat.isSymbolicLink() && path.includes(name);
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import matter from 'gray-matter';
|
|
2
|
+
import type { Agent, AgentFile } from '../types/index.js';
|
|
3
|
+
|
|
4
|
+
export function parseAgentFile(content: string, path: string): AgentFile {
|
|
5
|
+
const { data, content: body } = matter(content);
|
|
6
|
+
|
|
7
|
+
if (!data.description) {
|
|
8
|
+
throw new Error(`Agent at ${path} is missing required "description" field in frontmatter`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const agent: Agent = {
|
|
12
|
+
description: data.description,
|
|
13
|
+
name: data.name,
|
|
14
|
+
mode: data.mode,
|
|
15
|
+
model: data.model,
|
|
16
|
+
temperature: data.temperature,
|
|
17
|
+
maxSteps: data.maxSteps,
|
|
18
|
+
color: data.color,
|
|
19
|
+
trigger: data.trigger,
|
|
20
|
+
hidden: data.hidden,
|
|
21
|
+
tools: data.tools,
|
|
22
|
+
permission: data.permission,
|
|
23
|
+
mcp: data.mcp,
|
|
24
|
+
version: data.version,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
path,
|
|
29
|
+
agent,
|
|
30
|
+
content: body.trim(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseAgentFromString(content: string, filename: string): AgentFile {
|
|
35
|
+
const baseName = filename.replace(/\.md$/, '');
|
|
36
|
+
return parseAgentFile(content, baseName);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function validateAgent(agent: Agent): string[] {
|
|
40
|
+
const errors: string[] = [];
|
|
41
|
+
|
|
42
|
+
if (!agent.description) {
|
|
43
|
+
errors.push('Missing required "description" field');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (agent.temperature !== undefined && (agent.temperature < 0 || agent.temperature > 1)) {
|
|
47
|
+
errors.push('Temperature must be between 0 and 1');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (agent.mode && !['primary', 'subagent', 'all'].includes(agent.mode)) {
|
|
51
|
+
errors.push('Mode must be "primary", "subagent", or "all"');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return errors;
|
|
55
|
+
}
|