mcpField
O
opencli-oneshot

opencli-oneshot

github.com/jackwener開発30.7k3.9k 回インストール2 日前に更新
リポジトリ

opencli-oneshot について

opencli-oneshotは、ブラウザーのAPI通信を調べ、リクエストを検証してテンプレートからadapterを作成する、OpenCLIユーザー向けのスキルです。サイトごとに単一コマンドを作る用途に絞り、複雑な場合はopencli-explorerへの移行を案内します。

主な機能

  1. 01ページ内のAPIリクエストを捕捉
  2. 02エンドポイントと応答データを確認
  3. 03CookieやBearer認証を検証
  4. 04テンプレートからadapterを生成
  5. 05コマンドを実行して出力を検証

活用シーン

  1. 01Webサイト向けの単一CLIコマンド作成
  2. 02ページのリクエストをfetchで再現できるか確認
  3. 03adapterが想定データを返すかテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jackwener/opencli --skill opencli-oneshot

インストール後、Claude Code で/opencli-oneshotを入力して読み込まれたか確認します。

詳細

CLI-ONESHOT — 単一ポイントの CLI をすばやく生成

URL と一文の説明を渡すと、4 ステップで CLI コマンドを生成します。 探索型の開発全般については、opencli-explorer skill をご覧ください。

以下の場合は、すぐに explorer に切り替えてください。oneshot で無理に続けないでください:

  • Step 3 の検証で fetch が一貫してデータを取得できない(署名/リスク制御によるもので、cookie/header では解決できない)
  • Pinia Store Action による API の呼び出しが必要
  • 同じサイトで 2 つ以上のコマンドを生成する必要がある
  • opencli browser network が完全に空で、JS bundle にも baseURL が見つからない

入力

項目例
URLhttps://x.com/jakevin7/lists
Goal自分の Twitter Lists を取得する

手順

Step 1: ページを開いてリクエストをキャプチャ
opencli browser open <目标 URL>          # 打开目标页面(自动开始抓包)
opencli browser wait time 3              # 等页面加载完、API 请求触发
opencli browser network                  # 查看捕获的 JSON API 请求

重要:network はデフォルトで静的リソースをフィルターし、JSON/XML/text の API リクエストのみを表示します。 API が自動的に呼び出されない場合は、opencli browser state でボタンのインデックスを確認し、opencli browser click <N> でクリックしてから、もう一度 network でキャプチャしてください。

network が空ですか? ① open で再読み込みしてキャプチャウィンドウを更新します。② SPA の場合、API のドメインは app.xxx.com ではなく api.xxx.com の可能性があります。Step 2 の bundle 検索を使って実際の baseURL を探してください。

Step 2: インターフェースを特定

opencli browser network の結果から対象の API を1 つ見つけ、--detail で完全なレスポンスを確認します。

opencli browser network --detail <N>     # 查看第 N 条请求的完整响应体

次のフィールドを確認します。

フィールド確認する内容
URLAPI パスのパターン(例:/i/api/graphql/xxx/ListsManagePinTimeline)
MethodGET / POST
HeadersCookie、Bearer、CSRF、カスタム署名の有無
Responseデータのパス(例:data.list.lists)

SPA の注意点:network 内の API のホストがページのホストと異なる場合(例:api.xxx.com と app.xxx.com)、以降の fetch では完全な URL を使用してください。 network に API リクエストがまったくない場合は、JS bundle を検索して baseURL を見つけます。

opencli browser eval "(async()=>{const s=[...document.querySelectorAll('script[src]')].map(e=>e.src).find(s=>s.match(/index|main|app/));const t=await fetch(s).then(r=>r.text());const m=t.indexOf('baseURL')>-1?t.indexOf('baseURL'):t.indexOf('baseUrl');return m>-1?t.slice(m-10,m+80):'not found'})()"
Step 3: インターフェースを再現できるか検証

opencli browser eval を使い、ページ内で fetch を実行してリクエストを再現します。

