SuperAPI AI Directory

opencli-oneshot

Coding & development

View repository

Updated 10-16 06:19

About this skill

CLI-ONESHOT — Single-point quick CLI generation

Given a URL + a one-sentence description, generate a CLI command in 4 steps. For full exploratory development, see opencli-explorer skill。

Switch to explorer immediately in the following cases; don't keep forcing it in oneshot:

  • Step 3 verification: fetch consistently fails to get data (signing/risk control, not something cookie/header can solve)
  • Need a Pinia Store Action to trigger the API
  • Need to generate 2 or more commands for the same site
  • opencli browser network is completely empty, and baseURL can't be found in the JS bundle either

Input

Project Example
URL https://x.com/jakevin7/lists
Goal Get my Twitter Lists

Flow

Step 1: Open the page + capture traffic

opencli browser open <目标 URL>          # 打开目标页面(自动开始抓包)
opencli browser wait time 3              # 等页面加载完、API 请求触发
opencli browser network                  # 查看捕获的 JSON API 请求

Key:network By default, static resources are already filtered out, showing only JSON/XML/text API requests. If the API isn't triggered automatically, use opencli browser state to find the button index,opencli browser click <N> click it, then network capture again.

network is empty? ① Re- open refresh the capture window; ② If it's an SPA, the API domain may be api.xxx.com rather than app.xxx.com, use the bundle search in Step 2 to find the real baseURL.

Step 2: Lock onto one endpoint

From the opencli browser network results, findthattarget API, use --detail to view the full response:

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

Pay attention to these fields:

Field What to look for
URL API path pattern (e.g. /i/api/graphql/xxx/ListsManagePinTimeline)
Method GET / POST
Headers Has Cookie? Bearer? CSRF? Custom signature?
Response Which path the data is at (e.g. data.list.lists)

SPA note: If the API host in network differs from the page host (e.g. api.xxx.com vs app.xxx.com), then for subsequent fetch use the full URL. If network has no API requests at all, search the JS bundle for 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: Verify the endpoint can be reproduced

Use opencli browser eval to reproduce the request in the page via 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();
})()"

Carries Bearer but returns { error: "Missing X-Xxx-Id header" }(HTTP 400) → multi-tenant SaaS needs a business context Header. First call /servers or /workspaces to get the ID, then add it to headers.

If fetch can get the data → use a TS adapter (cli() pipeline or func()). If fetch can't get it (signing/risk control) → use the intercept strategy (TS func() + installInterceptor)。

Step 4: Apply the template, generate the adapter

Based on the strategy determined in Step 3, pick a template to generate the file.


Auth quick reference

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)

Templates

JS — Cookie/Public (simplest,func() mode)

// 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 (modern 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 (capture mode)

// 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 (e.g. 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 [];
  },
});

Testing (required)

Two development scenarios:

  • Repo contribution: Put the file in clis/<site>/<name>.js,npm run build and it auto-registers
  • Private adapter(for local use, no PR needed): Put the file in ~/.opencli/clis/<site>/<name>.js, no build needed
# Repo 贡献:build 后直接运行
npm run build
opencli list | grep mysite                 # 确认注册
opencli mysite mycommand --limit 3 -v      # 实际运行

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

Done criteria: After the command runs, it returns a non-empty table, and the fields match expectations.


That's it, nothing more

After writing the files → build + run (Repo contribution) or browser verify (private adapter) → submit. If there are issues, look again at opencli-explorer skill。