Skill: skywalker-lili/gemini-deep-research-jclaw (version 1.0.0)

--- name: gemini-deep-research description: "Gemini Deep Research via the gemini-cli deep-research MCP extension. Use when user wants to research a topic deeply, run market/industry analysis, or generate a comprehensive report. Triggers on: deep research X, 帮我研究 X, gemini deep research X, 研究一下 X, do a deep search on X. Requires the gemini-cli and gemini-deep-research extension to be installed and a paid Google AI API key to be configured." --- # Gemini Deep Research Executes a full Deep Research workflow using the official `gemini-deep-research` MCP extension. ## Prerequisites See `references/setup-guide.md` for full setup instructions. The skill assumes: - `gemini` CLI installed (`npm install -g @google/gemini-cli`) - `gemini-deep-research` extension installed and enabled - Paid Google AI API key configured via `gemini extensions config gemini-deep-research` If any prerequisite is missing, inform the user and link to `references/setup-guide.md`. ## Workflow ### Step 1 — Confirm Intent & Parameters Ask the user to confirm: 1. **Research topic** (exact wording goes to Gemini — make it clear and specific) 2. **Report format** — present as a choice: - `Comprehensive Research Report` (default, most thorough) - `Executive Brief` (concise, ~1-2 pages) - `Technical Deep Dive` (detailed, technical audience) 3. **Save path** — default: `~/ObsidianVault/Default/DeepResearch/<YYYYMMDD>-<slug>.md` If user does not specify format, use `Comprehensive Research Report`. If user does not specify a save path, use the default and inform them. ### Step 2 — Build the Command ```bash node <skill>/scripts/dr-client.js \ --input "<user's research topic>" \ --output "<full save path>" \ --format "<chosen format>" \ --timeout 900000 ``` - `<skill>` = the skill's scripts directory (resolved by the agent) - Timeout defaults to 15 minutes (900,000 ms) ### Step 3 — Spawn as Background Sub-agent Use `sessions_spawn` with `runtime: "subagent"` to run the script in the background so the main session remains responsive. Pass all parameters (input, output, format, timeout) via the `task` string. Monitor completion via the sub-agent's completion event. ### Step 4 — Handle Result Parse the JSON output from the script: - `status: "completed"` → Report the success to the user with the file path - `status: "error"` → Report the error message to the user with a suggestion to check the API key or extension setup ### Step 5 — Notify Send a Discord message (via current session reply) confirming: - Topic researched - File path - Format used - Any errors if applicable ## Report Format Reference | Format | Description | |--------|-------------| | `Comprehensive Research Report` | Full multi-section report with analysis, data, and citations | | `Executive Brief` | Condensed summary for decision-makers, ~1-2 pages | | `Technical Deep Dive` | Detailed technical analysis, suited for specialists | The format is passed as-is to the Gemini API — it serves as a style hint, not a strict guarantee. ## Error Handling | Error | Likely Cause | Resolution | |-------|-------------|------------| | `API key not found` | Key not configured | Guide user to `references/setup-guide.md` step 4 | | `429 Too Many Requests` | Free-tier key used or quota exceeded | Requires paid key — inform user | | `Research timed out` | Took > 15 minutes | Retry or shorten query | | MCP server spawn failed | Extension not installed or path wrong | Verify `~/.gemini/extensions/gemini-deep-research/` exists | ## File Naming Default pattern: `YYYYMMDD-<slug>.md` - `YYYYMMDD` = today's date - `<slug>` = sanitized version of the research topic (lowercase, spaces to hyphens, strip special chars) - Example: `20260325-iran-hormuz-strait-market-impact.md`

path: references/setup-guide.md
content: # Gemini Deep Research — Setup Guide

Prerequisite: a paid Google AI API key with billing enabled on your Google Cloud project.

## 1. Install Gemini CLI

```bash
npm install -g @google/gemini-cli
```

Verify:
```bash
gemini --version
```

## 2. Authenticate

```bash
gemini auth
```

This opens a browser for Google account auth (free_tier够用 for CLI auth).

## 3. Install the Deep Research Extension

```bash
gemini extensions install https://github.com/allenhutchison/gemini-cli-deep-research --auto-update
```

Verify installation:
```bash
gemini extensions list
# ✓ gemini-deep-research (0.2.x)
```

## 4. Configure API Key

> **Important:** The Gemini CLI strips environment variables containing `KEY` from MCP server processes. You must use extension settings — shell `GEMINI_API_KEY` will not work.

