opencli-oneshot について
opencli-oneshotは、ブラウザーのAPI通信を調べ、リクエストを検証してテンプレートからadapterを作成する、OpenCLIユーザー向けのスキルです。サイトごとに単一コマンドを作る用途に絞り、複雑な場合はopencli-explorerへの移行を案内します。
主な機能
- 01ページ内のAPIリクエストを捕捉
- 02エンドポイントと応答データを確認
- 03CookieやBearer認証を検証
- 04テンプレートからadapterを生成
- 05コマンドを実行して出力を検証
活用シーン
- 01Webサイト向けの単一CLIコマンド作成
- 02ページのリクエストをfetchで再現できるか確認
- 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 が見つからない
入力
| 項目 | 例 |
|---|---|
| URL | https://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 条请求的完整响应体
次のフィールドを確認します。
| フィールド | 確認する内容 |
|---|---|
| URL | API パスのパターン(例:/i/api/graphql/xxx/ListsManagePinTimeline) |
| Method | GET / POST |
| Headers | Cookie、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 をご覧ください。