Skill: 17817942676/stocki (version 0.4.3)

--- name: stocki version: 0.4.2 description: "AI financial analyst with institutional-grade data covering A-shares, HK stocks, US stocks, ETFs, and indices. Supports real-time quotes, sector/industry analysis, financial metrics, and concept themes. For real-time market data and quantitative analysis, use Stocki." homepage: https://github.com/stocki-ai/open-stocki metadata: { "openclaw": { "emoji": "📊", "requires": { "bins": ["python3"], "env": ["STOCKI_GATEWAY_URL", "STOCKI_API_KEY"], "os": ["darwin", "linux"] }, "primaryEnv": "STOCKI_API_KEY" } } --- # Stocki — AI Financial Analyst Professional financial analyst with institutional-grade data. Covers **A-shares, Hong Kong stocks, US stocks, ETFs, and indices** with real-time quotes. Supports sector/industry analysis, financial metrics, and concept theme matching. Two modes: **instant** (Q&A) and **quant** (backtesting, strategy, deep analysis). ## Core Principle **For real-time market data and quantitative analysis, use Stocki.** General financial knowledge questions (e.g. "what is P/E ratio?") can be answered directly without Stocki. Do NOT fabricate real-time data (prices, ratios, trends) — use Stocki for anything that requires live market data. ## Setup ```bash export STOCKI_GATEWAY_URL="https://api.stocki.com.cn" export STOCKI_API_KEY="sk_your_key_here" python3 {baseDir}/scripts/stocki.py diagnose ``` See [INSTALL.md](INSTALL.md) for installation and update methods. `{baseDir}` is resolved automatically by OpenClaw. ## Mode Selection **Default to instant.** Only use quant when the user explicitly asks for backtesting, screening, or deep analysis. | Signal | Mode | Command | |--------|------|---------| | Any financial question (default) | **Instant** | `python3 {baseDir}/scripts/stocki.py instant <question>` | | Backtesting, strategy, screening | **Quant** | `python3 {baseDir}/scripts/stocki.py quant <question>` | | Iterate on existing analysis | **Quant** | `python3 {baseDir}/scripts/stocki.py quant <question> --task-id <id>` | | Scheduled monitoring | **Instant** | `stocki.py instant` on cron | --- ## Instant Mode Call and return output immediately. **No extra processing, no commentary, no reformatting.** ```bash python3 {baseDir}/scripts/stocki.py instant "A股半导体行业前景?" python3 {baseDir}/scripts/stocki.py instant "US tech outlook?" --timezone America/New_York ``` Server maintains persistent context — follow-up questions work. --- ## Quant Mode For complex analysis (minutes to complete). Tasks are auto-created. > Only one quant analysis can run at a time. If busy, retry later. **Submit:** ```bash python3 {baseDir}/scripts/stocki.py quant "回测CSI 300动量策略，近3年数据" # Returns: id, name ``` **Iterate:** ```bash python3 {baseDir}/scripts/stocki.py quant "增加小盘股过滤器" --task-id <id> ``` **Poll status** (every 30s-1min, stay silent until done): ```bash python3 {baseDir}/scripts/stocki.py status <id> ``` **Get results:** ```bash python3 {baseDir}/scripts/stocki.py files <id> python3 {baseDir}/scripts/stocki.py download <id> runs/run_001/report.md ``` Present the **summary** as the main message. Download reports and images as attachments. --- ## Scheduled Monitoring Use `stocki.py instant` on cron. **Do NOT write custom scripts.** ```bash # A-shares + HK: 9-16 Beijing time, weekdays 0 9-16 * * 1-5 python3 {baseDir}/scripts/stocki.py instant "A股和港股有什么重要变化？只报告重大事件" # US stocks: 21:30-04:00 Beijing time 30 21 * * 1-5 python3 {baseDir}/scripts/stocki.py instant "US market: any significant movements? Brief summary only" 0 22-23 * * 1-5 python3 {baseDir}/scripts/stocki.py instant "US market: significant changes in the last hour?" 0 0-4 * * 2-6 python3 {baseDir}/scripts/stocki.py instant "US market: significant changes in the last hour?" ``` Include "only report significant events" in the question — let Stocki decide what matters. --- ## CLI Reference Always use these commands for Stocki interactions. Do NOT call the Gateway API directly. | Command | Usage | Description | |---------|-------|-------------| | `stocki.py instant` | `<question> [--timezone TZ]` | Quick Q&A (180s) | | `stocki.py quant` | `<question> [--task-id ID] [--timezone TZ]` | Submit quant (30s) | | `stocki.py list` | *(no args)* | List analyses (30s) | | `stocki.py status` | `<id>` | Analysis status (120s) | | `stocki.py files` | `<id>` | List result files (120s) | | `stocki.py download` | `<id> <path> [--output]` | Download file (300s) | | `stocki.py diagnose` | *(no args)* | Self-diagnostic (180s) | | `stocki.py doctor` | *(no args)* | Check/fix setup (60s) | Exit: 0=success, 1=auth error, 2=unavailable, 3=rate limited/quota. Invoke as: `python3 {baseDir}/scripts/stocki.py <command> [args]` --- ## Error Handling | Code | Action | |------|--------| | `auth_missing` | Guide: `export STOCKI_API_KEY="sk_..."` and `export STOCKI_GATEWAY_URL="https://api.stocki.com.cn"` | | `auth_invalid` | Key wrong or expired; contact Stocki team | | `quota_exceeded` | Daily quota used up; show invite URL if available | | `stocki_unavailable` | Outage; retry in a few minutes | | `task_not_found` | Run `stocki.py list` to find valid analyses | | `run_error` | Report error verbatim; offer to resubmit | | `rate_limited` | Queue full; wait and retry | --- ## Output Rules - **Instant mode:** Present output directly. No attribution, no processing. - **Quant mode:** Prefix with "以下分析来自Stocki：". Preserve content, clean up formatting for mobile. - **Language:** Respond in user's language. --- ## Local Workspace ``` ~/stocki/ ├── profile.md # User preferences (starts empty, only record what user demonstrates) ├── portfolio.md # Holdings (only with explicit user consent to share) └── quant/ # Downloaded reports and notes per analysis ``` Create on first use: `mkdir -p ~/stocki/quant` **Preferences** (markets, language, timezone) from `profile.md` can be included in queries. **Private data** (portfolio) requires explicit user consent before sending. --- ## Updates **Version: 0.4.2** — Check for updates daily: ```bash clawhub install stocki --force ``` After updating, re-read this SKILL.md. See [INSTALL.md](INSTALL.md) for all methods.