Get a paid API key from [Google AI Studio](https://aistudio.google.com/apikey) (ensure billing is enabled on your Google Cloud project).

```bash
gemini extensions config gemini-deep-research
# Navigate to "API Key" → paste your key → confirm
```

The extension stores the key securely in your system keychain.

## 5. Verify MCP Server Connects

```bash
node ~/.gemini/extensions/gemini-deep-research/dist/index.js &
# Should print: "Gemini Deep Research MCP server running on stdio"
# Press Ctrl+C to exit
```

Or test via the CLI:
```bash
gemini --extensions gemini-deep-research --prompt "test" -p
```

## Notes

- **Paid API key required.** Deep Research uses the Gemini Interactions API, which has separate quota. Free-tier keys get 429 errors.
- **Quota:** Check your usage at [Google AI Studio](https://aistudio.google.com) → API Keys → manage.
- **Extension updates:** Run `gemini extensions update` periodically to stay on latest version.
- **Platform:** Requires Node.js 18+.


path: scripts/dr-client.js
content: #!/usr/bin/env node
/**
 * Gemini Deep Research MCP Client
 * 
 * Wraps the gemini-deep-research MCP server to perform a complete
 * research workflow: start → poll → save report.
 * 
 * Usage:
 *   node dr-client.js --input "<research query>" [--output "<path>"] [--format "<report format>"] [--timeout <ms>]
 * 
 * Report formats:
 *   Comprehensive Research Report  (default)
 *   Executive Brief
 *   Technical Deep Dive
 * 
 * Output: JSON to stdout { status, researchId, reportPath, error }
 */

import { spawn } from 'child_process';
import { writeFileSync, mkdirSync, readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';

const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// ─── Argument Parsing ──────────────────────────────────────────────────────────

const args = process.argv.slice(2).reduce((acc, arg, i, arr) => {
  if (arg.startsWith('--')) {
    const key = arg.slice(2);
    acc[key] = arr[i + 1] && !arr[i + 1].startsWith('--') ? arr[i + 1] : true;
  }
  return acc;
}, {});

const INPUT     = args.input     || args.i;
const OUTPUT    = args.output    || args.o;
const FORMAT    = args.format    || args.f || 'Comprehensive Research Report';
const TIMEOUT   = parseInt(args.timeout || args.t || '900000', 10); // 15 min default
const DRY_RUN   = args['dry-run'] === true;

// ─── Extension Path ──────────────────────────────────────────────────────────

// Resolve ~/.gemini/extensions/gemini-deep-research
// __dirname = .../skills/gemini-deep-research/scripts
// Up 3 levels = .../skills/gemini-deep-research
// Up 4 levels = ~/.openclaw/workspace
// Up 5 levels = ~
const HOME = process.env.HOME || '/home/node';
const EXT_PATH = join(HOME, '.gemini', 'extensions', 'gemini-deep-research');

if (!INPUT) {
  console.error(JSON.stringify({ status: 'error', error: 'Missing --input <research query>' }));
  process.exit(1);
}

const VALID_FORMATS = ['Comprehensive Research Report', 'Executive Brief', 'Technical Deep Dive'];
if (!VALID_FORMATS.includes(FORMAT)) {
  console.error(JSON.stringify({ status: 'error', error: `Invalid --format. Must be one of: ${VALID_FORMATS.join(', ')}` }));
  process.exit(1);
}

// ─── MCP Server Communication ─────────────────────────────────────────────────

let id = 1;

function nextId() {
  return id++;
}

function sendCommand(cmd) {
  return new Promise((resolve, reject) => {
    const line = JSON.stringify(cmd);
    server.stdin.write(line + '\n');
    
    function handler(data) {
      const lines = data.toString().split('\n').filter(l => l.trim());
      for (const l of lines) {
        try {
          const j = JSON.parse(l);
          if (j.id === cmd.id) {
            server.stdout.off('data', handler);
            resolve(j);
            return;
          }
        } catch {}
      }
    }
    
    server.stdout.on('data', handler);
    
    // Timeout for individual commands (15 min max for deep research)
    setTimeout(() => {
      server.stdout.off('data', handler);
      reject(new Error(`Command timeout after 900000ms: ${JSON.stringify(cmd).slice(0, 80)}`));
    }, 900000);
  });
}

// ─── Load .env for MCP server ───────────────────────────────────────────────

function loadEnv(envPath) {
  const env = {};
  try {
    const content = readFileSync(envPath, 'utf8');
    for (const line of content.split('\n')) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('#')) continue;
      const eqIdx = trimmed.indexOf('=');
      if (eqIdx < 0) continue;
      const key = trimmed.slice(0, eqIdx).trim();
      const val = trimmed.slice(eqIdx + 1).trim();
      env[key] = val;
    }
  } catch {}
  return env;
}

const envPath = join(EXT_PATH, '.env');
const extEnv = loadEnv(envPath);

// ─── Spawn MCP Server ─────────────────────────────────────────────────────────

const distPath = join(EXT_PATH, 'dist', 'index.js');

let server;
try {
  server = spawn('node', ['dist/index.js'], {
    cwd: EXT_PATH,
    stdio: ['pipe', 'pipe', 'pipe'],
    env: { ...process.env, ...extEnv }
  });
} catch (err) {
  console.error(JSON.stringify({ status: 'error', error: `Failed to spawn MCP server: ${err.message}` }));
  process.exit(1);
}

server.stderr.on('data', d => {
  process.stderr.write('[MCP STDERR] ' + d.toString());
});

// ─── Main Workflow ────────────────────────────────────────────────────────────

async function main() {
  // 1 ─── Initialize
  process.stderr.write('[DR] Initializing MCP server...\n');
  
  const initResult = await sendCommand({
    jsonrpc: '2.0',
    id: nextId(),
    method: 'initialize',
    params: {
      protocolVersion: '2024-11-05',
      capabilities: {},
      clientInfo: { name: 'gemini-deep-research-client', version: '1.0' }
    }
  });

  if (!initResult.result || !initResult.result.serverInfo) {
    throw new Error('MCP server initialize failed: ' + JSON.stringify(initResult));
  }

  process.stderr.write(`[DR] Server: ${initResult.result.serverInfo.name} v${initResult.result.serverInfo.version}\n`);

  // 2 ─── Start Research
  process.stderr.write(`[DR] Starting research: "${INPUT}"\n`);
  
  const startResult = await sendCommand({
    jsonrpc: '2.0',
    id: nextId(),
    method: 'tools/call',
    params: {
      name: 'research_start',
      arguments: {
        input: INPUT,
        report_format: FORMAT
      }
    }
  });

  if (startResult.isError) {
    throw new Error('research_start error: ' + JSON.stringify(startResult));
  }

  const startText = startResult.result.content[0].text;
  const idMatch = startText.match(/ID:\s*([^\s]+)/);
  const researchId = idMatch ? idMatch[1] : null;

  if (!researchId) {
    throw new Error('Could not extract research ID from response: ' + startText);
  }

  process.stderr.write(`[DR] Research started. ID: ${researchId}\n`);

  // 3 ─── Poll with Exponential Backoff
  const pollIntervals = [0, 15, 30, 60, 120, 180, 240, 300, 360, 420]; // seconds
  let pollIndex = 0;
  let status = 'in_progress';
  const startTime = Date.now();

  while (status === 'in_progress') {
    if (Date.now() - startTime > TIMEOUT) {
      throw new Error(`Research timed out after ${TIMEOUT / 1000}s`);
    }

    const delay = (pollIndex < pollIntervals.length)
      ? pollIntervals[pollIndex] * 1000
      : 60000; // cap at 60s

    if (delay > 0) {
      await new Promise(r => setTimeout(r, delay));
    }

    pollIndex++;
    process.stderr.write(`[DR] Polling status (attempt ${pollIndex})...\n`);

    const statusResult = await sendCommand({
      jsonrpc: '2.0',
      id: nextId(),
      method: 'tools/call',
      params: {
        name: 'research_status',
        arguments: { id: researchId }
      }
    });

    if (statusResult.isError) {
      // Some errors are transient (rate limits etc) - retry
      process.stderr.write(`[DR] Status check error, retrying: ${JSON.stringify(statusResult)}\n`);
      continue;
    }

    const statusText = statusResult.result.content[0].text;
    let statusObj;
    try {
      statusObj = JSON.parse(statusText);
    } catch {
      // Sometimes the response isn't JSON
      process.stderr.write(`[DR] Status response: ${statusText}\n`);
      break;
    }

    status = statusObj.status;
    process.stderr.write(`[DR] Status: ${status}\n`);

    if (status === 'completed') {
      break;
    } else if (status === 'failed') {
      throw new Error(`Research failed: ${statusText}`);
    }
  }

  // 4 ─── Save Report
  if (!OUTPUT) {
    // No output path provided — return result as JSON
    console.log(JSON.stringify({
      status: 'completed',
      researchId,
      format: FORMAT,
      query: INPUT
    }));
    server.kill();
    process.exit(0);
    return;
  }

  process.stderr.write(`[DR] Saving report to: ${OUTPUT}\n`);

  // Ensure output dir exists
  try {
    mkdirSync(dirname(OUTPUT), { recursive: true });
  } catch {}

  const saveResult = await sendCommand({
    jsonrpc: '2.0',
    id: nextId(),
    method: 'tools/call',
    params: {
      name: 'research_save_report',
      arguments: { id: researchId, filePath: OUTPUT }
    }
  });

  if (saveResult.isError) {
    throw new Error('research_save_report error: ' + JSON.stringify(saveResult));
  }

  process.stderr.write(`[DR] Report saved.\n`);

  // 5 ─── Verify
  let savedSize = 0;
  try {
    savedSize = readFileSync(OUTPUT, 'utf8').length;
  } catch {}

  server.kill();

  console.log(JSON.stringify({
    status: 'completed',
    researchId,
    reportPath: OUTPUT,
    format: FORMAT,
    query: INPUT,
    savedBytes: savedSize
  }));

  process.exit(0);
}

// ─── Error Handler ────────────────────────────────────────────────────────────

main().catch(err => {
  server?.kill();
  console.error(JSON.stringify({
    status: 'error',
    researchId: null,
    error: err.message
  }));
  process.exit(1);
});


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)