@plosson/agentio 0.1.24 → 0.1.26
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/package.json +3 -2
- package/src/commands/claude.ts +195 -78
- package/src/commands/rss.ts +68 -0
- package/src/config/config-manager.ts +1 -1
- package/src/index.ts +2 -0
- package/src/services/rss/client.ts +230 -0
- package/src/types/rss.ts +33 -0
- package/src/utils/output.ts +48 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plosson/agentio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "CLI for LLM agents to interact with communication and tracking services",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"commander": "^14.0.2",
|
|
50
|
-
"googleapis": "^169.0.0"
|
|
50
|
+
"googleapis": "^169.0.0",
|
|
51
|
+
"rss-parser": "^3.13.0"
|
|
51
52
|
}
|
|
52
53
|
}
|
package/src/commands/claude.ts
CHANGED
|
@@ -10,6 +10,15 @@ import {
|
|
|
10
10
|
removePlugin,
|
|
11
11
|
} from '../services/claude-plugin/agentio-json';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Determine if the argument is a marketplace URL or a plugin name.
|
|
15
|
+
* - URL (contains :// or github.com/) -> marketplace
|
|
16
|
+
* - Contains @ (e.g., plugin@marketplace) -> plugin
|
|
17
|
+
*/
|
|
18
|
+
function isMarketplaceUrl(arg: string): boolean {
|
|
19
|
+
return arg.includes('://') || arg.startsWith('github.com/');
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
/**
|
|
14
23
|
* Execute a claude CLI command and return the result.
|
|
15
24
|
*/
|
|
@@ -90,6 +99,82 @@ async function installPluginCmd(name: string): Promise<boolean> {
|
|
|
90
99
|
return false;
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Get list of installed marketplaces with their names and sources.
|
|
104
|
+
*/
|
|
105
|
+
async function getMarketplaces(): Promise<Array<{ name: string; source: string }>> {
|
|
106
|
+
const result = await execClaude(['plugin', 'marketplace', 'list']);
|
|
107
|
+
if (!result.success) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const marketplaces: Array<{ name: string; source: string }> = [];
|
|
112
|
+
const lines = result.stdout.split('\n');
|
|
113
|
+
|
|
114
|
+
let currentName: string | null = null;
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
// Match marketplace name: " ❯ marketplace-name"
|
|
117
|
+
const nameMatch = line.match(/^\s*❯\s+(.+)$/);
|
|
118
|
+
if (nameMatch) {
|
|
119
|
+
currentName = nameMatch[1].trim();
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Match source line: " Source: GitHub (user/repo)" or " Source: Git (url)"
|
|
124
|
+
const sourceMatch = line.match(/^\s*Source:\s*(?:GitHub|Git)\s*\((.+)\)$/);
|
|
125
|
+
if (sourceMatch && currentName) {
|
|
126
|
+
marketplaces.push({ name: currentName, source: sourceMatch[1].trim() });
|
|
127
|
+
currentName = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return marketplaces;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Find marketplace name by URL.
|
|
136
|
+
*/
|
|
137
|
+
async function findMarketplaceName(url: string): Promise<string | null> {
|
|
138
|
+
const marketplaces = await getMarketplaces();
|
|
139
|
+
|
|
140
|
+
// Normalize the URL for comparison
|
|
141
|
+
const normalizedUrl = url
|
|
142
|
+
.replace(/^https?:\/\//, '')
|
|
143
|
+
.replace(/^github\.com\//, '')
|
|
144
|
+
.replace(/\.git$/, '')
|
|
145
|
+
.toLowerCase();
|
|
146
|
+
|
|
147
|
+
for (const mp of marketplaces) {
|
|
148
|
+
const normalizedSource = mp.source
|
|
149
|
+
.replace(/^https?:\/\//, '')
|
|
150
|
+
.replace(/^github\.com\//, '')
|
|
151
|
+
.replace(/\.git$/, '')
|
|
152
|
+
.toLowerCase();
|
|
153
|
+
|
|
154
|
+
if (normalizedSource === normalizedUrl || normalizedSource.includes(normalizedUrl) || normalizedUrl.includes(normalizedSource)) {
|
|
155
|
+
return mp.name;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Update a marketplace by name.
|
|
164
|
+
*/
|
|
165
|
+
async function updateMarketplace(name: string): Promise<boolean> {
|
|
166
|
+
console.error(`Updating marketplace: ${name}`);
|
|
167
|
+
const result = await execClaude(['plugin', 'marketplace', 'update', name]);
|
|
168
|
+
|
|
169
|
+
if (result.success) {
|
|
170
|
+
console.log(` Updated: ${name}`);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.error(` Failed: ${result.stderr.trim()}`);
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
93
178
|
/**
|
|
94
179
|
* Uninstall a plugin by calling claude plugin uninstall --scope project.
|
|
95
180
|
*/
|
|
@@ -118,79 +203,71 @@ export function registerClaudeCommands(program: Command): void {
|
|
|
118
203
|
.command('claude')
|
|
119
204
|
.description('Claude Code plugin operations');
|
|
120
205
|
|
|
121
|
-
// install command
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
.
|
|
126
|
-
.description('Add a plugin marketplace')
|
|
127
|
-
.argument('<url>', 'Marketplace GitHub URL')
|
|
206
|
+
// install command - auto-detects marketplace vs plugin
|
|
207
|
+
claude
|
|
208
|
+
.command('install')
|
|
209
|
+
.description('Install a marketplace (URL) or plugin (name@marketplace), or all from agentio.json')
|
|
210
|
+
.argument('[target]', 'Marketplace URL or plugin name (e.g., plugin@marketplace)')
|
|
128
211
|
.option('-d, --dir <path>', 'Directory with agentio.json (default: current directory)')
|
|
129
|
-
.action(async (
|
|
212
|
+
.action(async (target, options) => {
|
|
130
213
|
try {
|
|
131
214
|
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
132
215
|
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
} catch (error) {
|
|
138
|
-
handleError(error);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
install
|
|
143
|
-
.command('plugin')
|
|
144
|
-
.description('Install a plugin')
|
|
145
|
-
.argument('<name>', 'Plugin name (e.g., plugin-name@marketplace)')
|
|
146
|
-
.option('-d, --dir <path>', 'Directory with agentio.json (default: current directory)')
|
|
147
|
-
.action(async (name, options) => {
|
|
148
|
-
try {
|
|
149
|
-
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
216
|
+
// No argument - install all from agentio.json
|
|
217
|
+
if (!target) {
|
|
218
|
+
const config = loadAgentioJson(targetDir);
|
|
150
219
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
} catch (error) {
|
|
156
|
-
handleError(error);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
220
|
+
if (config.marketplaces.length === 0 && config.plugins.length === 0) {
|
|
221
|
+
console.log('No marketplaces or plugins defined in agentio.json');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
159
224
|
|
|
160
|
-
|
|
161
|
-
install.action(async (options) => {
|
|
162
|
-
try {
|
|
163
|
-
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
164
|
-
const config = loadAgentioJson(targetDir);
|
|
225
|
+
console.error(`Installing from agentio.json...`);
|
|
165
226
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
227
|
+
// Install marketplaces first
|
|
228
|
+
if (config.marketplaces.length > 0) {
|
|
229
|
+
console.error(`\nMarketplaces (${config.marketplaces.length}):`);
|
|
230
|
+
for (const url of config.marketplaces) {
|
|
231
|
+
await installMarketplace(url);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
170
234
|
|
|
171
|
-
|
|
235
|
+
// Then install plugins
|
|
236
|
+
if (config.plugins.length > 0) {
|
|
237
|
+
console.error(`\nPlugins (${config.plugins.length}):`);
|
|
238
|
+
for (const name of config.plugins) {
|
|
239
|
+
await installPluginCmd(name);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
172
242
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
console.error(`\nMarketplaces (${config.marketplaces.length}):`);
|
|
176
|
-
for (const url of config.marketplaces) {
|
|
177
|
-
await installMarketplace(url);
|
|
243
|
+
console.log('\nDone.');
|
|
244
|
+
return;
|
|
178
245
|
}
|
|
179
|
-
}
|
|
180
246
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
247
|
+
// Auto-detect type based on argument format
|
|
248
|
+
if (isMarketplaceUrl(target)) {
|
|
249
|
+
// It's a marketplace URL
|
|
250
|
+
const success = await installMarketplace(target);
|
|
251
|
+
if (success) {
|
|
252
|
+
addMarketplace(targetDir, target);
|
|
253
|
+
}
|
|
254
|
+
} else if (target.includes('@')) {
|
|
255
|
+
// It's a plugin (contains @)
|
|
256
|
+
const success = await installPluginCmd(target);
|
|
257
|
+
if (success) {
|
|
258
|
+
addPlugin(targetDir, target);
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
throw new CliError(
|
|
262
|
+
'INVALID_PARAMS',
|
|
263
|
+
`Cannot determine type for: ${target}`,
|
|
264
|
+
'Use a URL for marketplaces or name@marketplace for plugins'
|
|
265
|
+
);
|
|
186
266
|
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
handleError(error);
|
|
187
269
|
}
|
|
188
|
-
|
|
189
|
-
console.log('\nDone.');
|
|
190
|
-
} catch (error) {
|
|
191
|
-
handleError(error);
|
|
192
|
-
}
|
|
193
|
-
});
|
|
270
|
+
});
|
|
194
271
|
|
|
195
272
|
// list command
|
|
196
273
|
claude
|
|
@@ -228,40 +305,80 @@ export function registerClaudeCommands(program: Command): void {
|
|
|
228
305
|
}
|
|
229
306
|
});
|
|
230
307
|
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
.
|
|
236
|
-
.description('Remove a marketplace from agentio.json')
|
|
237
|
-
.argument('<url>', 'Marketplace URL to remove')
|
|
308
|
+
// update command - updates marketplaces
|
|
309
|
+
claude
|
|
310
|
+
.command('update')
|
|
311
|
+
.description('Update marketplaces (all from agentio.json or a specific URL)')
|
|
312
|
+
.argument('[url]', 'Marketplace URL to update (updates all from agentio.json if not specified)')
|
|
238
313
|
.option('-d, --dir <path>', 'Directory with agentio.json (default: current directory)')
|
|
239
314
|
.action(async (url, options) => {
|
|
240
315
|
try {
|
|
241
316
|
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
242
317
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
318
|
+
if (url) {
|
|
319
|
+
// Update specific marketplace by URL
|
|
320
|
+
const name = await findMarketplaceName(url);
|
|
321
|
+
if (!name) {
|
|
322
|
+
throw new CliError('NOT_FOUND', `Marketplace not found for URL: ${url}`, 'Make sure the marketplace is installed');
|
|
323
|
+
}
|
|
324
|
+
await updateMarketplace(name);
|
|
246
325
|
} else {
|
|
247
|
-
|
|
326
|
+
// Update all marketplaces from agentio.json
|
|
327
|
+
const config = loadAgentioJson(targetDir);
|
|
328
|
+
|
|
329
|
+
if (config.marketplaces.length === 0) {
|
|
330
|
+
console.log('No marketplaces defined in agentio.json');
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
console.error(`Updating marketplaces from agentio.json...`);
|
|
335
|
+
|
|
336
|
+
for (const marketplaceUrl of config.marketplaces) {
|
|
337
|
+
const name = await findMarketplaceName(marketplaceUrl);
|
|
338
|
+
if (name) {
|
|
339
|
+
await updateMarketplace(name);
|
|
340
|
+
} else {
|
|
341
|
+
console.error(` Skipped (not installed): ${marketplaceUrl}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
console.log('\nDone.');
|
|
248
346
|
}
|
|
249
347
|
} catch (error) {
|
|
250
348
|
handleError(error);
|
|
251
349
|
}
|
|
252
350
|
});
|
|
253
351
|
|
|
254
|
-
remove
|
|
255
|
-
|
|
256
|
-
.
|
|
257
|
-
.
|
|
352
|
+
// remove command - auto-detects marketplace vs plugin
|
|
353
|
+
claude
|
|
354
|
+
.command('remove')
|
|
355
|
+
.description('Remove a marketplace (URL) or plugin (name@marketplace)')
|
|
356
|
+
.argument('<target>', 'Marketplace URL or plugin name to remove')
|
|
258
357
|
.option('-d, --dir <path>', 'Directory with agentio.json (default: current directory)')
|
|
259
|
-
.action(async (
|
|
358
|
+
.action(async (target, options) => {
|
|
260
359
|
try {
|
|
261
360
|
const targetDir = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
262
361
|
|
|
263
|
-
|
|
264
|
-
|
|
362
|
+
// Auto-detect type based on argument format
|
|
363
|
+
if (isMarketplaceUrl(target)) {
|
|
364
|
+
// It's a marketplace URL
|
|
365
|
+
const removed = removeMarketplace(targetDir, target);
|
|
366
|
+
if (removed) {
|
|
367
|
+
console.log(`Removed marketplace: ${target}`);
|
|
368
|
+
} else {
|
|
369
|
+
throw new CliError('NOT_FOUND', `Marketplace not found: ${target}`);
|
|
370
|
+
}
|
|
371
|
+
} else if (target.includes('@')) {
|
|
372
|
+
// It's a plugin (contains @)
|
|
373
|
+
await uninstallPluginCmd(target);
|
|
374
|
+
removePlugin(targetDir, target);
|
|
375
|
+
} else {
|
|
376
|
+
throw new CliError(
|
|
377
|
+
'INVALID_PARAMS',
|
|
378
|
+
`Cannot determine type for: ${target}`,
|
|
379
|
+
'Use a URL for marketplaces or name@marketplace for plugins'
|
|
380
|
+
);
|
|
381
|
+
}
|
|
265
382
|
} catch (error) {
|
|
266
383
|
handleError(error);
|
|
267
384
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { RssClient } from '../services/rss/client';
|
|
3
|
+
import { handleError } from '../utils/errors';
|
|
4
|
+
import {
|
|
5
|
+
printRssArticleList,
|
|
6
|
+
printRssArticle,
|
|
7
|
+
printRssFeedInfo,
|
|
8
|
+
} from '../utils/output';
|
|
9
|
+
|
|
10
|
+
export function registerRssCommands(program: Command): void {
|
|
11
|
+
const rss = program
|
|
12
|
+
.command('rss')
|
|
13
|
+
.description('RSS feed operations');
|
|
14
|
+
|
|
15
|
+
// Get articles from a feed
|
|
16
|
+
rss
|
|
17
|
+
.command('articles')
|
|
18
|
+
.description('List articles from a blog')
|
|
19
|
+
.argument('<url>', 'Blog URL (feed will be auto-discovered)')
|
|
20
|
+
.option('--limit <n>', 'Number of articles', '20')
|
|
21
|
+
.option('--since <date>', 'Only articles after this date (YYYY-MM-DD)')
|
|
22
|
+
.action(async (url, options) => {
|
|
23
|
+
try {
|
|
24
|
+
const client = new RssClient();
|
|
25
|
+
const listOptions = {
|
|
26
|
+
limit: parseInt(options.limit, 10),
|
|
27
|
+
since: options.since ? new Date(options.since) : undefined,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const info = await client.getInfo(url);
|
|
31
|
+
const articles = await client.list(url, listOptions);
|
|
32
|
+
printRssArticleList(articles, info.title);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
handleError(error);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Get a specific article
|
|
39
|
+
rss
|
|
40
|
+
.command('get')
|
|
41
|
+
.description('Get a specific article')
|
|
42
|
+
.argument('<url>', 'Blog URL (feed will be auto-discovered)')
|
|
43
|
+
.argument('<article-id>', 'Article ID or URL')
|
|
44
|
+
.action(async (url, articleId) => {
|
|
45
|
+
try {
|
|
46
|
+
const client = new RssClient();
|
|
47
|
+
const article = await client.get(url, articleId);
|
|
48
|
+
printRssArticle(article);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
handleError(error);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Get feed info
|
|
55
|
+
rss
|
|
56
|
+
.command('info')
|
|
57
|
+
.description('Get feed information')
|
|
58
|
+
.argument('<url>', 'Blog URL (feed will be auto-discovered)')
|
|
59
|
+
.action(async (url) => {
|
|
60
|
+
try {
|
|
61
|
+
const client = new RssClient();
|
|
62
|
+
const info = await client.getInfo(url);
|
|
63
|
+
printRssFeedInfo(info);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
handleError(error);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -115,7 +115,7 @@ export async function listProfiles(service?: ServiceName): Promise<{
|
|
|
115
115
|
default?: string;
|
|
116
116
|
}[]> {
|
|
117
117
|
const config = await loadConfig();
|
|
118
|
-
const services: ServiceName[] = service ? [service] : ['gmail', 'gchat', 'jira', 'telegram'];
|
|
118
|
+
const services: ServiceName[] = service ? [service] : ['gmail', 'gchat', 'jira', 'slack', 'telegram'];
|
|
119
119
|
|
|
120
120
|
return services.map((svc) => ({
|
|
121
121
|
service: svc,
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { registerTelegramCommands } from './commands/telegram';
|
|
|
5
5
|
import { registerGChatCommands } from './commands/gchat';
|
|
6
6
|
import { registerJiraCommands } from './commands/jira';
|
|
7
7
|
import { registerSlackCommands } from './commands/slack';
|
|
8
|
+
import { registerRssCommands } from './commands/rss';
|
|
8
9
|
import { registerUpdateCommand } from './commands/update';
|
|
9
10
|
import { registerConfigCommands } from './commands/config';
|
|
10
11
|
import { registerClaudeCommands } from './commands/claude';
|
|
@@ -31,6 +32,7 @@ registerTelegramCommands(program);
|
|
|
31
32
|
registerGChatCommands(program);
|
|
32
33
|
registerJiraCommands(program);
|
|
33
34
|
registerSlackCommands(program);
|
|
35
|
+
registerRssCommands(program);
|
|
34
36
|
registerUpdateCommand(program);
|
|
35
37
|
registerConfigCommands(program);
|
|
36
38
|
registerClaudeCommands(program);
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import Parser from 'rss-parser';
|
|
2
|
+
import type { RssFeed, RssArticle, RssListOptions } from '../../types/rss';
|
|
3
|
+
import { CliError } from '../../utils/errors';
|
|
4
|
+
|
|
5
|
+
type CustomItem = {
|
|
6
|
+
contentEncoded?: string;
|
|
7
|
+
dcCreator?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type CustomFeed = {
|
|
11
|
+
language?: string;
|
|
12
|
+
lastBuildDate?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Common feed paths to try if discovery fails
|
|
16
|
+
const COMMON_FEED_PATHS = [
|
|
17
|
+
'/feed',
|
|
18
|
+
'/feed.xml',
|
|
19
|
+
'/rss',
|
|
20
|
+
'/rss.xml',
|
|
21
|
+
'/atom.xml',
|
|
22
|
+
'/index.xml',
|
|
23
|
+
'/feed/atom',
|
|
24
|
+
'/feed/rss',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export class RssClient {
|
|
28
|
+
private parser: Parser<CustomFeed, CustomItem>;
|
|
29
|
+
private feedUrl: string | null = null;
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
this.parser = new Parser({
|
|
33
|
+
customFields: {
|
|
34
|
+
item: [['content:encoded', 'contentEncoded'], ['dc:creator', 'dcCreator']],
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Discover RSS feed URL from a blog URL
|
|
41
|
+
*/
|
|
42
|
+
async discoverFeed(url: string): Promise<string> {
|
|
43
|
+
// Normalize URL
|
|
44
|
+
let baseUrl = url.trim();
|
|
45
|
+
if (!baseUrl.startsWith('http://') && !baseUrl.startsWith('https://')) {
|
|
46
|
+
baseUrl = 'https://' + baseUrl;
|
|
47
|
+
}
|
|
48
|
+
baseUrl = baseUrl.replace(/\/$/, '');
|
|
49
|
+
|
|
50
|
+
// First, try to parse the URL directly as a feed
|
|
51
|
+
try {
|
|
52
|
+
await this.parser.parseURL(baseUrl);
|
|
53
|
+
this.feedUrl = baseUrl;
|
|
54
|
+
return baseUrl;
|
|
55
|
+
} catch {
|
|
56
|
+
// Not a feed, continue with discovery
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fetch the page and look for feed links in HTML
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(baseUrl, {
|
|
62
|
+
headers: { 'User-Agent': 'agentio-rss/1.0' },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (response.ok) {
|
|
66
|
+
const html = await response.text();
|
|
67
|
+
const feedUrl = this.extractFeedUrl(html, baseUrl);
|
|
68
|
+
if (feedUrl) {
|
|
69
|
+
// Verify it's a valid feed
|
|
70
|
+
try {
|
|
71
|
+
await this.parser.parseURL(feedUrl);
|
|
72
|
+
this.feedUrl = feedUrl;
|
|
73
|
+
return feedUrl;
|
|
74
|
+
} catch {
|
|
75
|
+
// Invalid feed, continue trying
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Failed to fetch page, continue with common paths
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Try common feed paths
|
|
84
|
+
for (const path of COMMON_FEED_PATHS) {
|
|
85
|
+
const feedUrl = baseUrl + path;
|
|
86
|
+
try {
|
|
87
|
+
await this.parser.parseURL(feedUrl);
|
|
88
|
+
this.feedUrl = feedUrl;
|
|
89
|
+
return feedUrl;
|
|
90
|
+
} catch {
|
|
91
|
+
// Try next path
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
throw new CliError(
|
|
96
|
+
'NOT_FOUND',
|
|
97
|
+
`Could not find RSS feed for: ${url}`,
|
|
98
|
+
'Try providing the direct feed URL instead'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Extract feed URL from HTML link tags
|
|
104
|
+
*/
|
|
105
|
+
private extractFeedUrl(html: string, baseUrl: string): string | null {
|
|
106
|
+
// Match <link> tags with rel="alternate" and type containing rss or atom
|
|
107
|
+
const linkRegex = /<link[^>]*rel=["']alternate["'][^>]*>/gi;
|
|
108
|
+
const matches = html.match(linkRegex) || [];
|
|
109
|
+
|
|
110
|
+
for (const link of matches) {
|
|
111
|
+
// Check if it's an RSS or Atom feed
|
|
112
|
+
if (link.includes('application/rss+xml') ||
|
|
113
|
+
link.includes('application/atom+xml') ||
|
|
114
|
+
link.includes('application/feed+json')) {
|
|
115
|
+
// Extract href
|
|
116
|
+
const hrefMatch = link.match(/href=["']([^"']+)["']/i);
|
|
117
|
+
if (hrefMatch) {
|
|
118
|
+
let href = hrefMatch[1];
|
|
119
|
+
// Handle relative URLs
|
|
120
|
+
if (href.startsWith('/')) {
|
|
121
|
+
const urlObj = new URL(baseUrl);
|
|
122
|
+
href = urlObj.origin + href;
|
|
123
|
+
} else if (!href.startsWith('http')) {
|
|
124
|
+
href = baseUrl + '/' + href;
|
|
125
|
+
}
|
|
126
|
+
return href;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Fetch and parse the RSS feed
|
|
136
|
+
*/
|
|
137
|
+
async getFeed(url?: string): Promise<RssFeed> {
|
|
138
|
+
const feedUrl = url || this.feedUrl;
|
|
139
|
+
if (!feedUrl) {
|
|
140
|
+
throw new CliError('INVALID_PARAMS', 'No feed URL provided', 'Call discoverFeed() first or provide a URL');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const feed = await this.parser.parseURL(feedUrl);
|
|
145
|
+
return this.parseFeed(feed);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof CliError) throw error;
|
|
148
|
+
if (error instanceof Error) {
|
|
149
|
+
if (error.message.includes('ENOTFOUND') || error.message.includes('ECONNREFUSED')) {
|
|
150
|
+
throw new CliError('NETWORK_ERROR', `Cannot reach feed: ${feedUrl}`, 'Check the URL and your internet connection');
|
|
151
|
+
}
|
|
152
|
+
if (error.message.includes('Non-whitespace before first tag') || error.message.includes('Invalid XML')) {
|
|
153
|
+
throw new CliError('INVALID_PARAMS', 'Invalid RSS feed format', 'The URL may not be an RSS feed');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
throw new CliError('API_ERROR', `Failed to parse feed: ${error}`, 'Verify the feed URL is valid');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get articles from the feed
|
|
162
|
+
*/
|
|
163
|
+
async list(url: string, options?: RssListOptions): Promise<RssArticle[]> {
|
|
164
|
+
const feedUrl = await this.discoverFeed(url);
|
|
165
|
+
const feed = await this.getFeed(feedUrl);
|
|
166
|
+
let articles = feed.items;
|
|
167
|
+
|
|
168
|
+
// Filter by date if specified
|
|
169
|
+
if (options?.since) {
|
|
170
|
+
articles = articles.filter(article => {
|
|
171
|
+
if (!article.pubDate) return true;
|
|
172
|
+
return new Date(article.pubDate) >= options.since!;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Apply limit
|
|
177
|
+
const limit = options?.limit ?? 20;
|
|
178
|
+
return articles.slice(0, limit);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get a specific article by ID
|
|
183
|
+
*/
|
|
184
|
+
async get(url: string, articleId: string): Promise<RssArticle> {
|
|
185
|
+
const feedUrl = await this.discoverFeed(url);
|
|
186
|
+
const feed = await this.getFeed(feedUrl);
|
|
187
|
+
const article = feed.items.find(item => item.id === articleId || item.link === articleId);
|
|
188
|
+
|
|
189
|
+
if (!article) {
|
|
190
|
+
throw new CliError('NOT_FOUND', `Article not found: ${articleId}`, 'Use "agentio rss articles" to list available articles');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return article;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get feed info
|
|
198
|
+
*/
|
|
199
|
+
async getInfo(url: string): Promise<RssFeed & { feedUrl: string }> {
|
|
200
|
+
const feedUrl = await this.discoverFeed(url);
|
|
201
|
+
const feed = await this.getFeed(feedUrl);
|
|
202
|
+
return { ...feed, feedUrl };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private parseFeed(raw: Parser.Output<CustomFeed & CustomItem>): RssFeed {
|
|
206
|
+
// Cast to access optional feed properties not in base type
|
|
207
|
+
const feedData = raw as Parser.Output<CustomFeed & CustomItem> & CustomFeed;
|
|
208
|
+
return {
|
|
209
|
+
title: raw.title || 'Untitled Feed',
|
|
210
|
+
description: raw.description,
|
|
211
|
+
link: raw.link,
|
|
212
|
+
language: feedData.language,
|
|
213
|
+
lastBuildDate: feedData.lastBuildDate,
|
|
214
|
+
items: (raw.items || []).map(item => this.parseArticle(item)),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private parseArticle(raw: Parser.Item & CustomItem): RssArticle {
|
|
219
|
+
return {
|
|
220
|
+
id: raw.guid || raw.link || raw.title || '',
|
|
221
|
+
title: raw.title || 'Untitled',
|
|
222
|
+
link: raw.link,
|
|
223
|
+
description: raw.contentSnippet || raw.content,
|
|
224
|
+
content: raw.contentEncoded || raw.content,
|
|
225
|
+
author: raw.creator || raw.dcCreator,
|
|
226
|
+
pubDate: raw.pubDate || raw.isoDate,
|
|
227
|
+
categories: raw.categories,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
package/src/types/rss.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsed feed metadata
|
|
3
|
+
*/
|
|
4
|
+
export interface RssFeed {
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
link?: string;
|
|
8
|
+
language?: string;
|
|
9
|
+
lastBuildDate?: string;
|
|
10
|
+
items: RssArticle[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Individual article/item from feed
|
|
15
|
+
*/
|
|
16
|
+
export interface RssArticle {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
link?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
content?: string;
|
|
22
|
+
author?: string;
|
|
23
|
+
pubDate?: string;
|
|
24
|
+
categories?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Options for listing articles
|
|
29
|
+
*/
|
|
30
|
+
export interface RssListOptions {
|
|
31
|
+
limit?: number;
|
|
32
|
+
since?: Date;
|
|
33
|
+
}
|
package/src/utils/output.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { GmailMessage, GmailAttachmentInfo } from '../types/gmail';
|
|
|
2
2
|
import type { GChatMessage } from '../types/gchat';
|
|
3
3
|
import type { JiraProject, JiraIssue, JiraTransition, JiraCommentResult, JiraTransitionResult } from '../types/jira';
|
|
4
4
|
import type { SlackSendResult } from '../types/slack';
|
|
5
|
+
import type { RssFeed, RssArticle } from '../types/rss';
|
|
5
6
|
|
|
6
7
|
function formatBytes(bytes: number): string {
|
|
7
8
|
if (bytes === 0) return '0 B';
|
|
@@ -230,3 +231,50 @@ export function printSlackSendResult(result: SlackSendResult): void {
|
|
|
230
231
|
console.log('Type: Block Kit payload');
|
|
231
232
|
}
|
|
232
233
|
}
|
|
234
|
+
|
|
235
|
+
// RSS specific formatters
|
|
236
|
+
export function printRssArticleList(articles: RssArticle[], feedName: string): void {
|
|
237
|
+
if (articles.length === 0) {
|
|
238
|
+
console.log('No articles found');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log(`Articles from ${feedName} (${articles.length})\n`);
|
|
243
|
+
|
|
244
|
+
for (let i = 0; i < articles.length; i++) {
|
|
245
|
+
const article = articles[i];
|
|
246
|
+
console.log(`[${i + 1}] ${article.title}`);
|
|
247
|
+
if (article.author) console.log(` Author: ${article.author}`);
|
|
248
|
+
if (article.pubDate) console.log(` Date: ${article.pubDate}`);
|
|
249
|
+
if (article.link) console.log(` Link: ${article.link}`);
|
|
250
|
+
if (article.description) {
|
|
251
|
+
const snippet = article.description.length > 150
|
|
252
|
+
? article.description.substring(0, 150) + '...'
|
|
253
|
+
: article.description;
|
|
254
|
+
console.log(` > ${snippet}`);
|
|
255
|
+
}
|
|
256
|
+
console.log('');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function printRssArticle(article: RssArticle): void {
|
|
261
|
+
console.log(`Title: ${article.title}`);
|
|
262
|
+
if (article.author) console.log(`Author: ${article.author}`);
|
|
263
|
+
if (article.pubDate) console.log(`Date: ${article.pubDate}`);
|
|
264
|
+
if (article.link) console.log(`Link: ${article.link}`);
|
|
265
|
+
if (article.categories && article.categories.length > 0) {
|
|
266
|
+
console.log(`Categories: ${article.categories.join(', ')}`);
|
|
267
|
+
}
|
|
268
|
+
console.log('---');
|
|
269
|
+
console.log(article.content || article.description || 'No content available');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function printRssFeedInfo(feed: RssFeed & { feedUrl: string }): void {
|
|
273
|
+
console.log(`Title: ${feed.title}`);
|
|
274
|
+
console.log(`Feed URL: ${feed.feedUrl}`);
|
|
275
|
+
if (feed.description) console.log(`Description: ${feed.description}`);
|
|
276
|
+
if (feed.link) console.log(`Site: ${feed.link}`);
|
|
277
|
+
if (feed.language) console.log(`Language: ${feed.language}`);
|
|
278
|
+
if (feed.lastBuildDate) console.log(`Last Updated: ${feed.lastBuildDate}`);
|
|
279
|
+
console.log(`Articles: ${feed.items.length}`);
|
|
280
|
+
}
|