@plosson/agentio 0.1.18 → 0.1.22

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/README.md CHANGED
@@ -67,10 +67,11 @@ Download from [GitHub Releases](https://github.com/plosson/agentio/releases/late
67
67
 
68
68
  | Service | Status | Commands |
69
69
  |---------|--------|----------|
70
- | Gmail | Available | `list`, `get`, `search`, `send`, `reply`, `archive`, `mark` |
70
+ | Gmail | Available | `list`, `get`, `search`, `send`, `reply`, `archive`, `mark`, `attachment`, `export` |
71
71
  | Telegram | Available | `send` |
72
- | Slack | Planned | - |
73
- | JIRA | Planned | - |
72
+ | Google Chat | Available | `send`, `list`, `get` |
73
+ | Slack | Available | `send` |
74
+ | JIRA | Available | `projects`, `search`, `get`, `comment`, `transitions`, `transition` |
74
75
  | Linear | Planned | - |
75
76
 
76
77
  ## Usage
@@ -95,6 +96,14 @@ agentio gmail send --to user@example.com --subject "Hello" --body "Message body"
95
96
 
96
97
  # Or pipe content
97
98
  echo "Message body" | agentio gmail send --to user@example.com --subject "Hello"
99
+
100
+ # Download attachments
101
+ agentio gmail attachment <message-id>
102
+ agentio gmail attachment <message-id> --name "document.pdf" --output ./downloads
103
+
104
+ # Export email as PDF
105
+ agentio gmail export <message-id>
106
+ agentio gmail export <message-id> --output email.pdf
98
107
  ```
99
108
 
100
109
  ### Telegram
@@ -110,6 +119,64 @@ agentio telegram send "Hello from agentio!"
110
119
  agentio telegram send --parse-mode markdown "**Bold** and _italic_"
111
120
  ```
112
121
 
122
+ ### Google Chat
123
+
124
+ ```bash
125
+ # Set up profile (webhook or OAuth)
126
+ agentio gchat profile add
127
+
128
+ # Send message via webhook
129
+ agentio gchat send "Hello from agentio!"
130
+
131
+ # Send with JSON payload for rich messages
132
+ agentio gchat send --json message.json
133
+
134
+ # List messages (OAuth profiles only)
135
+ agentio gchat list --space <space-id>
136
+
137
+ # Get a specific message (OAuth profiles only)
138
+ agentio gchat get <message-id> --space <space-id>
139
+ ```
140
+
141
+ ### Slack
142
+
143
+ ```bash
144
+ # Set up webhook profile
145
+ agentio slack profile add
146
+
147
+ # Send message
148
+ agentio slack send "Hello from agentio!"
149
+
150
+ # Send Block Kit message from JSON file
151
+ agentio slack send --json blocks.json
152
+ ```
153
+
154
+ ### JIRA
155
+
156
+ ```bash
157
+ # Authenticate with OAuth
158
+ agentio jira profile add
159
+
160
+ # List projects
161
+ agentio jira projects
162
+
163
+ # Search issues
164
+ agentio jira search --project MYPROJ --status "In Progress"
165
+ agentio jira search --jql "assignee = currentUser() AND status != Done"
166
+
167
+ # Get issue details
168
+ agentio jira get PROJ-123
169
+
170
+ # Add a comment
171
+ agentio jira comment PROJ-123 "This is my comment"
172
+
173
+ # View available transitions
174
+ agentio jira transitions PROJ-123
175
+
176
+ # Transition an issue
177
+ agentio jira transition PROJ-123 <transition-id>
178
+ ```
179
+
113
180
  ## Multi-Profile Support
114
181
 
115
182
  Each service supports multiple named profiles:
@@ -125,44 +192,49 @@ agentio gmail list --profile work
125
192
 
126
193
  ## Claude Code Integration
127
194
 
128
- agentio provides a plugin for [Claude Code](https://claude.com/claude-code) with skills for Gmail, Telegram, and Google Chat operations.
195
+ agentio provides plugins for [Claude Code](https://claude.ai/download) with skills for Gmail, Telegram, and Google Chat operations.
129
196
 
130
- ### Add the Marketplace
197
+ ### Install the Plugin
131
198
 
132
199
  ```bash
133
- /plugin marketplace add plosson/agentio
134
- ```
200
+ # Install from GitHub
201
+ agentio claude plugin install plosson/agentio
135
202
 
136
- ### Install the Plugin
203
+ # Or install from a full GitHub URL
204
+ agentio claude plugin install https://github.com/plosson/agentio
137
205
 
138
- ```bash
139
- /plugin install agentio@agentio
206
+ # Install to a specific directory
207
+ agentio claude plugin install plosson/agentio -d ~/myproject
208
+
209
+ # Install only skills (skip commands and hooks)
210
+ agentio claude plugin install plosson/agentio --skills
211
+
212
+ # Force reinstall if already exists
213
+ agentio claude plugin install plosson/agentio -f
140
214
  ```
141
215
 
142
216
  Once installed, Claude Code can use the agentio CLI skills to help you manage emails, send Telegram messages, and more.
143
217
 
144
- ### Install Skills Directly
145
-
146
- You can also install skills directly without the plugin system:
218
+ ### Manage Plugins
147
219
 
148
220
  ```bash
149
- # List available skills
150
- agentio skill list
221
+ # List installed plugins
222
+ agentio claude plugin list
151
223
 
152
- # Install all skills to current directory
153
- agentio skill install
224
+ # Remove a plugin
225
+ agentio claude plugin remove agentio
226
+ ```
154
227
 
155
- # Install a specific skill
156
- agentio skill install agentio-gmail
228
+ ### Install from agentio.json
157
229
 
158
- # Install to a specific directory
159
- agentio skill install -d ~/myproject
230
+ If your project has an `agentio.json` file listing plugins, you can install all of them at once:
160
231
 
161
- # Skip confirmation prompts
162
- agentio skill install -y
232
+ ```bash
233
+ # Install all plugins from agentio.json in current directory
234
+ agentio claude plugin install
163
235
  ```
164
236
 
165
- Skills are installed to `.claude/skills/` in the target directory.
237
+ Plugins are installed to `.claude/` in the target directory (skills, commands, and hooks subdirectories).
166
238
 
167
239
  ## Design
168
240
 
@@ -180,6 +252,27 @@ Configuration is stored in `~/.config/agentio/`:
180
252
  - `config.json` - Profile names and defaults
181
253
  - `tokens.enc` - Encrypted credentials (AES-256-GCM)
182
254
 
255
+ ### Export/Import
256
+
257
+ Transfer configuration between machines:
258
+
259
+ ```bash
260
+ # Export configuration (generates encryption key)
261
+ agentio config export
262
+
263
+ # Export with custom output file
264
+ agentio config export --output backup.config
265
+
266
+ # Import on another machine
267
+ agentio config import agentio.config --key <encryption-key>
268
+
269
+ # Or use environment variable
270
+ AGENTIO_KEY=<key> agentio config import agentio.config
271
+
272
+ # Merge with existing config instead of replacing
273
+ agentio config import agentio.config --key <key> --merge
274
+ ```
275
+
183
276
  ## License
184
277
 
185
278
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plosson/agentio",
3
- "version": "0.1.18",
3
+ "version": "0.1.22",
4
4
  "description": "CLI for LLM agents to interact with communication and tracking services",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,185 @@
1
+ import { Command } from 'commander';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ import { CliError, handleError } from '../utils/errors';
5
+ import {
6
+ loadAgentioJson,
7
+ agentioJsonExists,
8
+ listPlugins,
9
+ removePlugin,
10
+ getPlugin,
11
+ } from '../services/claude-plugin/agentio-json';
12
+ import {
13
+ installPlugin,
14
+ removePluginFiles,
15
+ } from '../services/claude-plugin/installer';
16
+ import type { InstalledComponent } from '../types/claude-plugin';
17
+
18
+ export function registerClaudeCommands(program: Command): void {
19
+ const claude = program
20
+ .command('claude')
21
+ .description('Claude Code plugin operations');
22
+
23
+ const plugin = claude.command('plugin').description('Manage Claude Code plugins');
24
+
25
+ plugin
26
+ .command('install')
27
+ .description('Install plugin(s) from GitHub or agentio.json')
28
+ .argument('[source]', 'GitHub URL or owner/repo (omit to install from agentio.json)')
29
+ .option('--skills', 'Install only skills')
30
+ .option('--commands', 'Install only commands')
31
+ .option('--hooks', 'Install only hooks')
32
+ .option('--agents', 'Install only agents')
33
+ .option('-f, --force', 'Force reinstall if already exists')
34
+ .option('-d, --dir <path>', 'Target directory (default: current directory)')
35
+ .option('-v, --verbose', 'Show detailed installation logs')
36
+ .action(async (source, options) => {
37
+ try {
38
+ const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
39
+
40
+ if (!fs.existsSync(targetDir)) {
41
+ throw new CliError('INVALID_PARAMS', `Directory does not exist: ${targetDir}`);
42
+ }
43
+
44
+ if (source) {
45
+ // Install specific plugin from source
46
+ console.error(`Installing plugin from: ${source}`);
47
+ console.error(`Target: ${path.join(targetDir, '.claude')}`);
48
+
49
+ const result = await installPlugin(source, {
50
+ skills: options.skills,
51
+ commands: options.commands,
52
+ hooks: options.hooks,
53
+ agents: options.agents,
54
+ force: options.force,
55
+ targetDir,
56
+ verbose: options.verbose,
57
+ });
58
+
59
+ console.log(`\nInstalled: ${result.manifest.name} v${result.manifest.version}`);
60
+ if (result.installed.length > 0) {
61
+ console.log(`Components: ${result.installed.length}`);
62
+ for (const comp of result.installed) {
63
+ console.log(` ${comp.type}/${comp.name}`);
64
+ }
65
+ }
66
+ } else {
67
+ // Install all plugins from agentio.json
68
+ if (!agentioJsonExists(targetDir)) {
69
+ throw new CliError(
70
+ 'NOT_FOUND',
71
+ 'No agentio.json found',
72
+ 'Run: agentio claude plugin install <source> to install a plugin'
73
+ );
74
+ }
75
+
76
+ const agentioJson = loadAgentioJson(targetDir);
77
+ const plugins = Object.entries(agentioJson.plugins);
78
+
79
+ if (plugins.length === 0) {
80
+ console.log('No plugins defined in agentio.json');
81
+ return;
82
+ }
83
+
84
+ console.error(`Installing ${plugins.length} plugin(s) from agentio.json...`);
85
+ console.error(`Target: ${path.join(targetDir, '.claude')}`);
86
+
87
+ let installed = 0;
88
+ for (const [name, entry] of plugins) {
89
+ console.error(`\nInstalling ${name}...`);
90
+
91
+ // Determine component flags based on entry.components
92
+ const installOptions = {
93
+ skills:
94
+ !entry.components || entry.components.includes('skills'),
95
+ commands:
96
+ !entry.components || entry.components.includes('commands'),
97
+ hooks: !entry.components || entry.components.includes('hooks'),
98
+ agents: !entry.components || entry.components.includes('agents'),
99
+ force: options.force,
100
+ targetDir,
101
+ verbose: options.verbose,
102
+ };
103
+
104
+ try {
105
+ const result = await installPlugin(entry.source, installOptions);
106
+ console.log(` Installed: ${result.manifest.name} v${result.manifest.version}`);
107
+ installed++;
108
+ } catch (error) {
109
+ console.error(` Failed to install ${name}: ${error}`);
110
+ }
111
+ }
112
+
113
+ console.log(`\nInstalled ${installed} of ${plugins.length} plugin(s)`);
114
+ }
115
+ } catch (error) {
116
+ handleError(error);
117
+ }
118
+ });
119
+
120
+ plugin
121
+ .command('list')
122
+ .description('List plugins from agentio.json')
123
+ .option('-d, --dir <path>', 'Directory with agentio.json (default: current directory)')
124
+ .action(async (options) => {
125
+ try {
126
+ const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
127
+
128
+ const plugins = listPlugins(targetDir);
129
+
130
+ if (plugins.length === 0) {
131
+ console.log('No plugins in agentio.json');
132
+ return;
133
+ }
134
+
135
+ console.log(`Plugins (${plugins.length}):\n`);
136
+ for (const { name, entry } of plugins) {
137
+ console.log(`${name} v${entry.version}`);
138
+ console.log(` Source: ${entry.source}`);
139
+ if (entry.components) {
140
+ console.log(` Components: ${entry.components.join(', ')}`);
141
+ }
142
+ console.log('');
143
+ }
144
+ } catch (error) {
145
+ handleError(error);
146
+ }
147
+ });
148
+
149
+ plugin
150
+ .command('remove')
151
+ .description('Remove an installed plugin')
152
+ .argument('<name>', 'Plugin name')
153
+ .option('-d, --dir <path>', 'Directory with agentio.json (default: current directory)')
154
+ .action(async (name, options) => {
155
+ try {
156
+ const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
157
+
158
+ const entry = getPlugin(targetDir, name);
159
+ if (!entry) {
160
+ throw new CliError('NOT_FOUND', `Plugin '${name}' not found in agentio.json`);
161
+ }
162
+
163
+ console.error(`Removing plugin: ${name}...`);
164
+
165
+ // Use stored installed components (no network calls needed)
166
+ const components: InstalledComponent[] = entry.installedComponents || [];
167
+
168
+ // Remove files
169
+ removePluginFiles(targetDir, components);
170
+
171
+ // Update agentio.json
172
+ removePlugin(targetDir, name);
173
+
174
+ console.log(`Removed: ${name}`);
175
+ if (components.length > 0) {
176
+ console.log(`Removed components: ${components.length}`);
177
+ for (const comp of components) {
178
+ console.log(` ${comp.type}/${comp.name}`);
179
+ }
180
+ }
181
+ } catch (error) {
182
+ handleError(error);
183
+ }
184
+ });
185
+ }
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ import { registerJiraCommands } from './commands/jira';
7
7
  import { registerSlackCommands } from './commands/slack';
8
8
  import { registerUpdateCommand } from './commands/update';
9
9
  import { registerConfigCommands } from './commands/config';
10
- import { registerSkillCommands } from './commands/skill';
10
+ import { registerClaudeCommands } from './commands/claude';
11
11
 
12
12
  declare const BUILD_VERSION: string | undefined;
13
13
 
@@ -33,6 +33,6 @@ registerJiraCommands(program);
33
33
  registerSlackCommands(program);
34
34
  registerUpdateCommand(program);
35
35
  registerConfigCommands(program);
36
- registerSkillCommands(program);
36
+ registerClaudeCommands(program);
37
37
 
38
38
  program.parse();
@@ -0,0 +1,103 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import type { AgentioJson, AgentioPluginEntry } from '../../types/claude-plugin';
4
+
5
+ const AGENTIO_JSON_FILE = 'agentio.json';
6
+
7
+ /**
8
+ * Get the path to agentio.json in the given directory.
9
+ */
10
+ function getAgentioJsonPath(dir: string): string {
11
+ return path.join(dir, AGENTIO_JSON_FILE);
12
+ }
13
+
14
+ /**
15
+ * Load agentio.json from the given directory.
16
+ * Returns empty structure if file doesn't exist.
17
+ */
18
+ export function loadAgentioJson(dir: string): AgentioJson {
19
+ const filePath = getAgentioJsonPath(dir);
20
+
21
+ if (!fs.existsSync(filePath)) {
22
+ return { plugins: {} };
23
+ }
24
+
25
+ const content = fs.readFileSync(filePath, 'utf-8');
26
+ const data = JSON.parse(content) as AgentioJson;
27
+
28
+ // Ensure plugins object exists
29
+ if (!data.plugins) {
30
+ data.plugins = {};
31
+ }
32
+
33
+ return data;
34
+ }
35
+
36
+ /**
37
+ * Save agentio.json to the given directory.
38
+ */
39
+ export function saveAgentioJson(dir: string, data: AgentioJson): void {
40
+ const filePath = getAgentioJsonPath(dir);
41
+ const content = JSON.stringify(data, null, 2) + '\n';
42
+ fs.writeFileSync(filePath, content);
43
+ }
44
+
45
+ /**
46
+ * Check if agentio.json exists in the given directory.
47
+ */
48
+ export function agentioJsonExists(dir: string): boolean {
49
+ return fs.existsSync(getAgentioJsonPath(dir));
50
+ }
51
+
52
+ /**
53
+ * Add or update a plugin entry in agentio.json.
54
+ */
55
+ export function addPlugin(
56
+ dir: string,
57
+ name: string,
58
+ entry: AgentioPluginEntry
59
+ ): void {
60
+ const data = loadAgentioJson(dir);
61
+ data.plugins[name] = entry;
62
+ saveAgentioJson(dir, data);
63
+ }
64
+
65
+ /**
66
+ * Remove a plugin entry from agentio.json.
67
+ * Returns true if plugin was found and removed.
68
+ */
69
+ export function removePlugin(dir: string, name: string): boolean {
70
+ const data = loadAgentioJson(dir);
71
+
72
+ if (!data.plugins[name]) {
73
+ return false;
74
+ }
75
+
76
+ delete data.plugins[name];
77
+ saveAgentioJson(dir, data);
78
+ return true;
79
+ }
80
+
81
+ /**
82
+ * Get a plugin entry from agentio.json.
83
+ */
84
+ export function getPlugin(
85
+ dir: string,
86
+ name: string
87
+ ): AgentioPluginEntry | undefined {
88
+ const data = loadAgentioJson(dir);
89
+ return data.plugins[name];
90
+ }
91
+
92
+ /**
93
+ * List all plugins in agentio.json.
94
+ */
95
+ export function listPlugins(
96
+ dir: string
97
+ ): Array<{ name: string; entry: AgentioPluginEntry }> {
98
+ const data = loadAgentioJson(dir);
99
+ return Object.entries(data.plugins).map(([name, entry]) => ({
100
+ name,
101
+ entry,
102
+ }));
103
+ }
@@ -0,0 +1,410 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import { tmpdir } from 'os';
5
+ import { CliError } from '../../utils/errors';
6
+ import type {
7
+ ParsedSource,
8
+ PluginManifest,
9
+ DiscoveredComponents,
10
+ PluginInstallOptions,
11
+ InstalledComponent,
12
+ InstallResult,
13
+ ComponentType,
14
+ } from '../../types/claude-plugin';
15
+ import { parseSource, buildGitCloneUrl } from './source-parser';
16
+ import { addPlugin } from './agentio-json';
17
+
18
+ /**
19
+ * Clone a repository to a temporary directory.
20
+ */
21
+ function cloneRepo(parsed: ParsedSource): string {
22
+ const tempDir = fs.mkdtempSync(path.join(tmpdir(), 'agentio-plugin-'));
23
+ const cloneUrl = buildGitCloneUrl(parsed);
24
+
25
+ let cmd = `git clone --depth 1`;
26
+ if (parsed.branch) {
27
+ cmd += ` -b ${parsed.branch}`;
28
+ }
29
+ cmd += ` "${cloneUrl}" "${tempDir}"`;
30
+
31
+ try {
32
+ execSync(cmd, { stdio: 'pipe' });
33
+ } catch {
34
+ // Clean up temp dir on clone failure
35
+ fs.rmSync(tempDir, { recursive: true, force: true });
36
+ throw new CliError(
37
+ 'API_ERROR',
38
+ `Failed to clone repository: ${parsed.owner}/${parsed.repo}`,
39
+ 'Check the repository URL and your network connection'
40
+ );
41
+ }
42
+
43
+ return tempDir;
44
+ }
45
+
46
+ /**
47
+ * Clean up a temporary directory.
48
+ */
49
+ function cleanupTempDir(tempDir: string): void {
50
+ fs.rmSync(tempDir, { recursive: true, force: true });
51
+ }
52
+
53
+ /**
54
+ * Read the plugin manifest from a cloned repository.
55
+ */
56
+ function readPluginManifest(
57
+ repoDir: string,
58
+ parsed: ParsedSource
59
+ ): PluginManifest {
60
+ const basePath = parsed.path ? path.join(repoDir, parsed.path) : repoDir;
61
+ const manifestPath = path.join(basePath, '.claude-plugin', 'plugin.json');
62
+
63
+ if (!fs.existsSync(manifestPath)) {
64
+ throw new CliError(
65
+ 'NOT_FOUND',
66
+ 'Plugin manifest not found',
67
+ 'Ensure .claude-plugin/plugin.json exists in the plugin root'
68
+ );
69
+ }
70
+
71
+ try {
72
+ const content = fs.readFileSync(manifestPath, 'utf-8');
73
+ const manifest = JSON.parse(content) as PluginManifest;
74
+
75
+ if (!manifest.name || !manifest.version) {
76
+ throw new CliError(
77
+ 'INVALID_PARAMS',
78
+ 'Invalid plugin manifest: missing name or version'
79
+ );
80
+ }
81
+
82
+ return manifest;
83
+ } catch (error) {
84
+ if (error instanceof CliError) throw error;
85
+ if (error instanceof SyntaxError) {
86
+ throw new CliError('INVALID_PARAMS', 'Invalid plugin manifest JSON');
87
+ }
88
+ throw error;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Discover available components in a cloned repository.
94
+ */
95
+ function discoverComponents(
96
+ repoDir: string,
97
+ parsed: ParsedSource
98
+ ): DiscoveredComponents {
99
+ const basePath = parsed.path ? path.join(repoDir, parsed.path) : repoDir;
100
+ const result: DiscoveredComponents = {
101
+ skills: [],
102
+ commands: [],
103
+ hooks: [],
104
+ agents: [],
105
+ };
106
+
107
+ const componentTypes: ComponentType[] = ['skills', 'commands', 'hooks', 'agents'];
108
+
109
+ for (const type of componentTypes) {
110
+ const typePath = path.join(basePath, type);
111
+ if (fs.existsSync(typePath)) {
112
+ const entries = fs.readdirSync(typePath, { withFileTypes: true });
113
+ result[type] = entries
114
+ .filter((e) => e.isDirectory())
115
+ .map((e) => e.name);
116
+ }
117
+ }
118
+
119
+ return result;
120
+ }
121
+
122
+ /**
123
+ * Copy a component from the cloned repository to the target directory.
124
+ */
125
+ function copyComponent(
126
+ repoDir: string,
127
+ parsed: ParsedSource,
128
+ componentType: ComponentType,
129
+ componentName: string,
130
+ targetDir: string
131
+ ): void {
132
+ const basePath = parsed.path ? path.join(repoDir, parsed.path) : repoDir;
133
+ const srcPath = path.join(basePath, componentType, componentName);
134
+ const destPath = path.join(targetDir, '.claude', componentType, componentName);
135
+
136
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
137
+ fs.cpSync(srcPath, destPath, { recursive: true });
138
+ }
139
+
140
+ /**
141
+ * Determine which components to install based on options.
142
+ */
143
+ function determineComponentsToInstall(
144
+ options: PluginInstallOptions,
145
+ discovered: DiscoveredComponents
146
+ ): DiscoveredComponents {
147
+ // If no specific flags, install all
148
+ const installAll = !options.skills && !options.commands && !options.hooks && !options.agents;
149
+
150
+ return {
151
+ skills: installAll || options.skills ? discovered.skills : [],
152
+ commands: installAll || options.commands ? discovered.commands : [],
153
+ hooks: installAll || options.hooks ? discovered.hooks : [],
154
+ agents: installAll || options.agents ? discovered.agents : [],
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Get the component types array for agentio.json based on what was installed.
160
+ */
161
+ function getInstalledComponentTypes(
162
+ options: PluginInstallOptions
163
+ ): ComponentType[] | undefined {
164
+ const installAll = !options.skills && !options.commands && !options.hooks && !options.agents;
165
+ if (installAll) return undefined; // Default: all
166
+
167
+ const types: ComponentType[] = [];
168
+ if (options.skills) types.push('skills');
169
+ if (options.commands) types.push('commands');
170
+ if (options.hooks) types.push('hooks');
171
+ if (options.agents) types.push('agents');
172
+ return types;
173
+ }
174
+
175
+ /**
176
+ * Install a plugin from a source.
177
+ */
178
+ export async function installPlugin(
179
+ source: string,
180
+ options: PluginInstallOptions
181
+ ): Promise<InstallResult> {
182
+ const parsed = parseSource(source);
183
+ const verbose = options.verbose ?? false;
184
+
185
+ if (verbose) {
186
+ console.error(`\n[verbose] Parsed source:`);
187
+ console.error(` Owner: ${parsed.owner}`);
188
+ console.error(` Repo: ${parsed.repo}`);
189
+ console.error(` Branch: ${parsed.branch ?? '(default)'}`);
190
+ console.error(` Path: ${parsed.path ?? '(root)'}`);
191
+ console.error(` Clone URL: ${buildGitCloneUrl(parsed)}`);
192
+ }
193
+
194
+ // Clone repo to temp directory
195
+ if (verbose) {
196
+ console.error(`\n[verbose] Cloning repository...`);
197
+ }
198
+ const repoDir = cloneRepo(parsed);
199
+ if (verbose) {
200
+ console.error(`[verbose] Cloned to: ${repoDir}`);
201
+ }
202
+
203
+ try {
204
+ // Read manifest from cloned repo
205
+ const manifest = readPluginManifest(repoDir, parsed);
206
+
207
+ if (verbose) {
208
+ console.error(`\n[verbose] Plugin manifest:`);
209
+ console.error(` Name: ${manifest.name}`);
210
+ console.error(` Version: ${manifest.version}`);
211
+ if (manifest.description) {
212
+ console.error(` Description: ${manifest.description}`);
213
+ }
214
+ }
215
+
216
+ // Discover available components
217
+ const discovered = discoverComponents(repoDir, parsed);
218
+
219
+ if (verbose) {
220
+ const totalDiscovered = discovered.skills.length + discovered.commands.length + discovered.hooks.length + discovered.agents.length;
221
+ console.error(`\n[verbose] Discovered ${totalDiscovered} component(s):`);
222
+ if (discovered.skills.length > 0) {
223
+ console.error(` Skills (${discovered.skills.length}): ${discovered.skills.join(', ')}`);
224
+ }
225
+ if (discovered.commands.length > 0) {
226
+ console.error(` Commands (${discovered.commands.length}): ${discovered.commands.join(', ')}`);
227
+ }
228
+ if (discovered.hooks.length > 0) {
229
+ console.error(` Hooks (${discovered.hooks.length}): ${discovered.hooks.join(', ')}`);
230
+ }
231
+ if (discovered.agents.length > 0) {
232
+ console.error(` Agents (${discovered.agents.length}): ${discovered.agents.join(', ')}`);
233
+ }
234
+ }
235
+
236
+ // Determine what to install
237
+ const toInstall = determineComponentsToInstall(options, discovered);
238
+
239
+ if (verbose) {
240
+ const totalToInstall = toInstall.skills.length + toInstall.commands.length + toInstall.hooks.length + toInstall.agents.length;
241
+ console.error(`\n[verbose] Installing ${totalToInstall} component(s):`);
242
+ if (toInstall.skills.length > 0) {
243
+ console.error(` Skills: ${toInstall.skills.join(', ')}`);
244
+ }
245
+ if (toInstall.commands.length > 0) {
246
+ console.error(` Commands: ${toInstall.commands.join(', ')}`);
247
+ }
248
+ if (toInstall.hooks.length > 0) {
249
+ console.error(` Hooks: ${toInstall.hooks.join(', ')}`);
250
+ }
251
+ if (toInstall.agents.length > 0) {
252
+ console.error(` Agents: ${toInstall.agents.join(', ')}`);
253
+ }
254
+ }
255
+
256
+ const targetDir = options.targetDir || process.cwd();
257
+ const installed: InstalledComponent[] = [];
258
+
259
+ if (verbose) {
260
+ console.error(`\n[verbose] Target directory: ${targetDir}`);
261
+ }
262
+
263
+ // Install skills
264
+ for (const skillName of toInstall.skills) {
265
+ const destPath = path.join(targetDir, '.claude', 'skills', skillName);
266
+
267
+ if (fs.existsSync(destPath)) {
268
+ if (!options.force) {
269
+ console.error(` Skipping existing skill: ${skillName}`);
270
+ if (verbose) {
271
+ console.error(` [verbose] Path: ${destPath}`);
272
+ }
273
+ continue;
274
+ }
275
+ if (verbose) {
276
+ console.error(` [verbose] Removing existing: ${destPath}`);
277
+ }
278
+ fs.rmSync(destPath, { recursive: true });
279
+ }
280
+
281
+ copyComponent(repoDir, parsed, 'skills', skillName, targetDir);
282
+ installed.push({ name: skillName, type: 'skills', path: destPath });
283
+ console.error(` Installed skill: ${skillName}`);
284
+ if (verbose) {
285
+ console.error(` [verbose] Path: ${destPath}`);
286
+ }
287
+ }
288
+
289
+ // Install commands
290
+ for (const cmdName of toInstall.commands) {
291
+ const destPath = path.join(targetDir, '.claude', 'commands', cmdName);
292
+
293
+ if (fs.existsSync(destPath)) {
294
+ if (!options.force) {
295
+ console.error(` Skipping existing command: ${cmdName}`);
296
+ if (verbose) {
297
+ console.error(` [verbose] Path: ${destPath}`);
298
+ }
299
+ continue;
300
+ }
301
+ if (verbose) {
302
+ console.error(` [verbose] Removing existing: ${destPath}`);
303
+ }
304
+ fs.rmSync(destPath, { recursive: true });
305
+ }
306
+
307
+ copyComponent(repoDir, parsed, 'commands', cmdName, targetDir);
308
+ installed.push({ name: cmdName, type: 'commands', path: destPath });
309
+ console.error(` Installed command: ${cmdName}`);
310
+ if (verbose) {
311
+ console.error(` [verbose] Path: ${destPath}`);
312
+ }
313
+ }
314
+
315
+ // Install hooks
316
+ for (const hookName of toInstall.hooks) {
317
+ const destPath = path.join(targetDir, '.claude', 'hooks', hookName);
318
+
319
+ if (fs.existsSync(destPath)) {
320
+ if (!options.force) {
321
+ console.error(` Skipping existing hook: ${hookName}`);
322
+ if (verbose) {
323
+ console.error(` [verbose] Path: ${destPath}`);
324
+ }
325
+ continue;
326
+ }
327
+ if (verbose) {
328
+ console.error(` [verbose] Removing existing: ${destPath}`);
329
+ }
330
+ fs.rmSync(destPath, { recursive: true });
331
+ }
332
+
333
+ copyComponent(repoDir, parsed, 'hooks', hookName, targetDir);
334
+ installed.push({ name: hookName, type: 'hooks', path: destPath });
335
+ console.error(` Installed hook: ${hookName}`);
336
+ if (verbose) {
337
+ console.error(` [verbose] Path: ${destPath}`);
338
+ }
339
+ }
340
+
341
+ // Install agents
342
+ for (const agentName of toInstall.agents) {
343
+ const destPath = path.join(targetDir, '.claude', 'agents', agentName);
344
+
345
+ if (fs.existsSync(destPath)) {
346
+ if (!options.force) {
347
+ console.error(` Skipping existing agent: ${agentName}`);
348
+ if (verbose) {
349
+ console.error(` [verbose] Path: ${destPath}`);
350
+ }
351
+ continue;
352
+ }
353
+ if (verbose) {
354
+ console.error(` [verbose] Removing existing: ${destPath}`);
355
+ }
356
+ fs.rmSync(destPath, { recursive: true });
357
+ }
358
+
359
+ copyComponent(repoDir, parsed, 'agents', agentName, targetDir);
360
+ installed.push({ name: agentName, type: 'agents', path: destPath });
361
+ console.error(` Installed agent: ${agentName}`);
362
+ if (verbose) {
363
+ console.error(` [verbose] Path: ${destPath}`);
364
+ }
365
+ }
366
+
367
+ // Update agentio.json
368
+ if (verbose) {
369
+ console.error(`\n[verbose] Updating agentio.json`);
370
+ }
371
+ addPlugin(targetDir, manifest.name, {
372
+ source: source,
373
+ version: manifest.version,
374
+ components: getInstalledComponentTypes(options),
375
+ installedComponents: installed,
376
+ });
377
+
378
+ return {
379
+ success: true,
380
+ manifest,
381
+ installed,
382
+ };
383
+ } finally {
384
+ // Always cleanup temp directory
385
+ if (verbose) {
386
+ console.error(`\n[verbose] Cleaning up temp directory: ${repoDir}`);
387
+ }
388
+ cleanupTempDir(repoDir);
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Remove installed components for a plugin.
394
+ */
395
+ export function removePluginFiles(
396
+ targetDir: string,
397
+ components: InstalledComponent[]
398
+ ): void {
399
+ for (const comp of components) {
400
+ const compPath = path.join(
401
+ targetDir,
402
+ '.claude',
403
+ comp.type,
404
+ comp.name
405
+ );
406
+ if (fs.existsSync(compPath)) {
407
+ fs.rmSync(compPath, { recursive: true });
408
+ }
409
+ }
410
+ }
@@ -0,0 +1,114 @@
1
+ import { CliError } from '../../utils/errors';
2
+ import type { ParsedSource } from '../../types/claude-plugin';
3
+
4
+ /**
5
+ * Parse various GitHub source formats into a normalized ParsedSource.
6
+ *
7
+ * Supported formats:
8
+ * - https://github.com/owner/repo/tree/branch/path
9
+ * - git@github.com:owner/repo.git
10
+ * - https://github.com/owner/repo.git
11
+ * - owner/repo
12
+ * - owner/repo/path/to/plugin
13
+ */
14
+ export function parseSource(source: string): ParsedSource {
15
+ // Full GitHub tree URL: https://github.com/owner/repo/tree/branch/path
16
+ if (source.includes('github.com') && source.includes('/tree/')) {
17
+ return parseGitHubTreeUrl(source);
18
+ }
19
+
20
+ // SSH URL: git@github.com:owner/repo.git
21
+ if (source.startsWith('git@')) {
22
+ return parseSshUrl(source);
23
+ }
24
+
25
+ // HTTPS clone URL: https://github.com/owner/repo.git
26
+ if (source.includes('github.com') && source.endsWith('.git')) {
27
+ return parseHttpsCloneUrl(source);
28
+ }
29
+
30
+ // Plain GitHub URL without tree: https://github.com/owner/repo or https://github.com/owner/repo/path
31
+ if (source.includes('github.com')) {
32
+ return parseGitHubUrl(source);
33
+ }
34
+
35
+ // Short form: owner/repo or owner/repo/path
36
+ return parseShortForm(source);
37
+ }
38
+
39
+ function parseGitHubTreeUrl(url: string): ParsedSource {
40
+ // https://github.com/owner/repo/tree/branch/path/to/plugin
41
+ const match = url.match(
42
+ /github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)(?:\/(.+))?/
43
+ );
44
+ if (!match) {
45
+ throw new CliError('INVALID_PARAMS', `Invalid GitHub tree URL: ${url}`);
46
+ }
47
+ return {
48
+ owner: match[1],
49
+ repo: match[2],
50
+ branch: match[3],
51
+ path: match[4] || undefined,
52
+ };
53
+ }
54
+
55
+ function parseSshUrl(url: string): ParsedSource {
56
+ // git@github.com:owner/repo.git
57
+ const match = url.match(/git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
58
+ if (!match) {
59
+ throw new CliError('INVALID_PARAMS', `Invalid SSH URL: ${url}`);
60
+ }
61
+ return {
62
+ owner: match[1],
63
+ repo: match[2],
64
+ };
65
+ }
66
+
67
+ function parseHttpsCloneUrl(url: string): ParsedSource {
68
+ // https://github.com/owner/repo.git
69
+ const match = url.match(/github\.com\/([^/]+)\/([^/]+?)\.git$/);
70
+ if (!match) {
71
+ throw new CliError('INVALID_PARAMS', `Invalid HTTPS clone URL: ${url}`);
72
+ }
73
+ return {
74
+ owner: match[1],
75
+ repo: match[2],
76
+ };
77
+ }
78
+
79
+ function parseGitHubUrl(url: string): ParsedSource {
80
+ // https://github.com/owner/repo or https://github.com/owner/repo/path
81
+ const match = url.match(/github\.com\/([^/]+)\/([^/]+?)(?:\/(.+))?$/);
82
+ if (!match) {
83
+ throw new CliError('INVALID_PARAMS', `Invalid GitHub URL: ${url}`);
84
+ }
85
+ return {
86
+ owner: match[1],
87
+ repo: match[2],
88
+ path: match[3] || undefined,
89
+ };
90
+ }
91
+
92
+ function parseShortForm(source: string): ParsedSource {
93
+ // owner/repo or owner/repo/path/to/plugin
94
+ const parts = source.split('/');
95
+ if (parts.length < 2) {
96
+ throw new CliError(
97
+ 'INVALID_PARAMS',
98
+ `Invalid source format: ${source}`,
99
+ 'Use owner/repo or a GitHub URL'
100
+ );
101
+ }
102
+ return {
103
+ owner: parts[0],
104
+ repo: parts[1],
105
+ path: parts.length > 2 ? parts.slice(2).join('/') : undefined,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Build a git clone URL from parsed source.
111
+ */
112
+ export function buildGitCloneUrl(parsed: ParsedSource): string {
113
+ return `https://github.com/${parsed.owner}/${parsed.repo}.git`;
114
+ }
@@ -0,0 +1,65 @@
1
+ // Source URL parsing result
2
+ export interface ParsedSource {
3
+ owner: string;
4
+ repo: string;
5
+ branch?: string; // undefined = default branch
6
+ path?: string; // path within repo to plugin root
7
+ }
8
+
9
+ // Plugin manifest (.claude-plugin/plugin.json)
10
+ export interface PluginManifest {
11
+ name: string;
12
+ version: string;
13
+ description?: string;
14
+ }
15
+
16
+ // Component types that can be installed
17
+ export type ComponentType = 'skills' | 'commands' | 'hooks' | 'agents';
18
+
19
+ // Discovered components from filesystem
20
+ export interface DiscoveredComponents {
21
+ skills: string[];
22
+ commands: string[];
23
+ hooks: string[];
24
+ agents: string[];
25
+ }
26
+
27
+ // Installation options
28
+ export interface PluginInstallOptions {
29
+ skills?: boolean;
30
+ commands?: boolean;
31
+ hooks?: boolean;
32
+ agents?: boolean;
33
+ force?: boolean;
34
+ targetDir?: string;
35
+ verbose?: boolean;
36
+ }
37
+
38
+ // Single installed component record
39
+ export interface InstalledComponent {
40
+ name: string;
41
+ type: ComponentType;
42
+ path: string;
43
+ }
44
+
45
+ // Plugin entry in agentio.json
46
+ export interface AgentioPluginEntry {
47
+ source: string;
48
+ version: string;
49
+ components?: ComponentType[];
50
+ installedComponents: InstalledComponent[];
51
+ }
52
+
53
+ // agentio.json structure
54
+ export interface AgentioJson {
55
+ plugins: {
56
+ [pluginName: string]: AgentioPluginEntry;
57
+ };
58
+ }
59
+
60
+ // Installation result
61
+ export interface InstallResult {
62
+ success: boolean;
63
+ manifest: PluginManifest;
64
+ installed: InstalledComponent[];
65
+ }
@@ -1,204 +0,0 @@
1
- import { Command } from 'commander';
2
- import { createInterface } from 'readline';
3
- import { CliError, handleError } from '../utils/errors';
4
- import * as fs from 'fs';
5
- import * as path from 'path';
6
-
7
- const GITHUB_REPO = 'plosson/agentio';
8
- const SKILLS_PATH = 'claude/skills';
9
-
10
- interface GitHubContent {
11
- name: string;
12
- path: string;
13
- type: 'file' | 'dir';
14
- download_url: string | null;
15
- }
16
-
17
- function prompt(question: string): Promise<string> {
18
- const rl = createInterface({
19
- input: process.stdin,
20
- output: process.stderr,
21
- });
22
-
23
- return new Promise((resolve) => {
24
- rl.question(question, (answer) => {
25
- rl.close();
26
- resolve(answer.trim());
27
- });
28
- });
29
- }
30
-
31
- async function fetchGitHubContents(repoPath: string): Promise<GitHubContent[]> {
32
- const url = `https://api.github.com/repos/${GITHUB_REPO}/contents/${repoPath}`;
33
- const response = await fetch(url, {
34
- headers: {
35
- 'Accept': 'application/vnd.github.v3+json',
36
- 'User-Agent': 'agentio-skill-manager',
37
- },
38
- });
39
-
40
- if (!response.ok) {
41
- if (response.status === 404) {
42
- throw new CliError('NOT_FOUND', `Path not found: ${repoPath}`);
43
- }
44
- throw new CliError('API_ERROR', `GitHub API error: ${response.statusText}`);
45
- }
46
-
47
- return response.json();
48
- }
49
-
50
- async function fetchFileContent(downloadUrl: string): Promise<string> {
51
- const response = await fetch(downloadUrl, {
52
- headers: {
53
- 'User-Agent': 'agentio-skill-manager',
54
- },
55
- });
56
-
57
- if (!response.ok) {
58
- throw new CliError('API_ERROR', `Failed to download file: ${response.statusText}`);
59
- }
60
-
61
- return response.text();
62
- }
63
-
64
- async function listAvailableSkills(): Promise<string[]> {
65
- const contents = await fetchGitHubContents(SKILLS_PATH);
66
- return contents
67
- .filter((item) => item.type === 'dir')
68
- .map((item) => item.name);
69
- }
70
-
71
- async function downloadSkillFolder(
72
- skillName: string,
73
- targetDir: string
74
- ): Promise<void> {
75
- const skillPath = `${SKILLS_PATH}/${skillName}`;
76
- const contents = await fetchGitHubContents(skillPath);
77
-
78
- // Create target directory
79
- fs.mkdirSync(targetDir, { recursive: true });
80
-
81
- for (const item of contents) {
82
- const targetPath = path.join(targetDir, item.name);
83
-
84
- if (item.type === 'file' && item.download_url) {
85
- const content = await fetchFileContent(item.download_url);
86
- fs.writeFileSync(targetPath, content);
87
- } else if (item.type === 'dir') {
88
- // Recursively download subdirectories
89
- await downloadSkillFolder(`${skillName}/${item.name}`, targetPath);
90
- }
91
- }
92
- }
93
-
94
- async function installSkill(
95
- skillName: string,
96
- baseDir: string,
97
- skipPrompt: boolean
98
- ): Promise<boolean> {
99
- const targetDir = path.join(baseDir, '.claude', 'skills', skillName);
100
-
101
- // Check if skill already exists
102
- if (fs.existsSync(targetDir)) {
103
- if (!skipPrompt) {
104
- const answer = await prompt(
105
- `Skill '${skillName}' already exists at ${targetDir}. Update? [y/N] `
106
- );
107
- if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
108
- console.error(`Skipping '${skillName}'`);
109
- return false;
110
- }
111
- }
112
- // Remove existing skill directory before updating
113
- fs.rmSync(targetDir, { recursive: true });
114
- }
115
-
116
- console.error(`Installing skill: ${skillName}...`);
117
- await downloadSkillFolder(skillName, targetDir);
118
- console.log(`Installed: ${skillName} -> ${targetDir}`);
119
- return true;
120
- }
121
-
122
- export function registerSkillCommands(program: Command): void {
123
- const skill = program
124
- .command('skill')
125
- .description('Manage Claude Code skills');
126
-
127
- skill
128
- .command('list')
129
- .description('List available skills from the repository')
130
- .action(async () => {
131
- try {
132
- console.error('Fetching available skills...');
133
- const skills = await listAvailableSkills();
134
-
135
- if (skills.length === 0) {
136
- console.log('No skills found in repository');
137
- return;
138
- }
139
-
140
- console.log('Available skills:');
141
- for (const name of skills) {
142
- console.log(` ${name}`);
143
- }
144
- } catch (error) {
145
- handleError(error);
146
- }
147
- });
148
-
149
- skill
150
- .command('install')
151
- .description('Install skills from the repository')
152
- .argument('[skill-name]', 'Name of the skill to install (omit to install all)')
153
- .option('-d, --dir <path>', 'Target directory (default: current directory)')
154
- .option('-y, --yes', 'Skip confirmation prompts')
155
- .action(async (skillName, options) => {
156
- try {
157
- const baseDir = options.dir ? path.resolve(options.dir) : process.cwd();
158
-
159
- // Verify base directory exists
160
- if (!fs.existsSync(baseDir)) {
161
- throw new CliError(
162
- 'INVALID_PARAMS',
163
- `Directory does not exist: ${baseDir}`
164
- );
165
- }
166
-
167
- console.error(`Target: ${path.join(baseDir, '.claude', 'skills')}`);
168
-
169
- if (skillName) {
170
- // Install specific skill
171
- const skills = await listAvailableSkills();
172
- if (!skills.includes(skillName)) {
173
- throw new CliError(
174
- 'NOT_FOUND',
175
- `Skill '${skillName}' not found`,
176
- `Available skills: ${skills.join(', ')}`
177
- );
178
- }
179
- await installSkill(skillName, baseDir, options.yes);
180
- } else {
181
- // Install all skills
182
- console.error('Fetching available skills...');
183
- const skills = await listAvailableSkills();
184
-
185
- if (skills.length === 0) {
186
- console.log('No skills found in repository');
187
- return;
188
- }
189
-
190
- console.error(`Found ${skills.length} skill(s)`);
191
-
192
- let installed = 0;
193
- for (const name of skills) {
194
- const success = await installSkill(name, baseDir, options.yes);
195
- if (success) installed++;
196
- }
197
-
198
- console.log(`\nInstalled ${installed} of ${skills.length} skill(s)`);
199
- }
200
- } catch (error) {
201
- handleError(error);
202
- }
203
- });
204
- }