@plosson/agentio 0.1.17 → 0.1.20

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:
@@ -141,6 +208,29 @@ agentio provides a plugin for [Claude Code](https://claude.com/claude-code) with
141
208
 
142
209
  Once installed, Claude Code can use the agentio CLI skills to help you manage emails, send Telegram messages, and more.
143
210
 
211
+ ### Install Skills Directly
212
+
213
+ You can also install skills directly without the plugin system:
214
+
215
+ ```bash
216
+ # List available skills
217
+ agentio skill list
218
+
219
+ # Install all skills to current directory
220
+ agentio skill install
221
+
222
+ # Install a specific skill
223
+ agentio skill install agentio-gmail
224
+
225
+ # Install to a specific directory
226
+ agentio skill install -d ~/myproject
227
+
228
+ # Skip confirmation prompts
229
+ agentio skill install -y
230
+ ```
231
+
232
+ Skills are installed to `.claude/skills/` in the target directory.
233
+
144
234
  ## Design
145
235
 
146
236
  agentio is designed for LLM consumption:
@@ -157,6 +247,27 @@ Configuration is stored in `~/.config/agentio/`:
157
247
  - `config.json` - Profile names and defaults
158
248
  - `tokens.enc` - Encrypted credentials (AES-256-GCM)
159
249
 
250
+ ### Export/Import
251
+
252
+ Transfer configuration between machines:
253
+
254
+ ```bash
255
+ # Export configuration (generates encryption key)
256
+ agentio config export
257
+
258
+ # Export with custom output file
259
+ agentio config export --output backup.config
260
+
261
+ # Import on another machine
262
+ agentio config import agentio.config --key <encryption-key>
263
+
264
+ # Or use environment variable
265
+ AGENTIO_KEY=<key> agentio config import agentio.config
266
+
267
+ # Merge with existing config instead of replacing
268
+ agentio config import agentio.config --key <key> --merge
269
+ ```
270
+
160
271
  ## License
161
272
 
162
273
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plosson/agentio",
3
- "version": "0.1.17",
3
+ "version": "0.1.20",
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,179 @@
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('-f, --force', 'Force reinstall if already exists')
33
+ .option('-d, --dir <path>', 'Target directory (default: current directory)')
34
+ .action(async (source, options) => {
35
+ try {
36
+ const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
37
+
38
+ if (!fs.existsSync(targetDir)) {
39
+ throw new CliError('INVALID_PARAMS', `Directory does not exist: ${targetDir}`);
40
+ }
41
+
42
+ if (source) {
43
+ // Install specific plugin from source
44
+ console.error(`Installing plugin from: ${source}`);
45
+ console.error(`Target: ${path.join(targetDir, '.claude')}`);
46
+
47
+ const result = await installPlugin(source, {
48
+ skills: options.skills,
49
+ commands: options.commands,
50
+ hooks: options.hooks,
51
+ force: options.force,
52
+ targetDir,
53
+ });
54
+
55
+ console.log(`\nInstalled: ${result.manifest.name} v${result.manifest.version}`);
56
+ if (result.installed.length > 0) {
57
+ console.log(`Components: ${result.installed.length}`);
58
+ for (const comp of result.installed) {
59
+ console.log(` ${comp.type}/${comp.name}`);
60
+ }
61
+ }
62
+ } else {
63
+ // Install all plugins from agentio.json
64
+ if (!agentioJsonExists(targetDir)) {
65
+ throw new CliError(
66
+ 'NOT_FOUND',
67
+ 'No agentio.json found',
68
+ 'Run: agentio claude plugin install <source> to install a plugin'
69
+ );
70
+ }
71
+
72
+ const agentioJson = loadAgentioJson(targetDir);
73
+ const plugins = Object.entries(agentioJson.plugins);
74
+
75
+ if (plugins.length === 0) {
76
+ console.log('No plugins defined in agentio.json');
77
+ return;
78
+ }
79
+
80
+ console.error(`Installing ${plugins.length} plugin(s) from agentio.json...`);
81
+ console.error(`Target: ${path.join(targetDir, '.claude')}`);
82
+
83
+ let installed = 0;
84
+ for (const [name, entry] of plugins) {
85
+ console.error(`\nInstalling ${name}...`);
86
+
87
+ // Determine component flags based on entry.components
88
+ const installOptions = {
89
+ skills:
90
+ !entry.components || entry.components.includes('skills'),
91
+ commands:
92
+ !entry.components || entry.components.includes('commands'),
93
+ hooks: !entry.components || entry.components.includes('hooks'),
94
+ force: options.force,
95
+ targetDir,
96
+ };
97
+
98
+ try {
99
+ const result = await installPlugin(entry.source, installOptions);
100
+ console.log(` Installed: ${result.manifest.name} v${result.manifest.version}`);
101
+ installed++;
102
+ } catch (error) {
103
+ console.error(` Failed to install ${name}: ${error}`);
104
+ }
105
+ }
106
+
107
+ console.log(`\nInstalled ${installed} of ${plugins.length} plugin(s)`);
108
+ }
109
+ } catch (error) {
110
+ handleError(error);
111
+ }
112
+ });
113
+
114
+ plugin
115
+ .command('list')
116
+ .description('List plugins from agentio.json')
117
+ .option('-d, --dir <path>', 'Directory with agentio.json (default: current directory)')
118
+ .action(async (options) => {
119
+ try {
120
+ const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
121
+
122
+ const plugins = listPlugins(targetDir);
123
+
124
+ if (plugins.length === 0) {
125
+ console.log('No plugins in agentio.json');
126
+ return;
127
+ }
128
+
129
+ console.log(`Plugins (${plugins.length}):\n`);
130
+ for (const { name, entry } of plugins) {
131
+ console.log(`${name} v${entry.version}`);
132
+ console.log(` Source: ${entry.source}`);
133
+ if (entry.components) {
134
+ console.log(` Components: ${entry.components.join(', ')}`);
135
+ }
136
+ console.log('');
137
+ }
138
+ } catch (error) {
139
+ handleError(error);
140
+ }
141
+ });
142
+
143
+ plugin
144
+ .command('remove')
145
+ .description('Remove an installed plugin')
146
+ .argument('<name>', 'Plugin name')
147
+ .option('-d, --dir <path>', 'Directory with agentio.json (default: current directory)')
148
+ .action(async (name, options) => {
149
+ try {
150
+ const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
151
+
152
+ const entry = getPlugin(targetDir, name);
153
+ if (!entry) {
154
+ throw new CliError('NOT_FOUND', `Plugin '${name}' not found in agentio.json`);
155
+ }
156
+
157
+ console.error(`Removing plugin: ${name}...`);
158
+
159
+ // Use stored installed components (no network calls needed)
160
+ const components: InstalledComponent[] = entry.installedComponents || [];
161
+
162
+ // Remove files
163
+ removePluginFiles(targetDir, components);
164
+
165
+ // Update agentio.json
166
+ removePlugin(targetDir, name);
167
+
168
+ console.log(`Removed: ${name}`);
169
+ if (components.length > 0) {
170
+ console.log(`Removed components: ${components.length}`);
171
+ for (const comp of components) {
172
+ console.log(` ${comp.type}/${comp.name}`);
173
+ }
174
+ }
175
+ } catch (error) {
176
+ handleError(error);
177
+ }
178
+ });
179
+ }
package/src/index.ts CHANGED
@@ -7,6 +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 { registerClaudeCommands } from './commands/claude';
10
11
 