path: INSTALL.md
content: # Stocki Skill — Install & Update Guide

## Install

Try the following methods in order until one succeeds:

### Option 1: ClawHub (Recommended)

```bash
clawhub install stocki --force
```

### Option 2: SkillHub

```bash
skillhub install stocki --force
```

### Option 3: Manual install from GitHub

```bash
rm -rf ~/.openclaw/workspace/skills/stocki
git -c http.postBuffer=524288000 -c http.lowSpeedLimit=0 -c http.lowSpeedTime=300 clone https://github.com/stocki-ai/open-stocki.git ~/.openclaw/workspace/skills/stocki
```

If all methods fail, check the latest install instructions at:
https://github.com/stocki-ai/open-stocki/blob/main/INSTALL.md

## Configure

After installation, set the required environment variables:

```bash
export STOCKI_GATEWAY_URL="https://api.stocki.com.cn"
export STOCKI_API_KEY="sk_your_key_here"
```

To persist across reboots, add the above lines to `~/.bashrc` or `~/.zshrc`.

## Verify

Run the self-diagnostic to confirm the skill works:

```bash
python3 ~/.openclaw/workspace/skills/stocki/scripts/stocki.py diagnose
```

Then run the doctor to check the full setup:

```bash
python3 ~/.openclaw/workspace/skills/stocki/scripts/stocki.py doctor
```

## Update

Stocki is under active development. Update regularly — same methods as installation:

```bash
clawhub install stocki --force
# or
skillhub install stocki --force
```

If all methods fail, check the latest install instructions at:
https://github.com/stocki-ai/open-stocki/blob/main/INSTALL.md