# Tier 2 (Cookie): 传统网站
opencli browser eval "fetch('/api/endpoint', { credentials: 'include' }).then(r => r.json())"

# Tier 2.5 (localStorage Bearer): 现代 SaaS 主流(slock、Linear、Notion 等)
opencli browser eval "(async () => {
  const token = localStorage.getItem('access_token');  // 换成实际 key
  const res = await fetch('https://api.example.com/endpoint', {
    headers: { 'Authorization': 'Bearer ' + token },
    credentials: 'include'
  });
  return res.json();
})()"

# Tier 3 (Header): 如 Twitter 需要额外 header
opencli browser eval "(async () => {
  const ct0 = document.cookie.match(/ct0=([^;]+)/)?.[1];
  const res = await fetch('/api/endpoint', {
    headers: { 'Authorization': 'Bearer ...', 'X-Csrf-Token': ct0 },
    credentials: 'include'
  });
  return res.json();
})()"

Bearer を付けても { error: "Missing X-Xxx-Id header" } が返る(HTTP 400)場合 → マルチテナント SaaS では業務コンテキストの Header が必要です。まず /servers または /workspaces を呼び出して ID を取得し、headers に追加してください。

fetch でデータを取得できる場合 → TS adapter(cli() pipeline または func())を使用します。 fetch でデータを取得できない場合(署名/リスク制御)→ intercept 戦略(TS func() + installInterceptor)を使用します。

Step 4: テンプレートを使って adapter を生成

Step 3 で判定した戦略に応じてテンプレートを選び、ファイルを生成します。


認証の早見表

fetch(url) 直接能拿到?                        → Tier 1: public   (browser: false)
fetch(url, {credentials:'include'})?          → Tier 2: cookie
localStorage 有 token + Bearer header 能拿到?  → Tier 2.5: localStorage Bearer  ← 现代 SaaS 主流
  带了 Bearer 但返回 400 "Missing X-Xxx header"? → 先拿业务上下文 ID,加进 header
加 CSRF/Bearer header 后拿到?                 → Tier 3: header
都不行,但页面自己能请求成功?                    → Tier 4: intercept (installInterceptor)

テンプレート

JS — Cookie/Public(最小構成、func() モード)
// clis/<site>/<name>.js
import { cli, Strategy } from '@jackwener/opencli/registry';

cli({
  site: 'mysite',
  name: 'mycommand',
  description: '一句话描述',
  domain: 'www.example.com',
  strategy: Strategy.COOKIE,   // 或 Strategy.PUBLIC (加 browser: false)
  browser: true,
  args: [
    { name: 'limit', type: 'int', default: 20 },
  ],
  columns: ['rank', 'title', 'value'],
  func: async (page, kwargs) => {
    await page.goto('https://www.example.com/target-page');
    const data = await page.evaluate(`(async () => {
      const res = await fetch('/api/target', { credentials: 'include' });
      const d = await res.json();
      return (d.data?.items || []).map(item => ({
        title: item.title,
        value: item.value,
      }));
    })()`);
    return (data as any[]).slice(0, kwargs.limit).map((item, i) => ({
      rank: i + 1,
      title: item.title || '',
      value: item.value || '',
    }));
  },
});
JS — localStorage Bearer(最新の SaaS)
// clis/<site>/<name>.js
import { cli, Strategy } from '@jackwener/opencli/registry';