11
12
  declare const BUILD_VERSION: string | undefined;
12
13
 
@@ -32,5 +33,6 @@ registerJiraCommands(program);
32
33
  registerSlackCommands(program);
33
34
  registerUpdateCommand(program);
34
35
  registerConfigCommands(program);
36
+ registerClaudeCommands(program);
35
37
 
36
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,285 @@
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
+ };
105
+
106
+ const componentTypes: ComponentType[] = ['skills', 'commands', 'hooks'];
107
+
108
+ for (const type of componentTypes) {
109
+ const typePath = path.join(basePath, type);
110
+ if (fs.existsSync(typePath)) {
111
+ const entries = fs.readdirSync(typePath, { withFileTypes: true });
112
+ result[type] = entries
113
+ .filter((e) => e.isDirectory())
114
+ .map((e) => e.name);
115
+ }
116
+ }
117
+
118
+ return result;
119
+ }
120
+
121
+ /**
122
+ * Copy a component from the cloned repository to the target directory.
123
+ */
124
+ function copyComponent(
125
+ repoDir: string,
126
+ parsed: ParsedSource,
127
+ componentType: ComponentType,
128
+ componentName: string,
129
+ targetDir: string
130
+ ): void {
131
+ const basePath = parsed.path ? path.join(repoDir, parsed.path) : repoDir;
132
+ const srcPath = path.join(basePath, componentType, componentName);
133
+ const destPath = path.join(targetDir, '.claude', componentType, componentName);
134
+
135
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
136
+ fs.cpSync(srcPath, destPath, { recursive: true });
137
+ }
138
+
139
+ /**
140
+ * Determine which components to install based on options.
141
+ */
142
+ function determineComponentsToInstall(
143
+ options: PluginInstallOptions,
144
+ discovered: DiscoveredComponents
145
+ ): DiscoveredComponents {
146
+ // If no specific flags, install all
147
+ const installAll = !options.skills && !options.commands && !options.hooks;
148
+
149
+ return {
150
+ skills: installAll || options.skills ? discovered.skills : [],
151
+ commands: installAll || options.commands ? discovered.commands : [],
152
+ hooks: installAll || options.hooks ? discovered.hooks : [],
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Get the component types array for agentio.json based on what was installed.
158
+ */
159
+ function getInstalledComponentTypes(
160
+ options: PluginInstallOptions
161
+ ): ComponentType[] | undefined {
162
+ const installAll = !options.skills && !options.commands && !options.hooks;
163
+ if (installAll) return undefined; // Default: all
164
+
165
+ const types: ComponentType[] = [];
166
+ if (options.skills) types.push('skills');
167
+ if (options.commands) types.push('commands');
168
+ if (options.hooks) types.push('hooks');
169
+ return types;
170
+ }
171
+
172
+ /**
173
+ * Install a plugin from a source.
174
+ */
175
+ export async function installPlugin(
176
+ source: string,
177
+ options: PluginInstallOptions
178
+ ): Promise<InstallResult> {
179
+ const parsed = parseSource(source);
180
+
181
+ // Clone repo to temp directory
182
+ const repoDir = cloneRepo(parsed);
183
+
184
+ try {
185
+ // Read manifest from cloned repo
186
+ const manifest = readPluginManifest(repoDir, parsed);
187
+
188
+ // Discover available components
189
+ const discovered = discoverComponents(repoDir, parsed);
190
+
191
+ // Determine what to install
192
+ const toInstall = determineComponentsToInstall(options, discovered);
193
+
194
+ const targetDir = options.targetDir || process.cwd();
195
+ const installed: InstalledComponent[] = [];
196
+
197
+ // Install skills
198
+ for (const skillName of toInstall.skills) {
199
+ const destPath = path.join(targetDir, '.claude', 'skills', skillName);
200
+
201
+ if (fs.existsSync(destPath)) {
202
+ if (!options.force) {
203
+ console.error(` Skipping existing skill: ${skillName}`);
204
+ continue;
205
+ }
206
+ fs.rmSync(destPath, { recursive: true });
207
+ }
208
+
209
+ copyComponent(repoDir, parsed, 'skills', skillName, targetDir);
210
+ installed.push({ name: skillName, type: 'skills', path: destPath });
211
+ console.error(` Installed skill: ${skillName}`);
212
+ }
213
+
214
+ // Install commands
215
+ for (const cmdName of toInstall.commands) {
216
+ const destPath = path.join(targetDir, '.claude', 'commands', cmdName);
217
+
218
+ if (fs.existsSync(destPath)) {
219
+ if (!options.force) {
220
+ console.error(` Skipping existing command: ${cmdName}`);
221
+ continue;
222
+ }
223
+ fs.rmSync(destPath, { recursive: true });
224
+ }
225
+
226
+ copyComponent(repoDir, parsed, 'commands', cmdName, targetDir);
227
+ installed.push({ name: cmdName, type: 'commands', path: destPath });
228
+ console.error(` Installed command: ${cmdName}`);
229
+ }
230
+
231
+ // Install hooks
232
+ for (const hookName of toInstall.hooks) {
233
+ const destPath = path.join(targetDir, '.claude', 'hooks', hookName);
234
+
235
+ if (fs.existsSync(destPath)) {
236
+ if (!options.force) {
237
+ console.error(` Skipping existing hook: ${hookName}`);
238
+ continue;
239
+ }
240
+ fs.rmSync(destPath, { recursive: true });
241
+ }
242
+
243
+ copyComponent(repoDir, parsed, 'hooks', hookName, targetDir);
244
+ installed.push({ name: hookName, type: 'hooks', path: destPath });
245
+ console.error(` Installed hook: ${hookName}`);
246
+ }
247
+
248
+ // Update agentio.json
249
+ addPlugin(targetDir, manifest.name, {
250
+ source: source,
251
+ version: manifest.version,
252
+ components: getInstalledComponentTypes(options),
253
+ installedComponents: installed,
254
+ });
255
+
256
+ return {
257
+ success: true,
258
+ manifest,
259
+ installed,
260
+ };
261
+ } finally {
262
+ // Always cleanup temp directory
263
+ cleanupTempDir(repoDir);
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Remove installed components for a plugin.
269
+ */
270
+ export function removePluginFiles(
271
+ targetDir: string,
272
+ components: InstalledComponent[]
273
+ ): void {
274
+ for (const comp of components) {
275
+ const compPath = path.join(
276
+ targetDir,
277
+ '.claude',
278
+ comp.type,
279
+ comp.name
280
+ );
281
+ if (fs.existsSync(compPath)) {
282
+ fs.rmSync(compPath, { recursive: true });
283
+ }
284
+ }
285
+ }
@@ -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,62 @@
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';
18
+
19
+ // Discovered components from filesystem
20
+ export interface DiscoveredComponents {
21
+ skills: string[];
22
+ commands: string[];
23
+ hooks: string[];
24
+ }
25
+
26
+ // Installation options
27
+ export interface PluginInstallOptions {
28
+ skills?: boolean;
29
+ commands?: boolean;
30
+ hooks?: boolean;
31
+ force?: boolean;
32
+ targetDir?: string;
33
+ }
34
+
35
+ // Single installed component record
36
+ export interface InstalledComponent {
37
+ name: string;
38
+ type: ComponentType;
39
+ path: string;
40
+ }
41
+
42
+ // Plugin entry in agentio.json
43
+ export interface AgentioPluginEntry {
44
+ source: string;
45
+ version: string;
46
+ components?: ComponentType[];
47
+ installedComponents: InstalledComponent[];
48
+ }
49
+
50
+ // agentio.json structure
51
+ export interface AgentioJson {
52
+ plugins: {
53
+ [pluginName: string]: AgentioPluginEntry;
54
+ };
55
+ }
56
+
57
+ // Installation result
58
+ export interface InstallResult {
59
+ success: boolean;
60
+ manifest: PluginManifest;
61
+ installed: InstalledComponent[];
62
+ }