path: README.md
content: # Stocki — AI Financial Analyst for OpenClaw

OpenClaw skill that brings Stocki's professional AI financial analysis to WeChat via ClawBot.

## Features

- **Instant Mode** — Quick financial Q&A: market prices, news, sector outlooks, company analysis
- **Quant Mode** — Complex quantitative analysis: backtesting, strategy modeling, portfolio review
- **Scheduled Monitoring** — Periodic market updates via cron-triggered tasks
- **Doctor** — Self-diagnostics and auto-repair for setup issues
- **Zero Dependencies** — Python stdlib only, no pip install needed

## Install

```bash
clawhub install stocki --force
```

See [INSTALL.md](INSTALL.md) for all installation methods and configuration.

## Quick Start

**Instant Q&A:**
```bash
stocki instant "What's the outlook for US tech stocks?"
```

**Quant Analysis:**
```bash
stocki quant "Backtest CSI 300 momentum strategy"
stocki status <id>
stocki files <id>
stocki download <id> runs/run_001/report.md
```

**Setup Check:**
```bash
stocki doctor
```

## CLI Commands

| Command | Purpose |
|---------|---------|
| `stocki instant` | Quick financial Q&A |
| `stocki quant` | Submit quantitative analysis |
| `stocki list` | List all quant analyses |
| `stocki status` | Show analysis details and run statuses |
| `stocki files` | List result files |
| `stocki download` | Download report or image |
| `stocki diagnose` | Self-diagnostic to verify installation |
| `stocki doctor` | Check and fix setup issues |

## License

Proprietary. All rights reserved.


path: scripts/_gateway.py
content: """
Shared HTTP helper for Stocki OpenClaw scripts.

Reads STOCKI_GATEWAY_URL and STOCKI_API_KEY from environment.
Provides gateway_request(), gateway_request_raw(), handle_error(), and format_for_wechat().
Uses only Python stdlib (urllib, json).
"""

import json
import os
import re
import sys
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


def _env():
    """Return (base_url, api_key) or exit 1 if missing."""
    base_url = os.environ.get("STOCKI_GATEWAY_URL", "").rstrip("/")
    [REDACTED_SECRET]"STOCKI_API_KEY", "")
    missing = []
    if not base_url:
        missing.append("STOCKI_GATEWAY_URL")
    if not [REDACTED_SECRET]"STOCKI_API_KEY")
    if missing:
        print(
            f"Missing environment variable(s): {', '.join(missing)}\n"
            "Set them with:\n"
            '  export STOCKI_GATEWAY_URL="https://api.stocki.com.cn"\n'
            '  export STOCKI_API_KEY="sk_your_key_here"',
            file=sys.stderr,
        )
        sys.exit(1)
    return base_url, api_key


def handle_error(code, message, details=None):
    """Map Gateway error codes to exit codes and print message."""
    if code in ("auth_missing", "auth_invalid"):
        print(f"Auth error: {message}", file=sys.stderr)
        if code == "auth_missing":
            print(
                'Set your API key: export STOCKI_API_KEY="sk_your_key_here"',
                file=sys.stderr,
            )
        sys.exit(1)
    elif code == "quota_exceeded":
        print(f"Quota exceeded: {message}", file=sys.stderr)
        invite = (details or {}).get("invite_url", "")
        if invite:
            print(f"Invite friends to get more quota: {invite}", file=sys.stderr)
        sys.exit(3)
    elif code == "stocki_unavailable":
        print(f"Stocki unavailable: {message}", file=sys.stderr)
        sys.exit(2)
    elif code == "rate_limited":
        retry = (details or {}).get("retry_after", "")
        msg = f"Rate limited: {message}"
        if retry:
            msg += f" (retry after {retry}s)"
        print(msg, file=sys.stderr)
        sys.exit(3)
    else:
        print(f"Error [{code}]: {message}", file=sys.stderr)
        sys.exit(1)