cli({
  site: 'mysite',
  name: 'mycommand',
  description: '一句话描述',
  domain: 'app.example.com',
  strategy: Strategy.COOKIE,
  browser: true,
  args: [
    { name: 'limit', type: 'int', default: 20 },
  ],
  columns: ['rank', 'title', 'value'],
  func: async (page, kwargs) => {
    await page.goto('https://app.example.com');
    const data = await page.evaluate(`(async () => {
      const token = localStorage.getItem('access_token');
      // 多租户 SaaS:先拿工作空间 ID
      const servers = await fetch('https://api.example.com/api/servers', {
        headers: { 'Authorization': 'Bearer ' + token }
      }).then(r => r.json());
      const server = servers[0];
      const res = await fetch('https://api.example.com/api/items', {
        headers: { 'Authorization': 'Bearer ' + token, 'X-Server-Id': server.id }
      });
      return res.json();
    })()`);
    return (data as any[]).slice(0, kwargs.limit).map((item, i) => ({
      rank: i + 1,
      title: item.title || '',
      value: item.value || '',
    }));
  },
});
JS — Intercept(リクエストキャプチャモード)
// clis/<site>/<name>.js
import { cli, Strategy } from '@jackwener/opencli/registry';

cli({
  site: 'mysite',
  name: 'mycommand',
  description: '一句话描述',
  domain: 'www.example.com',
  strategy: Strategy.INTERCEPT,
  browser: true,
  args: [
    { name: 'limit', type: 'int', default: 20 },
  ],
  columns: ['rank', 'title', 'value'],
  func: async (page, kwargs) => {
    // 1. 导航
    await page.goto('https://www.example.com/target-page');
    await page.wait(3);

    // 2. 注入拦截器(URL 子串匹配)
    await page.installInterceptor('target-api-keyword');

    // 3. 触发 API(滚动/点击)
    await page.autoScroll({ times: 2, delayMs: 2000 });

    // 4. 读取拦截的响应
    const requests = await page.getInterceptedRequests();
    if (!requests?.length) return [];

    let results: any[] = [];
    for (const req of requests) {
      const items = req.data?.data?.items || [];
      results.push(...items);
    }

    return results.slice(0, kwargs.limit).map((item, i) => ({
      rank: i + 1,
      title: item.title || '',
      value: item.value || '',
    }));
  },
});
TS — Header(Twitter GraphQL など)
import { cli, Strategy } from '@jackwener/opencli/registry';
import { AuthRequiredError } from '@jackwener/opencli/errors';

cli({
  site: 'twitter',
  name: 'mycommand',
  description: '一句话描述',
  domain: 'x.com',
  strategy: Strategy.HEADER,
  browser: true,
  args: [
    { name: 'limit', type: 'int', default: 20 },
  ],
  columns: ['rank', 'name', 'value'],
  func: async (page, kwargs) => {
    await page.goto('https://x.com');
    const data = await page.evaluate(`(async () => {
      const ct0 = document.cookie.match(/ct0=([^;]+)/)?.[1];
      if (!ct0) return { error: 'Not logged in' };
      const bearer = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D...';
      const res = await fetch('/i/api/graphql/QUERY_ID/Endpoint', {
        headers: {
          'Authorization': 'Bearer ' + decodeURIComponent(bearer),
          'X-Csrf-Token': ct0,
          'X-Twitter-Auth-Type': 'OAuth2Session',
        },
        credentials: 'include',
      });
      return res.json();
    })()`);
    if ((data as any).error) throw new AuthRequiredError('x.com');
    // 解析 data...
    return [];
  },
});

テスト(必須)

2 つの開発シナリオ:

  • Repo へのコントリビューション:ファイルを clis/<site>/<name>.js に配置します。npm run build の後、自動的に登録されます。
  • 個人用 adapter(ローカルでの使用、PR の提出不要):ファイルを ~/.opencli/clis/<site>/<name>.js に配置します。build は不要です。
# Repo 贡献:build 后直接运行
npm run build
opencli list | grep mysite                 # 确认注册
opencli mysite mycommand --limit 3 -v      # 实际运行

# 私人 adapter(~/.opencli/clis/):一键验证
opencli browser verify <site>/<name>

完了基準:コマンドの実行後に空でないテーブルが返され、フィールドが期待どおりであること。


以上です

ファイルを作成 → build + run(Repo へのコントリビューション)または browser verify(個人用 adapter)→ 提出します。問題があれば、opencli-explorer skill をご覧ください。

コメント