@plosson/agentio 0.1.16 → 0.1.18

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
@@ -141,6 +141,29 @@ agentio provides a plugin for [Claude Code](https://claude.com/claude-code) with
141
141
 
142
142
  Once installed, Claude Code can use the agentio CLI skills to help you manage emails, send Telegram messages, and more.
143
143
 
144
+ ### Install Skills Directly
145
+
146
+ You can also install skills directly without the plugin system:
147
+
148
+ ```bash
149
+ # List available skills
150
+ agentio skill list
151
+
152
+ # Install all skills to current directory
153
+ agentio skill install
154
+
155
+ # Install a specific skill
156
+ agentio skill install agentio-gmail
157
+
158
+ # Install to a specific directory
159
+ agentio skill install -d ~/myproject
160
+
161
+ # Skip confirmation prompts
162
+ agentio skill install -y
163
+ ```
164
+
165
+ Skills are installed to `.claude/skills/` in the target directory.
166
+
144
167
  ## Design
145
168
 
146
169
  agentio is designed for LLM consumption:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plosson/agentio",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "CLI for LLM agents to interact with communication and tracking services",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -47,7 +47,6 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "commander": "^14.0.2",
50
- "googleapis": "^169.0.0",
51
- "playwright-core": "^1.57.0"
50
+ "googleapis": "^169.0.0"
52
51
  }
53
52
  }
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { basename, join } from 'path';
3
+ import { tmpdir } from 'os';
3
4
  import { google } from 'googleapis';
4
5
  import { getValidTokens, createGoogleAuth } from '../auth/token-manager';
5
6
  import { setCredentials, removeCredentials, getCredentials } from '../auth/token-store';
@@ -19,6 +20,53 @@ function escapeHtml(text: string): string {
19
20
  .replace(/"/g, '"');
20
21
  }
21
22
 
23
+ function findChromePath(): string | null {
24
+ const platform = process.platform;
25
+
26
+ const paths: string[] = [];
27
+
28
+ if (platform === 'darwin') {
29
+ paths.push(
30
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
31
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
32
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
33
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
34
+ );
35
+ } else if (platform === 'linux') {
36
+ paths.push(
37
+ '/usr/bin/google-chrome',
38
+ '/usr/bin/google-chrome-stable',
39
+ '/usr/bin/chromium',
40
+ '/usr/bin/chromium-browser',
41
+ '/snap/bin/chromium',
42
+ '/usr/bin/microsoft-edge',
43
+ '/usr/bin/brave-browser',
44
+ );
45
+ } else if (platform === 'win32') {
46
+ const programFiles = process.env['PROGRAMFILES'] || 'C:\\Program Files';
47
+ const programFilesX86 = process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)';
48
+ const localAppData = process.env['LOCALAPPDATA'] || '';
49
+
50
+ paths.push(
51
+ `${programFiles}\\Google\\Chrome\\Application\\chrome.exe`,
52
+ `${programFilesX86}\\Google\\Chrome\\Application\\chrome.exe`,
53
+ `${localAppData}\\Google\\Chrome\\Application\\chrome.exe`,
54
+ `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe`,
55
+ `${programFilesX86}\\Microsoft\\Edge\\Application\\msedge.exe`,
56
+ `${programFiles}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`,
57
+ `${localAppData}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`,
58
+ );
59
+ }
60
+
61
+ for (const p of paths) {
62
+ if (Bun.file(p).size > 0) {
63
+ return p;
64
+ }
65
+ }
66
+
67
+ return null;
68
+ }
69
+
22
70
  async function getGmailClient(profileName?: string): Promise<{ client: GmailClient; profile: string }> {
23
71
  const { tokens, profile } = await getValidTokens('gmail', profileName);
24
72
  const auth = createGoogleAuth(tokens);
@@ -323,25 +371,47 @@ ${emailHeader}
323
371
  </html>`;
324
372
  }
325
373
 
326
- // Lazy load playwright-core to avoid bundling issues
327
- const { chromium } = await import('playwright-core');
328
-
329
- // Launch browser and generate PDF
330
- console.error('Launching browser...');
331
- const browser = await chromium.launch({
332
- channel: 'chrome', // Use system Chrome
333
- });
374
+ // Find Chrome browser
375
+ const chromePath = findChromePath();
376
+ if (!chromePath) {
377
+ throw new CliError(
378
+ 'NOT_FOUND',
379
+ 'Chrome/Chromium not found',
380
+ 'Install Google Chrome, Chromium, or Microsoft Edge'
381
+ );
382
+ }
334
383
 
335
- const page = await browser.newPage();
336
- await page.setContent(html, { waitUntil: 'networkidle' });
337
- await page.pdf({
338
- path: options.output,
339
- format: 'A4',
340
- margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' },
341
- });
384
+ // Write HTML to temp file
385
+ const tempHtml = join(tmpdir(), `agentio-email-${Date.now()}.html`);
386
+ await Bun.write(tempHtml, html);
387
+
388
+ // Resolve output path to absolute
389
+ const outputPath = options.output.startsWith('/')
390
+ ? options.output
391
+ : join(process.cwd(), options.output);
392
+
393
+ try {
394
+ // Run Chrome in headless mode to generate PDF
395
+ console.error('Generating PDF...');
396
+ const result = Bun.spawnSync([
397
+ chromePath,
398
+ '--headless=new',
399
+ '--disable-gpu',
400
+ '--no-pdf-header-footer',
401
+ `--print-to-pdf=${outputPath}`,
402
+ tempHtml,
403
+ ]);
404
+
405
+ if (result.exitCode !== 0) {
406
+ const stderr = result.stderr.toString();
407
+ throw new CliError('API_ERROR', `Chrome failed: ${stderr}`);
408
+ }
342
409
 
343
- await browser.close();
344
- console.log(`Exported to ${options.output}`);
410
+ console.log(`Exported to ${options.output}`);
411
+ } finally {
412
+ // Clean up temp file
413
+ await Bun.file(tempHtml).unlink();
414
+ }
345
415
  } catch (error) {
346
416
  handleError(error);
347
417
  }
@@ -0,0 +1,204 @@
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
+ }
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 { registerSkillCommands } from './commands/skill';
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
+ registerSkillCommands(program);
35
37
 
36
38
  program.parse();