def _handle_http_error(e):
    """Parse HTTP error response and call handle_error."""
    try:
        err_body = json.loads(e.read().decode())
        handle_error(
            err_body.get("error", "unknown"),
            err_body.get("message", str(e)),
            err_body.get("details"),
        )
    except (json.JSONDecodeError, UnicodeDecodeError):
        print(f"HTTP {e.code}: {e.reason}", file=sys.stderr)
        sys.exit(2 if e.code >= 500 else 1)


def gateway_request(method, path, body=None, timeout=120):
    """Make an HTTP request to the Gateway. Returns parsed JSON dict."""
    base_url, api_key = _env()
    url = f"{base_url}{path}"
    headers = {"Authorization": f"Bearer {api_key}"}
    if body is not None:
        headers["Content-Type"] = "application/json"
    data = json.dumps(body).encode() if body is not None else None
    req = Request(url, data=data, headers=headers, method=method)

    try:
        with urlopen(req, timeout=timeout) as resp:
            raw = resp.read().decode()
            return json.loads(raw) if raw else {}
    except HTTPError as e:
        _handle_http_error(e)
    except (URLError, TimeoutError, OSError) as e:
        print(f"Stocki unavailable: {e}", file=sys.stderr)
        sys.exit(2)


def gateway_request_raw(method, path, timeout=120):
    """Make an HTTP request to the Gateway. Returns raw bytes and content-type."""
    base_url, api_key = _env()
    url = f"{base_url}{path}"
    headers = {"Authorization": f"Bearer {api_key}"}
    req = Request(url, headers=headers, method=method)

    try:
        with urlopen(req, timeout=timeout) as resp:
            content_type = resp.headers.get("Content-Type", "")
            raw = resp.read()
            return raw, content_type
    except HTTPError as e:
        _handle_http_error(e)
    except (URLError, TimeoutError, OSError) as e:
        print(f"Stocki unavailable: {e}", file=sys.stderr)
        sys.exit(2)


def format_for_wechat(text):
    """Convert Stocki markdown output to WeChat-friendly plain text."""
    # 1. <stockidata> -> bracket highlights
    text = re.sub(r"<stockidata>(.*?)</stockidata>", r"[\1]", text)

    # 2. Collect inline markdown links as footnotes
    links = []

    def _replace_link(m):
        _label, url = m.group(1), m.group(2)
        links.append(url)
        return f"[{len(links)}]"

    text = re.sub(r"\[+([^\]]*)\]+\((https?://[^\)]+)\)", _replace_link, text)

    # 3. Strip remaining HTML tags
    text = re.sub(r"<[^>]+>", "", text)

    # 4. Simplify markdown formatting
    text = re.sub(r"^#{1,6}\s+", "", text, flags=re.MULTILINE)
    text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
    text = re.sub(r"\*(.+?)\*", r"\1", text)
    text = re.sub(r"^[\-\*]\s+", "· ", text, flags=re.MULTILINE)

    # 5. Append footnote references
    if links:
        text = text.rstrip() + "\n\n---\n"
        for i, url in enumerate(links, 1):
            text += f"[{i}] {url}\n"

    return text


path: scripts/stocki.py
content: #!/usr/bin/env python3
"""Stocki CLI — AI Financial Analyst"""

import argparse
import hashlib
import os
import re
import subprocess
import sys
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _gateway import (
    format_for_wechat,
    gateway_request,
    gateway_request_raw,
)


# --- instant ---

def cmd_instant(args):
    result = gateway_request(
        "POST", "/v1/instant",
        {"question": args.question, "timezone": args.timezone},
        timeout=180,
    )
    answer = result.get("answer", "")
    if not answer:
        print("No answer returned.", file=sys.stderr)
        sys.exit(1)
    print(format_for_wechat(answer))


# --- quant ---

def cmd_quant(args):
    body = {"question": args.question, "timezone": args.timezone}
    if args.task_id:
        body["task_id"] = args.task_id
    result = gateway_request("POST", "/v1/quant", body, timeout=30)
    print(f"task_id: {result.get('task_id', '')}")
    print(f"task_name: {result.get('task_name', '')}")


# --- tasks ---

