Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions integrations/opencli/fmhy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# FMHY OpenCLI adapter

Public, browserless adapters for the live `https://fmhy.net/` sitemap. The adapter reads only URLs currently published by FMHY, checks `robots.txt`, limits concurrency, and returns structured page text and resource links.

## Install

```powershell
& .\integrations\opencli\fmhy\install.ps1
```

Use `-Force` only to replace an existing managed FMHY adapter.

## Commands

```powershell
opencli fmhy pages -f json
opencli fmhy page ai -f json
opencli fmhy crawl --group other -f json
opencli fmhy search "open source" --limit 25 -f json
```

`crawl` discovers the current sitemap on each run. It therefore covers new or removed FMHY subpages without regenerating one command per URL.
28 changes: 28 additions & 0 deletions integrations/opencli/fmhy/crawl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { cli, Strategy } from '@jackwener/opencli/registry';
import { crawlPages } from './utils.js';

cli({
site: 'fmhy',
name: 'crawl',
access: 'read',
description: 'Crawl every live FMHY sitemap page with robots enforcement and bounded concurrency',
domain: 'fmhy.net',
strategy: Strategy.PUBLIC,
browser: false,
args: [
{ name: 'group', type: 'str', required: false, help: 'Optional first path segment, such as posts or other' },
{ name: 'max-pages', type: 'int', required: false, default: 0, help: 'Maximum pages to crawl; 0 means every matching sitemap page' },
{ name: 'concurrency', type: 'int', required: false, default: 3, help: 'Concurrent requests, clamped to 1-6' },
{ name: 'delay-ms', type: 'int', required: false, default: 150, help: 'Polite delay before each page request, capped at 2000ms' },
],
columns: ['page', 'page_title', 'section', 'kind', 'title', 'description', 'url', 'links'],
func: async (kwargs) => {
const result = await crawlPages({
group: kwargs.group,
maxPages: kwargs['max-pages'],
concurrency: kwargs.concurrency,
delayMs: kwargs['delay-ms'],
});
return result.records;
},
});
19 changes: 19 additions & 0 deletions integrations/opencli/fmhy/install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[CmdletBinding()]
param(
[switch]$Force
)

$destination = Join-Path $env:USERPROFILE '.opencli\clis\fmhy'
$adapterFiles = @('utils.js', 'pages.js', 'page.js', 'crawl.js', 'search.js')

if ((Test-Path -LiteralPath $destination) -and -not $Force) {
throw "FMHY adapter already exists at $destination. Re-run with -Force to replace the managed files."
}

New-Item -ItemType Directory -Path $destination -Force | Out-Null
foreach ($name in $adapterFiles) {
Copy-Item -LiteralPath (Join-Path $PSScriptRoot $name) -Destination (Join-Path $destination $name) -Force
}

Write-Output "Installed FMHY OpenCLI adapter to $destination"
Write-Output 'Validate with: opencli validate fmhy'
7 changes: 7 additions & 0 deletions integrations/opencli/fmhy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"private": true,
"type": "module",
"scripts": {
"test": "node --test utils.test.mjs"
}
}
17 changes: 17 additions & 0 deletions integrations/opencli/fmhy/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { cli, Strategy } from '@jackwener/opencli/registry';
import { fetchPage } from './utils.js';

cli({
site: 'fmhy',
name: 'page',
access: 'read',
description: 'Fetch one live FMHY sitemap page as structured text and resource records',
domain: 'fmhy.net',
strategy: Strategy.PUBLIC,
browser: false,
args: [
{ name: 'path', type: 'str', required: true, positional: true, help: 'FMHY path or full fmhy.net URL, for example ai or /other/selfhosting' },
],
columns: ['page', 'page_title', 'section', 'kind', 'title', 'description', 'url', 'links'],
func: async (kwargs) => (await fetchPage(kwargs.path)).records,
});
21 changes: 21 additions & 0 deletions integrations/opencli/fmhy/pages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { cli, Strategy } from '@jackwener/opencli/registry';
import { fetchSitemap, sitemapRows } from './utils.js';

cli({
site: 'fmhy',
name: 'pages',
access: 'read',
description: 'List every crawlable FMHY page from the live sitemap',
domain: 'fmhy.net',
strategy: Strategy.PUBLIC,
browser: false,
args: [
{ name: 'group', type: 'str', required: false, help: 'Optional first path segment, such as posts or other' },
],
columns: ['path', 'group', 'kind', 'url'],
func: async (kwargs) => {
const rows = sitemapRows(await fetchSitemap());
const group = String(kwargs.group ?? '').trim().toLowerCase();
return group ? rows.filter(row => row.group.toLowerCase() === group) : rows;
},
});
38 changes: 38 additions & 0 deletions integrations/opencli/fmhy/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { cli, Strategy } from '@jackwener/opencli/registry';
import { crawlPages } from './utils.js';

cli({
site: 'fmhy',
name: 'search',
access: 'read',
description: 'Search structured content across all live FMHY sitemap pages',
domain: 'fmhy.net',
strategy: Strategy.PUBLIC,
browser: false,
args: [
{ name: 'query', type: 'str', required: true, positional: true, help: 'Case-insensitive text to search for' },
{ name: 'group', type: 'str', required: false, help: 'Optional first path segment, such as posts or other' },
{ name: 'limit', type: 'int', required: false, default: 50, help: 'Maximum matching records, capped at 500' },
{ name: 'concurrency', type: 'int', required: false, default: 3, help: 'Concurrent requests, clamped to 1-6' },
{ name: 'delay-ms', type: 'int', required: false, default: 150, help: 'Polite delay before each page request, capped at 2000ms' },
],
columns: ['page', 'page_title', 'section', 'kind', 'title', 'description', 'url', 'links'],
func: async (kwargs) => {
const query = String(kwargs.query ?? '').trim().toLowerCase();
if (!query) throw new Error('FMHY search requires a non-empty query.');
const limit = Math.max(1, Math.min(Number(kwargs.limit) || 50, 500));
const result = await crawlPages({
group: kwargs.group,
concurrency: kwargs.concurrency,
delayMs: kwargs['delay-ms'],
});
return result.records.filter(record => [
record.page,
record.page_title,
record.section,
record.title,
record.description,
record.url,
].some(value => String(value ?? '').toLowerCase().includes(query))).slice(0, limit);
},
});
Loading
Loading