def cmd_tasks(_args):
    result = gateway_request("GET", "/v1/tasks", timeout=30)
    tasks = result.get("tasks", [])
    if not tasks:
        print("No tasks found.")
        return
    print(f"{'Name':<40} {'Task ID':<16} {'Updated':<24} {'Msgs':>4}")
    print("-" * 88)
    for t in tasks:
        name = t.get("name", "")[:38]
        tid = t.get("task_id", "")
        updated = t.get("updated_at", "")[:19]
        msgs = t.get("message_count", 0)
        print(f"{name:<40} {tid:<16} {updated:<24} {msgs:>4}")


# --- status ---

def cmd_status(args):
    result = gateway_request("GET", f"/v1/tasks/{args.task_id}", timeout=120)
    name = result.get("name", "")
    print(f"Task: {name} ({args.task_id})")

    current = result.get("current_run")
    if current:
        print(f"\nCurrent run: {current.get('run_id', '')} [{current.get('status', '')}]")
        print(f"  Query: {current.get('query', '')}")

    runs = result.get("runs", [])
    if runs:
        print(f"\nRuns ({len(runs)}):")
        for r in runs:
            status = r.get("status", "")
            rid = r.get("run_id", "")
            query = r.get("query", "")[:60]
            summary = r.get("summary") or ""
            err = r.get("error_message") or ""
            print(f"  [{status}] {rid}: {query}")
            if summary:
                print(f"    Summary: {summary[:120]}")
            if err:
                print(f"    Error: {err}")
            files = r.get("files", [])
            if files:
                print(f"    Files: {', '.join(files)}")


# --- files ---

def cmd_files(args):
    result = gateway_request("GET", f"/v1/tasks/{args.task_id}", timeout=120)
    runs = result.get("runs", [])
    if not runs:
        print("No runs found.")
        return
    for r in runs:
        rid = r.get("run_id", "")
        status = r.get("status", "")
        files = r.get("files", [])
        if files:
            print(f"[{status}] {rid}:")
            for f in files:
                print(f"  {f}")
        else:
            print(f"[{status}] {rid}: (no files)")


# --- download ---

def cmd_download(args):
    raw, content_type = gateway_request_raw(
        "GET",
        f"/v1/tasks/{args.task_id}/files/{args.file_path}",
        timeout=300,
    )
    output = args.output or os.path.basename(args.file_path)
    # Sanitize output path to prevent writing outside current directory
    if os.path.isabs(output) or ".." in output:
        output = os.path.basename(output)
    mode = "wb" if "image" in content_type else "w"
    with open(output, mode) as f:
        if mode == "wb":
            f.write(raw)
        else:
            f.write(raw.decode("utf-8"))
    print(f"Saved: {output} ({len(raw)} bytes)")


# --- diagnose ---

def cmd_diagnose(_args):
    print("Stocki Self-Diagnostic")
    print("=" * 40)

    passed = 0
    total = 1

    if _check_instant():
        passed += 1

    print("=" * 40)
    print(f"Result: {passed}/{total} passed")
    sys.exit(0 if passed == total else 1)


def _check_instant():
    print("[1/1] Instant mode...", end=" ", flush=True)
    try:
        result = gateway_request(
            "POST", "/v1/instant",
            {"question": "What is the ticker symbol of Apple Inc on NASDAQ?", "timezone": "Asia/Shanghai"},
            timeout=180,
        )
        answer = result.get("answer", "")
        if not answer:
            print("FAIL (empty answer)")
            return False
        if "AAPL" in answer.upper():
            print(f"OK (verified AAPL, {len(answer)} chars)")
            return True
        else:
            print("FAIL (answer does not contain 'AAPL')")
            print(f"  Answer: {answer[:200]}...")
            return False
    except SystemExit:
        print("FAIL")
        return False


# --- doctor ---

GITHUB_SKILL_URL = "https://raw.githubusercontent.com/stocki-ai/open-stocki/main/SKILL.md"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SKILL_DIR = os.path.dirname(SCRIPT_DIR)


def _get_local_version():
    """Read version from local SKILL.md frontmatter."""
    skill_md = os.path.join(SKILL_DIR, "SKILL.md")
    if not os.path.isfile(skill_md):
        return None
    with open(skill_md, "r") as f:
        for line in f:
            if line.startswith("version:"):
                return line.split(":", 1)[1].strip().strip('"').strip("'")
    return None


def _get_remote_version():
    """Fetch latest version from GitHub."""
    try:
        req = Request(GITHUB_SKILL_URL, method="GET")
        with urlopen(req, timeout=15) as resp:
            for line in resp.read().decode().splitlines():
                if line.startswith("version:"):
                    return line.split(":", 1)[1].strip().strip('"').strip("'")
    except Exception:
        return None
    return None


def _sha256(filepath):
    """Compute SHA256 of a file."""
    h = hashlib.sha256()
    with open(filepath, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()


def _reinstall_skill():
    """Try to reinstall skill via clawhub, then GitHub."""
    print("      Reinstalling skill...")
    try:
        r = subprocess.run(["clawhub", "install", "stocki", "--force"],
                           capture_output=True, text=True, timeout=30)
        if r.returncode == 0:
            print("      -> Reinstalled via clawhub")
            return True
    except Exception:
        pass
    # Validate SKILL_DIR before rm -rf
    if "stocki" not in os.path.basename(SKILL_DIR):
        print("      -> Reinstall aborted: unexpected skill directory path")
        return False
    try:
        subprocess.run(["rm", "-rf", SKILL_DIR], check=True)
        r = subprocess.run(
            ["git", "-c", "http.postBuffer=524288000", "-c", "http.lowSpeedLimit=0",
             "-c", "http.lowSpeedTime=300", "clone",
             "https://github.com/stocki-ai/open-stocki.git", SKILL_DIR],
            capture_output=True, text=True, timeout=120)
        if r.returncode == 0:
            print("      -> Reinstalled from GitHub")
            return True
    except Exception:
        pass
    print("      -> Reinstall failed. See INSTALL.md for manual instructions.")
    return False


def cmd_doctor(_args):
    print("Stocki Doctor")
    print("=" * 48)

    ok = 0
    fixed = 0
    failed = 0
    total = 6

    # 1. API Key
    [REDACTED_SECRET]"STOCKI_API_KEY", "")
    if not [REDACTED_SECRET]"[1/6] API Key.............. FAIL")
        print("      No API key set. Register at:")
        print("      https://api.stocki.com.cn/wechat")
        print('      Then: export STOCKI_API_KEY="sk_your_key_here"')
        failed += 1
    elif not api_key.startswith("sk_"):
        print(f"[1/6] API Key.............. WARN (invalid format: {api_key[:10]}...)")
        print("      API key should start with 'sk_'")
        failed += 1
    else:
        masked = api_key[:3] + "..." + api_key[-2:]
        print(f"[1/6] API Key.............. OK ({masked})")
        ok += 1

    # 2. Gateway URL
    gw_url = os.environ.get("STOCKI_GATEWAY_URL", "")
    if not gw_url:
        print("[2/6] Gateway URL.......... FAIL")
        print('      export STOCKI_GATEWAY_URL="https://api.stocki.com.cn"')
        failed += 1
    else:
        print(f"[2/6] Gateway URL.......... OK ({gw_url})")
        ok += 1

    # 3. API Connectivity
    if api_key and gw_url:
        try:
            result = gateway_request(
                "POST", "/v1/instant",
                {"question": "ping", "timezone": "Asia/Shanghai"},
                timeout=30,
            )
            if result.get("answer"):
                print("[3/6] API Connectivity..... OK")
                ok += 1
            else:
                print("[3/6] API Connectivity..... WARN (empty response)")
                ok += 1
        except SystemExit:
            print("[3/6] API Connectivity..... FAIL (see error above)")
            failed += 1
    else:
        print("[3/6] API Connectivity..... SKIP (no credentials)")
        failed += 1

    # 4. Skill Version
    local_ver = _get_local_version()
    remote_ver = _get_remote_version()
    if not local_ver:
        print("[4/6] Skill Version........ FAIL (cannot read local version)")
        failed += 1
    elif not remote_ver:
        print(f"[4/6] Skill Version........ WARN (v{local_ver}, cannot check remote)")
        ok += 1
    elif local_ver == remote_ver:
        print(f"[4/6] Skill Version........ OK (v{local_ver}, up to date)")
        ok += 1
    else:
        print(f"[4/6] Skill Version........ WARN (v{local_ver} -> v{remote_ver} available)")
        if _reinstall_skill():
            fixed += 1
        else:
            failed += 1

    # 5. File Integrity
    checksum_file = os.path.join(SCRIPT_DIR, "checksums.sha256")
    if not os.path.isfile(checksum_file):
        print("[5/6] File Integrity....... WARN (no checksums file)")
        ok += 1
    else:
        integrity_ok = True
        with open(checksum_file) as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                parts = line.split()
                if len(parts) != 2:
                    continue
                expected_hash, filename = parts
                filepath = os.path.join(SCRIPT_DIR, filename)
                if not os.path.isfile(filepath):
                    print(f"[5/6] File Integrity....... FAIL ({filename} missing)")
                    integrity_ok = False
                    break
                actual_hash = _sha256(filepath)
                if actual_hash != expected_hash:
                    print(f"[5/6] File Integrity....... FAIL ({filename} modified)")
                    integrity_ok = False
                    break
        if integrity_ok:
            print("[5/6] File Integrity....... OK")
            ok += 1
        else:
            print("      Files have been modified or corrupted.")
            if _reinstall_skill():
                fixed += 1
            else:
                failed += 1

    # 6. Workspace
    home = os.path.expanduser("~")
    workspace = os.path.join(home, "stocki")
    quant_dir = os.path.join(workspace, "quant")
    profile = os.path.join(workspace, "profile.md")
    dirs_ok = os.path.isdir(workspace) and os.path.isdir(quant_dir)
    if dirs_ok:
        print("[6/6] Workspace............ OK")
        ok += 1
    else:
        print("[6/6] Workspace............ WARN (missing directories)")
        os.makedirs(quant_dir, exist_ok=True)
        if not os.path.isfile(profile):
            open(profile, "a").close()
        print("      -> Created ~/stocki/quant/ and ~/stocki/profile.md")
        fixed += 1

    # Summary
    print("=" * 48)
    print(f"Result: {ok} OK, {fixed} fixed, {failed} failed")
    sys.exit(0 if failed == 0 else 1)


# --- main ---

def main():
    parser = argparse.ArgumentParser(prog="stocki", description="Stocki — AI Financial Analyst CLI")
    sub = parser.add_subparsers(dest="command", required=True)

    p = sub.add_parser("instant", help="Quick financial Q&A")
    p.add_argument("question", help="The question to ask")
    p.add_argument("--timezone", default="Asia/Shanghai", help="IANA timezone (default: Asia/Shanghai)")

    p = sub.add_parser("quant", help="Submit quantitative analysis")
    p.add_argument("question", help="Analysis question")
    p.add_argument("--task-id", default=None, help="Existing task ID for iteration")
    p.add_argument("--timezone", default="Asia/Shanghai", help="IANA timezone (default: Asia/Shanghai)")

    sub.add_parser("list", help="List all quant analyses")

    p = sub.add_parser("status", help="Show task details and run statuses")
    p.add_argument("task_id", help="Task ID")

    p = sub.add_parser("files", help="List result files for a task")
    p.add_argument("task_id", help="Task ID")

    p = sub.add_parser("download", help="Download a result file")
    p.add_argument("task_id", help="Task ID")
    p.add_argument("file_path", help="File path (e.g. runs/run_001/report.md)")
    p.add_argument("--output", help="Local output path (default: basename)")

    sub.add_parser("diagnose", help="Self-diagnostic: verify instant mode")

    sub.add_parser("doctor", help="Check and fix setup issues")

    args = parser.parse_args()
    {
        "instant": cmd_instant,
        "quant": cmd_quant,
        "list": cmd_tasks,
        "status": cmd_status,
        "files": cmd_files,
        "download": cmd_download,
        "diagnose": cmd_diagnose,
        "doctor": cmd_doctor,
    }[args.command](args)


if __name__ == "__main__":
    main()


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