IMA Copilot One-command installer, troubleshooter, and personalization layer for the official Tencent IMA skill. Overview The official Tencent IMA skill (ima-skill) exposes a powerful OpenAPI for notes and knowledge base operations, but its installation flow is designed for a specific proprietary agent and recent releases have shipped submodule files that fail strict SKILL.md loaders. IMA Copilot solves both problems: 1. Installs ima-skill to Claude Code, Codex, and OpenClaw in a single command via the vercel-labs/skills open installer. 2. Walks the user through API key setup with a live vali…

; then\n echo \"already has frontmatter: $file\"\n return 0\n fi\n local tmp\n tmp=$(mktemp)\n {\n printf -- '---\\n'\n printf -- 'name: %s\\n' \"$name\"\n printf -- 'description: %s\\n' \"$desc\"\n printf -- '---\\n\\n'\n cat \"$file\"\n } > \"$tmp\"\n command mv \"$tmp\" \"$file\"\n}\n\nprepend_frontmatter \\\n \"\u003cinstall>/notes/SKILL.md\" \\\n \"ima-skill-notes\" \\\n \"IMA notes submodule. Read via the root ima-skill module decision table.\"\n\nprepend_frontmatter \\\n \"\u003cinstall>/knowledge-base/SKILL.md\" \\\n \"ima-skill-knowledge-base\" \\\n \"IMA knowledge-base submodule. Read via the root ima-skill module decision table.\"\n```\n\n**Rollback**:\n\n```bash\ncommand cp \"$BACKUP/notes-SKILL.md\" \"\u003cinstall>/notes/SKILL.md\"\ncommand cp \"$BACKUP/knowledge-base-SKILL.md\" \"\u003cinstall>/knowledge-base/SKILL.md\"\n```\n\n**Pros**: Smallest possible diff. Exact commands that Codex's first-pass fix used. Easiest to recreate from memory if the user loses the script.\n\n**Cons**: Creates two new skill identifiers in the loader's registry. On some agents, those names become visible as separate skills in menus, which can confuse users and — in the worst case — accidentally trigger the submodule on its own instead of going through the root ima-skill flow.\n\n#### Strategy skip — Leave the file alone\n\nValid when the user is only running on Claude Code and does not care about the startup warning (which is typically invisible without log inspection). Not recommended if the user ever runs the same install on Codex.\n\n## Adding new issues to this file\n\nWhen we discover a new upstream bug:\n\n1. Assign the next sequential `ISSUE-\u003cNNN>` number.\n2. Fill in the same sections: symptom, root cause, impact, plain-language explanation, at least one strategy with idempotent + reversible commands.\n3. Update `scripts/diagnose.sh` to detect it (still read-only) and print a line with the same issue ID.\n4. **Do not** add the fix commands into any shipped script — keep them in this file so the agent reads and executes them at runtime under user consent. This preserves the contract: we ship instructions, not patches.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":11145,"content_sha256":"87a2d281376b2331d9e74fb09b0694f821df90182fec849afcfd08bc04823861"},{"filename":"references/search_best_practices.md","content":"# Search Best Practices — Deep Dive\n\nThis document is the reference for Capability 4 (fan-out search with personalization). Read it when the user asks about how to search, reports weird search results, or wants to tune the `copilot.json` configuration.\n\n## The three hard constraints of the IMA search API\n\nAny search workflow on top of IMA has to account for these three constraints. They are not documented by upstream; they were discovered by observation.\n\n### 1. No cross-knowledge-base endpoint\n\n`search_knowledge` requires `knowledge_base_id` to be set. There is no endpoint that searches across all KBs at once. The only way to find content in all your KBs is a client-side fan-out: enumerate your KBs first, then call `search_knowledge` once per KB.\n\n### 2. No relevance score in the response\n\nA hit object looks like this:\n\n```json\n{\n \"media_id\": \"wechatarticle_…\",\n \"title\": \"…\",\n \"parent_folder_id\": \"…\",\n \"highlight_content\": \"…\",\n \"media_type\": 6\n}\n```\n\nThat's it. No similarity score, no BM25 rank, no recency weight, nothing that lets a client know \"this hit is more relevant than that hit\". Hits come back in an undocumented order that is believed to be insertion-order or last-modified order, but it cannot be trusted as a relevance ranking.\n\n**Consequence**: any ranking beyond \"here are the hits the server returned in the order it returned them\" must be invented by the client. This wrapper does not attempt cross-KB relevance ranking — it only groups by KB with user-declared priority.\n\n### 3. Silent 100-result truncation\n\n`search_knowledge` returns at most 100 hits per call. When the query saturates a KB, the response comes back with `info_list` of length exactly 100 and **no** `is_end` or `next_cursor` field. The API documentation mentions a `cursor` parameter in the request, but the server does not emit a cursor in the response, so pagination is impossible in practice.\n\nHigh-frequency queries against large KBs (e.g., searching \"AI\" across a 25,000-entry KB) return the \"first 100\" without any indication that more exist.\n\n**Detection rule**: a response with exactly 100 hits and no `is_end`/`next_cursor` is treated as truncated. `search_fanout.py` uses this rule and surfaces a warning listing the truncated KBs.\n\n**Mitigation**: the only workaround is to narrow the query — add a second term, a phrase match, or a distinctive keyword that reduces the candidate set below 100 per KB.\n\n## Permission model: subscribed KBs return 'no permission'\n\nEmpirically, the IMA OpenAPI search endpoints only work on KBs the user **created themselves**. KBs the user subscribed to (e.g., public curated libraries, friends' shared knowledge bases) enumerate successfully via `search_knowledge_base` (so they show up in the fan-out target list) but return `code: 220030, msg: 没有权限` when hit with `search_knowledge`.\n\nThis is not configurable on the client side — it is a server-side entitlement check tied to KB ownership.\n\n**Consequence for the wrapper**:\n- Do not hide denied KBs from the fan-out call list entirely — the user needs to know which ones they could search if they forked a copy.\n- Do not render denied KBs in the main results area — they are noise that drowns out real hits.\n- Collect them in a separate \"ℹ️ subscribed KBs (no search permission)\" block at the end of the output.\n\n`search_fanout.py` implements this partitioning in its `rank_groups()` function using the `220030` error code as the partition key.\n\n## The fan-out strategy\n\n```\nload credentials\nload ~/.config/ima/copilot.json (optional)\nenumerate all KBs via search_knowledge_base(\"\")\nfilter out KBs in skip_kbs\nfan out search_knowledge to the remainder, in parallel (default 12 workers)\npartition results into: priority | others | denied | empty\nrender:\n priority group first, in user-declared order\n others group next, sorted by hit count descending\n summary line\n denied group at the bottom (as an ℹ️ note, not a result)\n truncated warning (if any)\n```\n\nEvery step after credential load is stateless — rerunning the script with the same query produces the same output, modulo rare eventual-consistency windows on newly added content.\n\n## The `copilot.json` configuration file\n\nLocation: `~/.config/ima/copilot.json`. Override with `IMA_COPILOT_CONFIG=\u003cpath>` environment variable (primarily used by tests).\n\nShape:\n\n```json\n{\n \"priority_kbs\": [\"kb name 1\", \"kb name 2\"],\n \"skip_kbs\": [\"kb name 3\"],\n \"fanout_strategy\": \"parallel-then-merge\"\n}\n```\n\n### `priority_kbs` (list of strings)\n\nKB names that should be surfaced at the top of every search. Order within the list is preserved — the first entry becomes the first priority group, the second becomes the second, and so on. KBs that appear here but have no hits for the current query are silently omitted from the priority section.\n\n**Intent**: surface the user's trusted / curated / high-signal KBs ahead of noisy or exploratory ones. A good rule of thumb is \"KBs I've personally vetted\" in priority, \"KBs I added but haven't fully read\" in unranked others.\n\n**Naming**: must match the KB name exactly, including spacing and Unicode. If the user's config uses a name that no KB has, it is silently ignored.\n\n### `skip_kbs` (list of strings)\n\nKB names to exclude from the fan-out entirely. They are not searched, they don't appear in the denied block, they don't appear in the results. They *are* counted in the \"Searched across N knowledge bases\" header along with a `skipped via config: …` line.\n\n**Intent 1 — strict subsets**: if KB B is a strict subset of KB A (every document in B also appears in A), searching both produces duplicate hits. Skipping B eliminates the duplication without losing any content.\n\n**Intent 2 — off-topic noise**: some KBs never contain anything relevant to the user's searches (e.g., a parked KB they created for a different project). Skipping saves a round trip and reduces output clutter.\n\n### `fanout_strategy` (string, reserved)\n\nCurrently only `\"parallel-then-merge\"` is implemented. The field is kept in the schema for forward compatibility.\n\n## Evidence-based subset detection\n\nBefore adding a KB to `skip_kbs` as a strict subset of another, verify the subset relationship with multiple queries. Relying on hit counts alone is unsafe because of the 100-hit truncation: a KB that returns 100 hits on query X with 30 \"independent\" titles may actually be a strict subset, with the difference being a truncation artifact rather than real extra content.\n\n**Verification procedure**:\n\n1. Pick 2–3 queries expected to return strictly less than 100 hits in both KBs. \"RAG\", \"MCP\", \"embedding\" are good candidates for technical KBs — narrow enough to avoid truncation, common enough to return non-trivial result sets.\n2. For each query, run the two KBs separately via `search_knowledge` and collect the set of `title` values from each.\n3. Compute `set(B) - set(A)`. If this is consistently empty across all queries, B is (probably) a subset of A. If any difference persists, they are not in a subset relationship.\n4. If truncated results show up (exactly 100 hits), discard that query — the set difference will be a truncation artifact, not a real content difference.\n\nThe rationale is that querying at most ~100 hits is cheap (3–4 API calls) and any genuine subset relationship will be visible with just a few narrow queries, whereas high-frequency queries will mislead. See the conversation history around this skill's creation for a worked example on the `personal-kb` vs `master-kb` pair.\n\n## Rendering details\n\nText mode (the default):\n\n- Each KB group is a header line with an emoji (`🥇` for priority, `📚` for others) and a hit count.\n- The first `--max-results` hits per KB are listed with title and highlight snippet (truncated to 120 chars).\n- \"N more\" is printed if hits exceed the limit.\n- A separator line precedes the summary.\n- The summary shows total hits and total KBs with results.\n- Denied KBs are printed as an `ℹ️` block at the bottom so they never drown out real results.\n- Truncated KBs are printed as a `⚠️` block with guidance to narrow the query.\n\nJSON mode (`--json`): emits `{priority, others, denied, skipped_by_config}` arrays with full hit metadata for downstream tools.\n\n## When the agent should use this capability\n\nTrigger on explicit search intents:\n- \"搜一下 XXX\"\n- \"search for XXX in my IMA notes\"\n- \"find articles about XXX\"\n- \"在 ima 里搜 XXX\"\n- \"知识库里有没有 XXX\"\n\nAlso trigger on implicit intents when the user is asking a question whose answer the user is likely to have previously saved to their knowledge base:\n- \"what was that RAG framework the author of the HyDE paper mentioned?\"\n- \"I read something last month about Qwen3 fine-tuning, what was the key takeaway?\"\n\nFor these, run a fan-out search first with the key nouns, show the top priority-group hits, and let the user ask follow-ups based on what's returned.\n\n## When the agent should refuse\n\nRefuse (or at least suggest alternatives) when the user asks to:\n- Fuzzy-search across all KBs with a single vague word — this will likely truncate and give poor results. Suggest narrower queries first.\n- Rank results by recency — the API doesn't return timestamps; any such ranking would be a lie.\n- Deduplicate across KBs by semantic similarity — the hits only carry titles and snippets; full deduplication needs a separate embedding step that this skill does not implement.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":9464,"content_sha256":"1b5754d7188107f2bd470a57cf22e092b711767374eb6c1f15d125df1c238f19"},{"filename":"scripts/diagnose.sh","content":"#!/usr/bin/env bash\n#\n# diagnose.sh — Read-only health check for upstream ima-skill installs.\n#\n# Prints one status line per check, then a summary.\n#\n# Exit codes:\n# 0 — all checks passed\n# 1 — one or more issues need user action\n# 2 — diagnostic itself failed (network error, missing tooling)\n#\n# This script is strictly read-only. It will never modify, create, or delete\n# any file outside its own stdout. Safe to run as many times as you want.\n\nset -uo pipefail\n\nPASS=0\nWARN=0\nFAIL=0\n\nstatus_ok() { echo \"✅ $1\"; PASS=$((PASS + 1)); }\nstatus_warn() { echo \"⚠️ $1\"; WARN=$((WARN + 1)); }\nstatus_fail() { echo \"❌ $1\"; FAIL=$((FAIL + 1)); }\n\necho \"=== ima-copilot diagnostic report ===\"\necho\n\n# ==========================================================================\n# 1. Upstream ima-skill install presence\n# ==========================================================================\n\necho \"--- Upstream ima-skill installs ---\"\n\n# Agent target path resolution. Each agent has a short list of known\n# candidate install paths; the first one with a SKILL.md wins.\nfind_install() {\n local agent=\"$1\"; shift\n local path\n for path in \"$@\"; do\n if [ -f \"$path/SKILL.md\" ]; then\n echo \"$path\"\n return 0\n fi\n done\n return 1\n}\n\n# Resolve a path to its canonical realpath so we can detect when two agent\n# entries point at the same underlying directory via symlink. `npx skills add`\n# in its default mode promotes the first agent's install to canonical and\n# symlinks the rest to it — reporting issues four times when there are only\n# two real files is noisy and confuses the repair step counting.\ncanonical() {\n python3 -c \"import os,sys; print(os.path.realpath(sys.argv[1]))\" \"$1\" 2>/dev/null || echo \"$1\"\n}\n\nCLAUDE_PATH=\"\"\nCODEX_PATH=\"\"\nOPENCLAW_PATH=\"\"\nINSTALLED_AGENTS=\"\"\n\n# Claude Code\nif CLAUDE_PATH=$(find_install claude-code \\\n \"$HOME/.claude/skills/ima-skill\"); then\n status_ok \"ima-skill installed (claude-code) at $CLAUDE_PATH\"\n INSTALLED_AGENTS=\"$INSTALLED_AGENTS claude-code\"\nelse\n status_warn \"ima-skill NOT installed (claude-code) — run install_ima_skill.sh\"\nfi\n\n# Codex\nif CODEX_PATH=$(find_install codex \\\n \"$HOME/.agents/skills/ima-skill\" \\\n \"$HOME/.codex/skills/ima-skill\"); then\n status_ok \"ima-skill installed (codex) at $CODEX_PATH\"\n INSTALLED_AGENTS=\"$INSTALLED_AGENTS codex\"\nelse\n status_warn \"ima-skill NOT installed (codex) — run install_ima_skill.sh\"\nfi\n\n# OpenClaw — multiple candidate paths because the standard hasn't stabilized\nif OPENCLAW_PATH=$(find_install openclaw \\\n \"$HOME/.openclaw/skills/ima-skill\" \\\n \"$HOME/.config/openclaw/skills/ima-skill\" \\\n \"$HOME/.local/share/openclaw/skills/ima-skill\"); then\n status_ok \"ima-skill installed (openclaw) at $OPENCLAW_PATH\"\n INSTALLED_AGENTS=\"$INSTALLED_AGENTS openclaw\"\nelse\n status_warn \"ima-skill NOT installed (openclaw) — run install_ima_skill.sh\"\nfi\n\n# Detect whether multiple agents share the same underlying directory via\n# symlink. This matters for the issue scanner: we don't want to report the\n# same ISSUE-001 four times when there are really only two files behind\n# symlinks.\nCLAUDE_REAL=\"\"\nCODEX_REAL=\"\"\nOPENCLAW_REAL=\"\"\n[ -n \"$CLAUDE_PATH\" ] && CLAUDE_REAL=$(canonical \"$CLAUDE_PATH\")\n[ -n \"$CODEX_PATH\" ] && CODEX_REAL=$(canonical \"$CODEX_PATH\")\n[ -n \"$OPENCLAW_PATH\" ] && OPENCLAW_REAL=$(canonical \"$OPENCLAW_PATH\")\n\n# Report sharing if any two agents resolve to the same canonical directory\nif [ -n \"$CLAUDE_REAL\" ] && [ -n \"$CODEX_REAL\" ] && [ \"$CLAUDE_REAL\" = \"$CODEX_REAL\" ]; then\n echo \"ℹ️ claude-code and codex share the same install via symlink (canonical: $CLAUDE_REAL)\"\nfi\nif [ -n \"$CLAUDE_REAL\" ] && [ -n \"$OPENCLAW_REAL\" ] && [ \"$CLAUDE_REAL\" = \"$OPENCLAW_REAL\" ]; then\n echo \"ℹ️ claude-code and openclaw share the same install via symlink (canonical: $CLAUDE_REAL)\"\nfi\nif [ -n \"$CODEX_REAL\" ] && [ -n \"$OPENCLAW_REAL\" ] && [ \"$CODEX_REAL\" = \"$OPENCLAW_REAL\" ] && [ \"$CODEX_REAL\" != \"$CLAUDE_REAL\" ]; then\n echo \"ℹ️ codex and openclaw share the same install via symlink (canonical: $CODEX_REAL)\"\nfi\n\nif [ -z \"$INSTALLED_AGENTS\" ]; then\n echo\n echo \"No installs found across any supported agent.\"\n echo \"Start with: bash \\\"\\$(dirname \\\"\\$0\\\")/install_ima_skill.sh\\\"\"\n exit 1\nfi\n\necho\n\n# ==========================================================================\n# 2. API credentials presence and liveness\n# ==========================================================================\n\necho \"--- API credentials ---\"\n\nCLIENT_ID=\"${IMA_OPENAPI_CLIENTID:-}\"\nAPI_KEY=\"${IMA_OPENAPI_APIKEY:-}\"\n\nif [ -z \"$CLIENT_ID\" ] && [ -f \"$HOME/.config/ima/client_id\" ]; then\n CLIENT_ID=$(tr -d '\\n' \u003c \"$HOME/.config/ima/client_id\")\nfi\nif [ -z \"$API_KEY\" ] && [ -f \"$HOME/.config/ima/api_key\" ]; then\n API_KEY=$(tr -d '\\n' \u003c \"$HOME/.config/ima/api_key\")\nfi\n\nif [ -z \"$CLIENT_ID\" ] || [ -z \"$API_KEY\" ]; then\n status_fail \"API credentials missing (expected env vars or ~/.config/ima/{client_id,api_key})\"\nelse\n status_ok \"API credentials present\"\n\n if ! command -v curl >/dev/null 2>&1; then\n status_warn \"curl not on PATH — skipping liveness check\"\n else\n response=$(curl -sS -X POST \"https://ima.qq.com/openapi/wiki/v1/search_knowledge_base\" \\\n -H \"ima-openapi-clientid: $CLIENT_ID\" \\\n -H \"ima-openapi-apikey: $API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"query\": \"\", \"cursor\": \"\", \"limit\": 1}' 2>/dev/null || true)\n\n if echo \"$response\" | grep -q '\"code\"[[:space:]]*:[[:space:]]*0'; then\n status_ok \"API credentials verified by live liveness call\"\n elif [ -z \"$response\" ]; then\n status_fail \"API liveness call returned no response (network issue?)\"\n else\n status_fail \"API liveness call failed — server rejected credentials or returned error\"\n # Show a short snippet for debugging without dumping everything\n snippet=$(printf '%s' \"$response\" | head -c 200)\n echo \" response: $snippet\"\n fi\n fi\nfi\n\necho\n\n# ==========================================================================\n# 3. Known issue scan\n# ==========================================================================\n\necho \"--- Known upstream issues ---\"\n\n# ISSUE-001 — submodule SKILL.md missing YAML frontmatter\n#\n# Symptom: loaders like Codex's ~/.agents scanner skip the submodule SKILL.md\n# files and log \"missing YAML frontmatter delimited by ---\". Claude Code is\n# more lenient and usually loads them anyway, but the official design intent\n# is still that these files are module documentation, and fixing them removes\n# the loader warning universally.\n#\n# The check has to understand three post-install states:\n# - Untouched upstream: SKILL.md exists, starts with \"#\" (broken) or \"---\" (fixed upstream).\n# - Strategy A applied: SKILL.md is renamed to MODULE.md, so SKILL.md no longer exists.\n# - Strategy B applied: SKILL.md exists, now begins with \"---\".\n#\n# Return codes:\n# 0 — OK (either upstream-original good or Strategy B applied)\n# 1 — broken (file exists but lacks frontmatter)\n# 2 — submodule not present at all (legitimate for a future upstream layout change)\n# 3 — Strategy A applied (renamed to MODULE.md)\n# 4 — conflicted: both SKILL.md and MODULE.md exist simultaneously\n# (happens if a user switched repair strategies mid-session or\n# restored a partial backup — the agent needs to pick a winning state)\ncheck_submodule() {\n local dir=\"$1\"\n local skill_md=\"$dir/SKILL.md\"\n local module_md=\"$dir/MODULE.md\"\n\n # Check dual-state first — if both files exist, the install is in a\n # conflicted state that neither Strategy A nor Strategy B can claim cleanly.\n if [ -f \"$skill_md\" ] && [ -f \"$module_md\" ]; then\n return 4\n fi\n\n if [ -f \"$skill_md\" ]; then\n local first_line\n first_line=$(head -n 1 \"$skill_md\" 2>/dev/null || echo \"\")\n if [ \"$first_line\" = \"---\" ]; then\n return 0\n fi\n return 1\n fi\n\n if [ -f \"$module_md\" ]; then\n return 3\n fi\n\n return 2\n}\n\nscan_issue_001() {\n local agent=\"$1\"\n local base=\"$2\"\n local sub dir rc\n for sub in notes knowledge-base; do\n dir=\"$base/$sub\"\n check_submodule \"$dir\"\n rc=$?\n case \"$rc\" in\n 0) status_ok \"ISSUE-001 clear ($agent: $sub/SKILL.md has frontmatter)\" ;;\n 3) status_ok \"ISSUE-001 clear ($agent: $sub/MODULE.md — Strategy A applied)\" ;;\n 4) status_warn \"ISSUE-001 CONFLICTED ($agent: both $sub/SKILL.md and $sub/MODULE.md exist — pick one; see known_issues.md)\" ;;\n 2) echo \"ℹ️ $agent: $sub submodule not present (post-upstream-layout-change?)\" ;;\n *) status_warn \"ISSUE-001 TRIGGERED ($agent: $sub/SKILL.md missing YAML frontmatter)\" ;;\n esac\n done\n}\n\n# Scan each unique canonical directory exactly once. When multiple agents\n# share the same underlying install via symlink, scanning one represents all.\nSCANNED_REALS=\"\"\nscan_agent() {\n local agent=\"$1\"\n local path=\"$2\"\n local real=\"$3\"\n if [ -z \"$path\" ]; then\n return\n fi\n case \" $SCANNED_REALS \" in\n *\" $real \"*)\n # Already scanned via another agent entry\n return\n ;;\n esac\n SCANNED_REALS=\"$SCANNED_REALS $real\"\n scan_issue_001 \"$agent\" \"$path\"\n}\n\nscan_agent \"claude-code\" \"$CLAUDE_PATH\" \"$CLAUDE_REAL\"\nscan_agent \"codex\" \"$CODEX_PATH\" \"$CODEX_REAL\"\nscan_agent \"openclaw\" \"$OPENCLAW_PATH\" \"$OPENCLAW_REAL\"\n\necho\n\n# ==========================================================================\n# 4. Summary and exit code\n# ==========================================================================\n\necho \"--- Summary ---\"\necho \" ✅ ${PASS} pass ⚠️ ${WARN} warn ❌ ${FAIL} fail\"\necho\n\nif [ \"$FAIL\" -gt 0 ] || [ \"$WARN\" -gt 0 ]; then\n echo \"Next step: open references/known_issues.md and walk the agent through\"\n echo \"the warnings above. Each issue ID maps to a concrete repair procedure.\"\n exit 1\nfi\n\nexit 0\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":9861,"content_sha256":"75ca275b2ae86cc3a7cf4c6554edc77f5001fd77bd0415a2ed8de5a821fdaf0d"},{"filename":"scripts/install_ima_skill.sh","content":"#!/usr/bin/env bash\n#\n# install_ima_skill.sh — Install the upstream Tencent ima-skill to Claude Code,\n# Codex, and OpenClaw in one shot.\n#\n# Flow:\n# 1. Download the official zip from ima.qq.com\n# 2. Stage it in a temp directory\n# 3. Detect which of the three target agents are installed locally\n# 4. Delegate to `npx skills add \u003clocal-path>` (vercel-labs/skills) in its\n# default symlink mode so that the three agents share a single canonical\n# copy — a repair or upgrade applied once propagates to every agent.\n# 5. Clean up the staging dir on exit (safe: vercel skills promotes the\n# first agent's install to canonical and symlinks the rest to it,\n# independent of the staging source)\n#\n# Re-run safely — every step is idempotent. `npx skills add` will overwrite\n# existing ima-skill installs with the new version, and the symlink graph\n# gets rebuilt on every run.\n\nset -euo pipefail\n\nIMA_VERSION=\"${IMA_VERSION:-1.1.2}\"\nBASE_URL=\"https://app-dl.ima.qq.com/skills\"\nSTAGING_ROOT=\"/tmp/ima-copilot-staging\"\nSTAGING_DIR=\"${STAGING_ROOT}/$(date +%s)-$\"\n\ncleanup() {\n if [ -n \"${STAGING_DIR:-}\" ] && [ -d \"$STAGING_DIR\" ]; then\n rm -rf \"$STAGING_DIR\"\n fi\n}\ntrap cleanup EXIT\n\nusage() {\n cat \u003c\u003c'EOF'\nUsage: install_ima_skill.sh [--version \u003cx.y.z>]\n\nDownloads the upstream Tencent ima-skill and installs it globally to the\nsupported coding agents (Claude Code, Codex, OpenClaw) that are detected on\nthis machine. Uses vercel-labs/skills CLI (`npx skills add`) as the\ndistribution mechanism, in vercel's default symlink mode so that a repair or\nupgrade applied to any one of the three agent directories propagates through\nthe symlink graph to every other agent automatically.\n\nEnvironment overrides:\n IMA_VERSION Upstream version to install (default: 1.1.2)\n\nExamples:\n install_ima_skill.sh\n install_ima_skill.sh --version 1.1.2\n IMA_VERSION=1.2.0 install_ima_skill.sh\n\nFind the latest upstream version at https://ima.qq.com/agent-interface\nEOF\n}\n\nwhile [ $# -gt 0 ]; do\n case \"$1\" in\n --version)\n IMA_VERSION=\"$2\"\n shift 2\n ;;\n --version=*)\n IMA_VERSION=\"${1#*=}\"\n shift\n ;;\n -h|--help)\n usage\n exit 0\n ;;\n *)\n echo \"unknown argument: $1\" >&2\n usage >&2\n exit 1\n ;;\n esac\ndone\n\n# Require basic tools\nfor tool in curl unzip npx; do\n if ! command -v \"$tool\" >/dev/null 2>&1; then\n echo \"✗ Required tool not found on PATH: $tool\" >&2\n exit 1\n fi\ndone\n\n# Require Node.js >= 18 — `npx -y skills add` from vercel-labs/skills needs\n# a modern Node runtime. The error is otherwise opaque if it fires on an\n# ancient Node version.\nif command -v node >/dev/null 2>&1; then\n node_major=$(node --version 2>/dev/null | sed -E 's/^v([0-9]+).*/\\1/')\n if [ -n \"$node_major\" ] && [ \"$node_major\" -lt 18 ] 2>/dev/null; then\n echo \"✗ Node.js 18+ required for 'npx skills add' — found: $(node --version)\" >&2\n echo \" Upgrade via your package manager (brew/apt/nvm) and retry.\" >&2\n exit 1\n fi\nfi\n\necho \"▶ Staging upstream ima-skill v${IMA_VERSION}\"\nmkdir -p \"$STAGING_DIR\"\n\nZIP_URL=\"${BASE_URL}/ima-skills-${IMA_VERSION}.zip\"\nZIP_PATH=\"${STAGING_DIR}/ima-skills.zip\"\n\necho \" Downloading ${ZIP_URL}\"\nhttp_code=$(curl -sS -L --fail -o \"$ZIP_PATH\" -w \"%{http_code}\" \"$ZIP_URL\" || echo \"000\")\nif [ \"$http_code\" != \"200\" ]; then\n echo \"\" >&2\n echo \"✗ Download failed (HTTP ${http_code})\" >&2\n echo \"\" >&2\n echo \"If IMA has released a newer version, pass it explicitly:\" >&2\n echo \" IMA_VERSION=x.y.z bash $0\" >&2\n echo \"\" >&2\n echo \"or find the latest version at https://ima.qq.com/agent-interface\" >&2\n exit 1\nfi\n\nactual_size=$(wc -c \u003c \"$ZIP_PATH\" | tr -d ' ')\necho \" Downloaded ${actual_size} bytes\"\nif [ \"$actual_size\" -lt 1000 ]; then\n echo \"✗ Downloaded file is suspiciously small — aborting before extraction\" >&2\n exit 1\nfi\n\necho \" Extracting…\"\nunzip -q -o \"$ZIP_PATH\" -d \"$STAGING_DIR\"\n\n# Locate the root ima-skill directory inside the extracted archive.\n#\n# This matters more than it looks. The upstream 1.1.2 archive contains SKILL.md\n# at three depths (root, notes/, knowledge-base/) because ISSUE-001 exists —\n# notes/SKILL.md and knowledge-base/SKILL.md are documented as \"module files\"\n# but happen to use the same filename as the real root. A naive \"first SKILL.md\n# we find\" strategy will pick up the shallowest one if we're lucky and a\n# submodule if we're not, breaking the install non-deterministically.\n#\n# Resolution: prefer the well-known layout (\u003cstaging>/ima-skill/SKILL.md), and\n# only fall back to a recursive scan if that layout has changed in a future\n# release. The fallback picks the shallowest candidate, which is the root by\n# construction of every legal SKILL.md tree.\nSKILL_SRC=\"\"\nif [ -f \"$STAGING_DIR/ima-skill/SKILL.md\" ]; then\n SKILL_SRC=\"$STAGING_DIR/ima-skill\"\nelse\n shallowest_depth=999\n while IFS= read -r candidate; do\n # Count slashes in the relative portion to compare depths uniformly\n rel=\"${candidate#$STAGING_DIR/}\"\n depth=$(awk -F/ '{print NF}' \u003c\u003c\u003c \"$rel\")\n if [ \"$depth\" -lt \"$shallowest_depth\" ]; then\n shallowest_depth=\"$depth\"\n SKILL_SRC=$(dirname \"$candidate\")\n fi\n done \u003c \u003c(find \"$STAGING_DIR\" -maxdepth 4 -type f -name SKILL.md -print)\nfi\n\nif [ -z \"$SKILL_SRC\" ]; then\n echo \"✗ Could not locate SKILL.md in extracted archive\" >&2\n echo \" Archive contents:\" >&2\n find \"$STAGING_DIR\" -maxdepth 4 -type f -print >&2 || true\n exit 1\nfi\n\necho \" Found root SKILL.md at: ${SKILL_SRC}\"\n\n# Detect which target agents are installed. Being present is a proxy for\n# \"the user wants things installed here\"; absence means skip silently rather\n# than install anywhere they haven't opted in.\nAGENTS=()\n[ -d \"$HOME/.claude\" ] && AGENTS+=(\"claude-code\")\n[ -d \"$HOME/.agents\" ] && AGENTS+=(\"codex\")\nif [ -d \"$HOME/.openclaw\" ] || command -v openclaw >/dev/null 2>&1; then\n AGENTS+=(\"openclaw\")\nfi\n\nif [ ${#AGENTS[@]} -eq 0 ]; then\n echo \"\" >&2\n echo \"⚠ No supported agent detected on this machine.\" >&2\n echo \" Looked for: ~/.claude (Claude Code), ~/.agents (Codex), openclaw command.\" >&2\n echo \" Defaulting to claude-code as the most common case.\" >&2\n echo \"\" >&2\n AGENTS=(\"claude-code\")\nfi\n\necho \"▶ Targeting agents: ${AGENTS[*]}\"\n\nAGENT_FLAGS=()\nfor a in \"${AGENTS[@]}\"; do\n AGENT_FLAGS+=(\"-a\" \"$a\")\ndone\n\necho \" Running: npx -y skills add \\\"${SKILL_SRC}\\\" -g -y ${AGENT_FLAGS[*]}\"\nif ! npx -y skills add \"$SKILL_SRC\" -g -y \"${AGENT_FLAGS[@]}\"; then\n echo \"✗ npx skills add failed\" >&2\n echo \" Make sure Node.js is installed and the npm registry is reachable.\" >&2\n exit 1\nfi\n\necho \"\"\necho \"✓ Upstream ima-skill v${IMA_VERSION} installed successfully\"\necho \"\"\necho \"Next steps:\"\necho \" 1. Configure API credentials\"\necho \" Save your Client ID and API Key from https://ima.qq.com/agent-interface\"\necho \" into ~/.config/ima/client_id and ~/.config/ima/api_key (mode 600).\"\necho \"\"\necho \" 2. Run the diagnostic\"\necho \" bash \\\"\\$(dirname \\\"\\$0\\\")/diagnose.sh\\\"\"\necho \"\"\necho \" 3. Let the agent drive repairs\"\necho \" The diagnostic flags known upstream issues — rerun via your agent\"\necho \" and it will walk you through the fixes, asking consent for each.\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":7238,"content_sha256":"5825979b0e0da01a49cd09cfa8e82774d30ff05ab501d57a2549731089b721c4"},{"filename":"scripts/search_fanout.py","content":"#!/usr/bin/env python3\n\"\"\"\nsearch_fanout.py — Fan-out search across all IMA knowledge bases with\nuser-defined priority boosting and subset-KB skipping.\n\nRationale for this shape:\n IMA's OpenAPI has three constraints that force every serious search tool\n into the same pattern:\n 1. No cross-KB endpoint — search_knowledge takes a single knowledge_base_id.\n 2. No relevance score in results — ranking must be client-side.\n 3. Silent 100-hit truncation with no cursor — queries that saturate a KB\n are invisibly capped.\n\n Given those constraints, the most useful thing a personal wrapper can do\n is (a) fan out to every KB in parallel, (b) warn the user about truncated\n KBs, and (c) group results by KB with user-declared priority groups\n floated to the top. That's what this script does.\n\nConfig:\n ~/.config/ima/copilot.json — optional. Shape:\n {\n \"priority_kbs\": [\"kb name 1\", \"kb name 2\"], # hits from these go first\n \"skip_kbs\": [\"kb name 3\"], # silently skip (e.g. strict\n # subset of a priority KB)\n \"fanout_strategy\": \"parallel-then-merge\" # reserved for future modes\n }\n\nCredentials:\n Env vars IMA_OPENAPI_CLIENTID and IMA_OPENAPI_APIKEY take precedence.\n Falls back to ~/.config/ima/client_id and ~/.config/ima/api_key.\n\nUsage:\n python3 search_fanout.py \"your query here\"\n python3 search_fanout.py --max-results 5 \"your query\"\n python3 search_fanout.py --json \"your query\"\n\"\"\"\n\nimport argparse\nimport concurrent.futures\nimport json\nimport os\nimport sys\nimport urllib.error\nimport urllib.request\nfrom pathlib import Path\n\nAPI_BASE = \"https://ima.qq.com/openapi\"\nHARD_HIT_CAP = 100 # search_knowledge silently caps at 100, no cursor\n\n\ndef load_credentials():\n client_id = os.environ.get(\"IMA_OPENAPI_CLIENTID\", \"\").strip()\n api_key = os.environ.get(\"IMA_OPENAPI_APIKEY\", \"\").strip()\n\n config_dir = Path.home() / \".config\" / \"ima\"\n if not client_id:\n p = config_dir / \"client_id\"\n if p.is_file():\n client_id = p.read_text().strip()\n if not api_key:\n p = config_dir / \"api_key\"\n if p.is_file():\n api_key = p.read_text().strip()\n\n if not client_id or not api_key:\n sys.exit(\n \"error: credentials not found.\\n\"\n \" set IMA_OPENAPI_CLIENTID and IMA_OPENAPI_APIKEY, or write them to\\n\"\n \" ~/.config/ima/client_id and ~/.config/ima/api_key (mode 600).\"\n )\n return client_id, api_key\n\n\ndef load_config():\n \"\"\"\n Return (priority_kbs, skip_kbs, strategy). Missing config → empty preferences.\n\n Config path is resolved in this order:\n 1. $IMA_COPILOT_CONFIG if set — used by tests and advanced users\n 2. ~/.config/ima/copilot.json — the default XDG location\n \"\"\"\n env_override = os.environ.get(\"IMA_COPILOT_CONFIG\", \"\").strip()\n if env_override:\n p = Path(env_override)\n else:\n p = Path.home() / \".config\" / \"ima\" / \"copilot.json\"\n if not p.is_file():\n return [], [], \"parallel-then-merge\"\n try:\n data = json.loads(p.read_text())\n except json.JSONDecodeError as e:\n sys.exit(f\"error: ~/.config/ima/copilot.json is not valid JSON: {e}\")\n return (\n list(data.get(\"priority_kbs\", []) or []),\n list(data.get(\"skip_kbs\", []) or []),\n data.get(\"fanout_strategy\", \"parallel-then-merge\"),\n )\n\n\ndef api_post(path, body, client_id, api_key):\n \"\"\"POST JSON to the IMA API. Returns parsed dict on success, raises on failure.\"\"\"\n url = f\"{API_BASE}/{path}\"\n req = urllib.request.Request(\n url,\n data=json.dumps(body).encode(\"utf-8\"),\n method=\"POST\",\n headers={\n \"ima-openapi-clientid\": client_id,\n \"ima-openapi-apikey\": api_key,\n \"Content-Type\": \"application/json\",\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n payload = resp.read().decode(\"utf-8\")\n except urllib.error.HTTPError as e:\n raise RuntimeError(f\"HTTP {e.code}: {e.read().decode('utf-8', 'replace')[:200]}\")\n except urllib.error.URLError as e:\n raise RuntimeError(f\"network error: {e.reason}\")\n data = json.loads(payload)\n if data.get(\"code\") != 0:\n raise RuntimeError(f\"API error code={data.get('code')} msg={data.get('msg')}\")\n return data.get(\"data\", {})\n\n\ndef list_all_kbs(client_id, api_key):\n \"\"\"Paginate through search_knowledge_base with query='' to enumerate every KB.\"\"\"\n kbs = []\n cursor = \"\"\n while True:\n data = api_post(\n \"wiki/v1/search_knowledge_base\",\n {\"query\": \"\", \"cursor\": cursor, \"limit\": 50},\n client_id,\n api_key,\n )\n kbs.extend(data.get(\"info_list\", []))\n if data.get(\"is_end\") or not data.get(\"next_cursor\"):\n break\n cursor = data[\"next_cursor\"]\n if len(kbs) > 500: # defensive upper bound\n break\n return kbs\n\n\ndef search_one_kb(kb, query, client_id, api_key):\n \"\"\"Returns a dict with kb info + hits + truncation flag. Never raises; errors captured.\"\"\"\n try:\n data = api_post(\n \"wiki/v1/search_knowledge\",\n {\"query\": query, \"knowledge_base_id\": kb[\"kb_id\"], \"cursor\": \"\"},\n client_id,\n api_key,\n )\n except RuntimeError as e:\n return {\"kb\": kb, \"hits\": [], \"truncated\": False, \"error\": str(e)}\n\n hits = data.get(\"info_list\", []) or []\n # Silent truncation detection: exact 100 hits with no is_end/next_cursor\n # in the response is the upstream tell-tale.\n truncated = len(hits) >= HARD_HIT_CAP and not data.get(\"is_end\") and not data.get(\"next_cursor\")\n return {\"kb\": kb, \"hits\": hits, \"truncated\": truncated, \"error\": None}\n\n\nPERMISSION_DENIED_MARKER = \"220030\"\n\n\ndef is_permission_denied(result):\n return result[\"error\"] is not None and PERMISSION_DENIED_MARKER in result[\"error\"]\n\n\ndef rank_groups(results, priority_kbs, skip_kbs):\n \"\"\"\n Partition the per-KB results into four buckets:\n\n - priority: kbs named in priority_kbs (in that order), with >0 hits\n - others: every other searchable kb with >0 hits, sorted by hit count\n - denied: kbs that came back with \"no permission\" — this is common for\n subscribed (read-only) KBs where the user doesn't own search\n access. Collected separately so the user sees the list but\n it doesn't drown out real results.\n - empty: searchable kbs that simply had 0 hits for this query\n (silenced in the text renderer to keep output tight)\n \"\"\"\n skip_set = set(skip_kbs)\n priority_order = list(priority_kbs)\n\n by_name = {r[\"kb\"][\"kb_name\"]: r for r in results}\n\n priority, denied, others, empty = [], [], [], []\n\n # Priority group — walk in user-declared order so the output matches intent\n for name in priority_order:\n r = by_name.pop(name, None)\n if r is None or name in skip_set:\n continue\n if is_permission_denied(r):\n denied.append(r)\n elif r[\"hits\"]:\n priority.append(r)\n else:\n empty.append(r)\n\n # Everyone else\n for name, r in by_name.items():\n if name in skip_set:\n continue\n if is_permission_denied(r):\n denied.append(r)\n elif r[\"hits\"]:\n others.append(r)\n else:\n empty.append(r)\n\n # Sort primarily by hit count descending, secondarily by KB name ascending\n # for stable deterministic output. Without the secondary key, tied KBs\n # would be ordered by the concurrent.futures.ThreadPoolExecutor.map\n # completion order, which depends on network timing and is not reproducible\n # across runs. The kb_name tiebreaker makes the output byte-identical for\n # identical query + identical KB set regardless of network timing.\n others.sort(key=lambda r: (-len(r[\"hits\"]), r[\"kb\"][\"kb_name\"]))\n return priority, others, denied, empty\n\n\ndef truncate(s, n=120):\n s = s.replace(\"\\n\", \" \")\n return s if len(s) \u003c= n else s[: n - 1] + \"…\"\n\n\ndef render_text(query, priority, others, denied, skipped_cfg, total_kbs_listed, max_per_kb):\n searchable_with_hits = len(priority) + len(others)\n total_hits = sum(len(r[\"hits\"]) for r in priority + others)\n truncated_kbs = [r[\"kb\"][\"kb_name\"] for r in priority + others if r[\"truncated\"]]\n\n print(f'\\n🔍 Searched \"{query}\" across {total_kbs_listed} knowledge bases')\n if skipped_cfg:\n names = \", \".join(r[\"kb\"][\"kb_name\"] for r in skipped_cfg)\n print(f\" skipped via config: {names}\")\n print()\n\n def render_group(prefix, group, note=\"\"):\n for r in group:\n kb = r[\"kb\"]\n hits = r[\"hits\"]\n label = f\"{prefix} {kb['kb_name']} — {len(hits)} hit{'s' if len(hits) != 1 else ''}\"\n if note:\n label += f\" {note}\"\n if r[\"truncated\"]:\n label += \" (⚠️ truncated at 100)\"\n print(label)\n for i, hit in enumerate(hits[:max_per_kb], 1):\n title = hit.get(\"title\", \"(no title)\")\n print(f\" {i}. {title}\")\n snippet = hit.get(\"highlight_content\", \"\") or \"\"\n if snippet:\n print(f\" {truncate(snippet)}\")\n if len(hits) > max_per_kb:\n print(f\" … ({len(hits) - max_per_kb} more)\")\n print()\n\n if priority:\n render_group(\"🥇\", priority, \"(priority)\")\n if others:\n render_group(\"📚\", others)\n\n if not priority and not others:\n print(\"(no hits in any searchable knowledge base)\\n\")\n\n print(\"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\")\n print(f\"Total: {total_hits} hits across {searchable_with_hits} kb(s) with results\")\n\n if denied:\n print(\n f\"\\nℹ️ {len(denied)} kb(s) returned 'no permission' (typical for subscribed/read-only KBs):\"\n )\n for r in denied:\n print(f\" - {r['kb']['kb_name']}\")\n\n if truncated_kbs:\n print(\n f\"\\n⚠️ {len(truncated_kbs)} kb(s) hit the 100-result ceiling; try a narrower query:\"\n )\n for name in truncated_kbs:\n print(f\" - {name}\")\n\n\ndef render_json(query, priority, others, denied, skipped_cfg):\n def export(group):\n return [\n {\n \"kb_id\": r[\"kb\"][\"kb_id\"],\n \"kb_name\": r[\"kb\"][\"kb_name\"],\n \"hit_count\": len(r[\"hits\"]),\n \"truncated\": r[\"truncated\"],\n \"error\": r[\"error\"],\n \"hits\": [\n {\n \"title\": h.get(\"title\"),\n \"media_id\": h.get(\"media_id\"),\n \"parent_folder_id\": h.get(\"parent_folder_id\"),\n \"highlight_content\": h.get(\"highlight_content\"),\n }\n for h in r[\"hits\"]\n ],\n }\n for r in group\n ]\n\n out = {\n \"query\": query,\n \"priority\": export(priority),\n \"others\": export(others),\n \"denied\": [r[\"kb\"][\"kb_name\"] for r in denied],\n \"skipped_by_config\": [r[\"kb\"][\"kb_name\"] for r in skipped_cfg],\n }\n print(json.dumps(out, ensure_ascii=False, indent=2))\n\n\ndef main(argv=None):\n ap = argparse.ArgumentParser(description=__doc__.split(\"\\n\")[1])\n ap.add_argument(\"query\", help=\"Search query string\")\n ap.add_argument(\n \"--max-results\",\n type=int,\n default=5,\n help=\"Max hits to render per KB in text mode (default: 5)\",\n )\n ap.add_argument(\n \"--workers\",\n type=int,\n default=12,\n help=\"Parallel worker count for fan-out calls (default: 12)\",\n )\n ap.add_argument(\"--json\", action=\"store_true\", help=\"Output JSON instead of text\")\n args = ap.parse_args(argv)\n\n client_id, api_key = load_credentials()\n priority_kbs, skip_kbs, _strategy = load_config()\n\n try:\n all_kbs = list_all_kbs(client_id, api_key)\n except RuntimeError as e:\n sys.exit(f\"error listing knowledge bases: {e}\")\n if not all_kbs:\n sys.exit(\"no knowledge bases accessible with these credentials\")\n\n to_search = [kb for kb in all_kbs if kb[\"kb_name\"] not in set(skip_kbs)]\n\n with concurrent.futures.ThreadPoolExecutor(max_workers=args.workers) as pool:\n results = list(\n pool.map(\n lambda kb: search_one_kb(kb, args.query, client_id, api_key),\n to_search,\n )\n )\n\n # Enumerate config-skipped KBs separately so the user sees which ones\n # were intentionally filtered out vs which ones genuinely had no results.\n skipped_cfg_results = [\n {\"kb\": kb, \"hits\": [], \"truncated\": False, \"error\": None}\n for kb in all_kbs\n if kb[\"kb_name\"] in set(skip_kbs)\n ]\n\n priority, others, denied, _empty = rank_groups(results, priority_kbs, skip_kbs)\n\n if args.json:\n render_json(args.query, priority, others, denied, skipped_cfg_results)\n else:\n render_text(\n args.query,\n priority,\n others,\n denied,\n skipped_cfg_results,\n total_kbs_listed=len(all_kbs),\n max_per_kb=args.max_results,\n )\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":13500,"content_sha256":"f06994f832effbbf105f9428ef455f401a7f063074d838705490a3178b546a67"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"IMA Copilot","type":"text"}]},{"type":"paragraph","content":[{"text":"One-command installer, troubleshooter, and personalization layer for the official Tencent IMA skill.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Overview","type":"text"}]},{"type":"paragraph","content":[{"text":"The official Tencent IMA skill (ima-skill) exposes a powerful OpenAPI for notes and knowledge base operations, but its installation flow is designed for a specific proprietary agent and recent releases have shipped submodule files that fail strict SKILL.md loaders. IMA Copilot solves both problems:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Installs ima-skill to Claude Code, Codex, and OpenClaw in a single command via the ","type":"text"},{"text":"vercel-labs/skills","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/vercel-labs/skills","title":null}}]},{"text":" open installer.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Walks the user through API key setup with a live validation call.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Detects known upstream issues and — with explicit user consent — fixes them in place, without ever forking, vendoring, or mirroring any part of the upstream package.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Provides a fan-out search strategy that respects user-configured knowledge base priorities and boosts, with awareness of the 100-result per-KB truncation limit.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Architectural principles (do not violate)","type":"text"}]},{"type":"paragraph","content":[{"text":"This skill is a ","type":"text"},{"text":"wrapper layer","type":"text","marks":[{"type":"strong"}]},{"text":" around ima-skill. The wrapper contract is non-negotiable:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never vendor upstream files.","type":"text","marks":[{"type":"strong"}]},{"text":" This skill directory does not contain any copy, fork, or excerpt of ima-skill's own content. When ima-skill ships a new release, users get the new release without any interference from this wrapper.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Repairs happen at runtime, not at ship time.","type":"text","marks":[{"type":"strong"}]},{"text":" If an upstream bug needs patching, this skill carries the ","type":"text"},{"text":"instructions","type":"text","marks":[{"type":"em"}]},{"text":" for how to patch, not the patched files. Running a repair is idempotent: rerunning after an upstream update re-detects and re-fixes anything that came back.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Always ask before touching upstream files.","type":"text","marks":[{"type":"strong"}]},{"text":" Modifying ","type":"text"},{"text":"~/.claude/skills/ima-skill/**","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"~/.agents/skills/ima-skill/**","type":"text","marks":[{"type":"code_inline"}]},{"text":", or any other upstream install directory requires explicit user consent via AskUserQuestion. No silent patching.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Teach rather than hide.","type":"text","marks":[{"type":"strong"}]},{"text":" When a fix is applied, show the user exactly what changed and where the backup was saved. This is how users learn to maintain their own installs.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"What this skill does","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Capability","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Entry point","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Detail","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1. Install upstream ima-skill to 3 agents","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/install_ima_skill.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/installation_flow.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2. Configure API credentials (XDG style)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Inline workflow below","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/api_key_setup.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3. Diagnose and fix known upstream issues","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/diagnose.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" + workflow below","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/known_issues.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4. Fan-out search with priority boosting","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/search_fanout.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"references/search_best_practices.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Routing","type":"text"}]},{"type":"paragraph","content":[{"text":"When this skill is triggered, classify the user's intent and jump to the corresponding capability:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"User says something like…","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Go to","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"装 ima\"、\"install ima-skill\"、\"把 ima 装一下\"、\"我想用 ima\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Capability 1","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"配 ima 的 key\"、\"configure ima credentials\"、\"ima API key\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Capability 2","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"ima 报错\"、\"SKILL.md warning\"、\"frontmatter 错误\"、\"ima 加载失败\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Capability 3","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"搜 X\"、\"在 ima 里搜 X\"、\"跨知识库搜索\"、\"扇出搜 X\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Capability 4","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"帮我从头跑一遍 ima\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1 → 2 → 3 → 4 in sequence","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"When in doubt, start with Capability 3 (diagnose) — it surfaces exactly which capabilities are blocked and in what order.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Capability 1: Install upstream ima-skill","type":"text"}]},{"type":"paragraph","content":[{"text":"The installer downloads the latest official release from ","type":"text"},{"text":"https://app-dl.ima.qq.com/skills/","type":"text","marks":[{"type":"code_inline"}]},{"text":", stages it in a temp directory, and hands off to ","type":"text"},{"text":"npx skills add \u003clocal-path>","type":"text","marks":[{"type":"code_inline"}]},{"text":" to distribute it across Claude Code, Codex, and OpenClaw.","type":"text"}]},{"type":"paragraph","content":[{"text":"To run it:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash scripts/install_ima_skill.sh","type":"text"}]},{"type":"paragraph","content":[{"text":"The script auto-detects which of the three target agents are installed on the user's machine. For agents that are not present, it skips silently rather than installing anywhere the user hasn't opted in. For agents that are present, it installs globally (","type":"text"},{"text":"-g","type":"text","marks":[{"type":"code_inline"}]},{"text":") in vercel skills' default symlink mode: the first detected agent's directory becomes the canonical copy, and the remaining agents are symlinked to it. This means a repair or an upgrade applied once propagates automatically to every agent — ","type":"text"},{"text":"diagnose.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" detects this sharing and dedupes its reports so you don't see the same issue multiple times.","type":"text"}]},{"type":"paragraph","content":[{"text":"For a version override, detection logic, troubleshooting, and the full file-by-file layout produced by the installer, read ","type":"text"},{"text":"references/installation_flow.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Capability 2: Configure API credentials","type":"text"}]},{"type":"paragraph","content":[{"text":"Credentials are stored in XDG style, decoupled from any agent's skill directory:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"~/.config/ima/client_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" (mode ","type":"text"},{"text":"600","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"~/.config/ima/api_key","type":"text","marks":[{"type":"code_inline"}]},{"text":" (mode ","type":"text"},{"text":"600","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"~/.config/ima/","type":"text","marks":[{"type":"code_inline"}]},{"text":" (mode ","type":"text"},{"text":"700","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Environment variables ","type":"text"},{"text":"IMA_OPENAPI_CLIENTID","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"IMA_OPENAPI_APIKEY","type":"text","marks":[{"type":"code_inline"}]},{"text":" act as fall-back overrides — the wrapper reads the environment first, then the config file.","type":"text"}]},{"type":"paragraph","content":[{"text":"Step through the setup with the user:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Open ","type":"text"},{"text":"https://ima.qq.com/agent-interface","type":"text","marks":[{"type":"code_inline"}]},{"text":" and create a new Client ID and API Key.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Write both values into the XDG config path (or export the environment variables).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Make a single liveness call against ","type":"text"},{"text":"https://ima.qq.com/openapi/wiki/v1/search_knowledge_base","type":"text","marks":[{"type":"code_inline"}]},{"text":" with ","type":"text"},{"text":"{\"query\": \"\", \"cursor\": \"\", \"limit\": 1}","type":"text","marks":[{"type":"code_inline"}]},{"text":" to confirm the credentials are accepted — a ","type":"text"},{"text":"code: 0, msg: success","type":"text","marks":[{"type":"code_inline"}]},{"text":" response means ready.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"The full script and the exact request/response schema lives in ","type":"text"},{"text":"references/api_key_setup.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Capability 3: Diagnose and fix known issues","type":"text"}]},{"type":"paragraph","content":[{"text":"This is the reason this skill exists. The upstream package has real bugs that break loading on certain agents, and the fixes are well-understood but need user consent to apply. The diagnose/repair workflow is the ","type":"text"},{"text":"core contract","type":"text","marks":[{"type":"strong"}]},{"text":" of this skill.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 1 — Run the read-only diagnosis","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash scripts/diagnose.sh","type":"text"}]},{"type":"paragraph","content":[{"text":"diagnose.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"never modifies any file","type":"text","marks":[{"type":"strong"}]},{"text":". It prints a structured report with one line per check:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"✅ upstream ima-skill installed (claude-code)\n✅ upstream ima-skill installed (codex)\n❌ upstream ima-skill NOT installed (openclaw)\n✅ API credentials valid (search_knowledge_base returned 12 KBs)\n⚠️ ISSUE-001: notes/SKILL.md missing YAML frontmatter (claude-code)\n⚠️ ISSUE-001: knowledge-base/SKILL.md missing YAML frontmatter (claude-code)\n⚠️ ISSUE-001: notes/SKILL.md missing YAML frontmatter (codex)\n⚠️ ISSUE-001: knowledge-base/SKILL.md missing YAML frontmatter (codex)","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 2 — Parse the report and ask the user","type":"text"}]},{"type":"paragraph","content":[{"text":"For each ","type":"text"},{"text":"⚠️","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"❌","type":"text","marks":[{"type":"code_inline"}]},{"text":" line, look up the issue in ","type":"text"},{"text":"references/known_issues.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". That file is the source of truth for:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"What the issue is (symptom, root cause)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Which repair strategies exist (","type":"text"},{"text":"A","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"B","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"skip","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The exact shell commands for each strategy","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"What files each strategy touches","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Why the upstream maintainer probably hasn't fixed it yet","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 3 — Ask for explicit consent before touching upstream files","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"AskUserQuestion","type":"text","marks":[{"type":"strong"}]},{"text":" for every issue that has more than one repair strategy. Frame it plainly — the user may not know what \"YAML frontmatter\" means. Describe what the bug does to them in user terms (\"loader skips two files silently, so note-search and knowledge-base-search don't actually work\"), then describe each strategy in terms of the outcome, not the mechanism.","type":"text"}]},{"type":"paragraph","content":[{"text":"Never offer a single \"just fix it\" option when multiple strategies exist. The user's pick may legitimately differ based on factors the skill cannot observe — e.g., they might prefer Strategy B (minimal diff) if they plan to manually compare with upstream.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 4 — Execute the chosen strategy","type":"text"}]},{"type":"paragraph","content":[{"text":"Every repair command in ","type":"text"},{"text":"references/known_issues.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" is written to be:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Idempotent","type":"text","marks":[{"type":"strong"}]},{"text":" — rerunning after the fix is already applied does nothing harmful and prints a clear \"already fixed\" message.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Backed up","type":"text","marks":[{"type":"strong"}]},{"text":" — the repair copies the original file to ","type":"text"},{"text":"/tmp/ima-copilot-backups/\u003ctimestamp>/\u003crelative-path>","type":"text","marks":[{"type":"code_inline"}]},{"text":" before modifying anything, then tells the user the backup location.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reversible","type":"text","marks":[{"type":"strong"}]},{"text":" — the user can restore from the backup with a single ","type":"text"},{"text":"cp","type":"text","marks":[{"type":"code_inline"}]},{"text":" command shown at the end.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 5 — Re-run diagnose to confirm","type":"text"}]},{"type":"paragraph","content":[{"text":"After the repair, run ","type":"text"},{"text":"diagnose.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" a second time and show the user the diff. The issue should flip from ","type":"text"},{"text":"⚠️","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"✅","type":"text","marks":[{"type":"code_inline"}]},{"text":". If it does not, stop and surface the raw before/after to the user instead of silently retrying — unexpected failures here usually mean upstream shipped an unforeseen change.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"An important note about upstream updates","type":"text"}]},{"type":"paragraph","content":[{"text":"Every repair is ","type":"text"},{"text":"temporary in the sense that ima-skill upgrades replace everything","type":"text","marks":[{"type":"strong"}]},{"text":". This is by design: the skill does not fight upstream for persistent state. When the user upgrades ima-skill via Capability 1, Step 4 of diagnose will again flag the fixed issue, and the user can rerun the repair. This is a feature, not a bug — if upstream eventually fixes the issue, the repair becomes unnecessary and ","type":"text"},{"text":"diagnose.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" will report ✅ with no prompt.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Capability 4: Personalized fan-out search","type":"text"}]},{"type":"paragraph","content":[{"text":"IMA's OpenAPI has three hard constraints that any serious search workflow must account for:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No cross-knowledge-base endpoint.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"search_knowledge","type":"text","marks":[{"type":"code_inline"}]},{"text":" requires a single ","type":"text"},{"text":"knowledge_base_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" per call. Cross-KB search is a client-side fan-out, not an API feature.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No relevance score in results.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"info_list","type":"text","marks":[{"type":"code_inline"}]},{"text":" items only carry ","type":"text"},{"text":"media_id","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"title","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"parent_folder_id","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"highlight_content","type":"text","marks":[{"type":"code_inline"}]},{"text":". Any ranking beyond insertion order must happen on the client.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Silent 100-result truncation.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"search_knowledge","type":"text","marks":[{"type":"code_inline"}]},{"text":" returns at most 100 hits per KB with no ","type":"text"},{"text":"is_end","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"next_cursor","type":"text","marks":[{"type":"code_inline"}]},{"text":" field in the response. High-frequency queries are silently capped.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"scripts/search_fanout.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" implements the full workaround:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 scripts/search_fanout.py \"\u003cquery>\"","type":"text"}]},{"type":"paragraph","content":[{"text":"The script reads ","type":"text"},{"text":"~/.config/ima/copilot.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" for personalization (priority KBs, skip list, strategy), calls ","type":"text"},{"text":"search_knowledge_base","type":"text","marks":[{"type":"code_inline"}]},{"text":" to enumerate KBs, fans out ","type":"text"},{"text":"search_knowledge","type":"text","marks":[{"type":"code_inline"}]},{"text":" calls in parallel, detects truncation by exact-100 length match, and renders results grouped by KB with priority groups at the top.","type":"text"}]},{"type":"paragraph","content":[{"text":"The personalization file is ","type":"text"},{"text":"per-user","type":"text","marks":[{"type":"strong"}]},{"text":" and private. This skill ships only a template — see ","type":"text"},{"text":"config-template/copilot.json.example","type":"text","marks":[{"type":"code_inline"}]},{"text":". A user with no config file gets a neutral default: fan out all accessible KBs, sort groups by hit count, no boosting.","type":"text"}]},{"type":"paragraph","content":[{"text":"For the full algorithm, truncation handling strategy, rendering format, and a walkthrough of the evidence-based decision to allow a \"subset KB skip\" (e.g., a curated KB that is a strict subset of a master KB can be safely skipped to reduce duplicate hits), read ","type":"text"},{"text":"references/search_best_practices.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"What this skill refuses to do","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never vendor upstream content.","type":"text","marks":[{"type":"strong"}]},{"text":" This directory does not contain and will never contain a copy of ","type":"text"},{"text":"ima-skill/SKILL.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"ima-skill/notes/**","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"ima-skill/knowledge-base/**","type":"text","marks":[{"type":"code_inline"}]},{"text":", or any other upstream file. Anyone adding such files to this skill should be rejected.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never pin an upstream version in SKILL.md.","type":"text","marks":[{"type":"strong"}]},{"text":" The installer script carries a default version for fallback purposes, but SKILL.md itself is version-agnostic to survive upstream releases without requiring a skill bump.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never silently patch upstream files.","type":"text","marks":[{"type":"strong"}]},{"text":" Every modification path requires an explicit AskUserQuestion and the user's active choice.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never hardcode a user's knowledge base names.","type":"text","marks":[{"type":"strong"}]},{"text":" The ","type":"text"},{"text":"priority_kbs","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"skip_kbs","type":"text","marks":[{"type":"code_inline"}]},{"text":" fields in ","type":"text"},{"text":"copilot.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" are 100% user-configured. Example values in ","type":"text"},{"text":"config-template/copilot.json.example","type":"text","marks":[{"type":"code_inline"}]},{"text":" are illustrative only.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never skip the backup step","type":"text","marks":[{"type":"strong"}]},{"text":" when executing a repair, no matter how trivial the diff.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"File layout","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"ima-copilot/\n├── SKILL.md # This file — entry and routing\n├── scripts/\n│ ├── install_ima_skill.sh # Download → stage → npx skills add to 3 agents\n│ ├── diagnose.sh # Read-only health report\n│ └── search_fanout.py # Fan-out search with priority grouping\n├── references/\n│ ├── installation_flow.md # Capability 1 deep dive\n│ ├── api_key_setup.md # Capability 2 deep dive\n│ ├── known_issues.md # Issue registry — source of truth for repairs\n│ └── search_best_practices.md # Capability 4 deep dive\n└── config-template/\n └── copilot.json.example # Template for ~/.config/ima/copilot.json","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"ima-copilot","author":"@skillopedia","source":{"stars":1137,"repo_name":"claude-code-skills","origin_url":"https://github.com/daymade/claude-code-skills/blob/HEAD/ima-copilot/SKILL.md","repo_owner":"daymade","body_sha256":"73a29041c58babfe650f00d0b855af8220ea93ac79c77d6d642bab174efc48e2","cluster_key":"85f23d2d6e6cbf5f5bc52f063ef3a93277ca591f26d1cf0a15a5557a34f9fd2e","clean_bundle":{"format":"clean-skill-bundle-v1","source":"daymade/claude-code-skills/ima-copilot/SKILL.md","attachments":[{"id":"5d418bf8-15df-5ec5-be75-945edc139e2e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5d418bf8-15df-5ec5-be75-945edc139e2e/attachment","path":".security-scan-passed","size":181,"sha256":"4f3160c710f475d79cd878aa75ce611ba1c89120f46f2654dcb40200141928a4","contentType":"text/plain; charset=utf-8"},{"id":"dcd5f0fd-25b3-536d-bda0-366e5740424d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dcd5f0fd-25b3-536d-bda0-366e5740424d/attachment.example","path":"config-template/copilot.json.example","size":774,"sha256":"ec7ed9ffab7f71c392f855b2e9ac71762986ec88b1b5197fb9039d1991471084","contentType":"text/plain; charset=utf-8"},{"id":"6cb753f0-b4bf-5bb0-aa21-ecb72c54ae83","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6cb753f0-b4bf-5bb0-aa21-ecb72c54ae83/attachment.md","path":"references/api_key_setup.md","size":5950,"sha256":"1faea2fe2e52b46f0cda561aab53479c088b7522e990a433e29d5bead3e4316d","contentType":"text/markdown; charset=utf-8"},{"id":"b93a75ec-d0f0-5cbc-907b-ca33f06e8b62","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b93a75ec-d0f0-5cbc-907b-ca33f06e8b62/attachment.md","path":"references/installation_flow.md","size":7738,"sha256":"5a7c7544c8a200fe1ebb10b2362b97f84313b762337b052dbcafd5b6a5a4b3fe","contentType":"text/markdown; charset=utf-8"},{"id":"0a876ca6-422f-5456-a19c-770473e3076e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0a876ca6-422f-5456-a19c-770473e3076e/attachment.md","path":"references/known_issues.md","size":11145,"sha256":"87a2d281376b2331d9e74fb09b0694f821df90182fec849afcfd08bc04823861","contentType":"text/markdown; charset=utf-8"},{"id":"c0513563-688c-52fd-b75b-78b45f36da0c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c0513563-688c-52fd-b75b-78b45f36da0c/attachment.md","path":"references/search_best_practices.md","size":9464,"sha256":"1b5754d7188107f2bd470a57cf22e092b711767374eb6c1f15d125df1c238f19","contentType":"text/markdown; charset=utf-8"},{"id":"1d65cbd7-7ca3-5669-8b96-89e4f8e1177c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1d65cbd7-7ca3-5669-8b96-89e4f8e1177c/attachment.sh","path":"scripts/diagnose.sh","size":9861,"sha256":"75ca275b2ae86cc3a7cf4c6554edc77f5001fd77bd0415a2ed8de5a821fdaf0d","contentType":"application/x-sh; charset=utf-8"},{"id":"24a59018-8801-5c1b-84bb-3e4baf680cfc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/24a59018-8801-5c1b-84bb-3e4baf680cfc/attachment.sh","path":"scripts/install_ima_skill.sh","size":7238,"sha256":"5825979b0e0da01a49cd09cfa8e82774d30ff05ab501d57a2549731089b721c4","contentType":"application/x-sh; charset=utf-8"},{"id":"b17e1280-398e-5ecd-b807-bfe323504b4e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b17e1280-398e-5ecd-b807-bfe323504b4e/attachment.py","path":"scripts/search_fanout.py","size":13500,"sha256":"f06994f832effbbf105f9428ef455f401a7f063074d838705490a3178b546a67","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"2be57dd76bb9d9e8ac9c53ac65aeb94272c3b4b19f62d71a845565b5b7ed9d86","attachment_count":9,"text_attachments":7,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":2,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"ima-copilot/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"web-development","category_label":"Web"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"web-development","import_tag":"clean-skills-v1","description":"One-stop companion and installer for the official Tencent IMA skill (腾讯 IMA / ima.qq.com). Handles zero-config installation to Claude Code / Codex / OpenClaw via `npx skills add`, guides API key setup, detects and fixes known issues in the upstream package (including the missing-YAML-frontmatter bug in submodule SKILL.md files), and implements a personalized fan-out search strategy with priority-based knowledge base boosting. Use this skill whenever the user mentions IMA, 腾讯 IMA, ima.qq.com, ima-skill, installing or configuring ima-skill, searching across IMA knowledge bases, 知识库搜索, 笔记搜索, fan-out search with preferred KBs, or reports errors like \"Skipped loading skill(s) due to invalid SKILL.md\". Also trigger for any request to diagnose, repair, or personalize the behavior of an ima-skill installation. This is a wrapper layer around ima-skill — it installs and orchestrates ima-skill rather than replacing it."}},"renderedAt":1782979976030}

IMA Copilot One-command installer, troubleshooter, and personalization layer for the official Tencent IMA skill. Overview The official Tencent IMA skill (ima-skill) exposes a powerful OpenAPI for notes and knowledge base operations, but its installation flow is designed for a specific proprietary agent and recent releases have shipped submodule files that fail strict SKILL.md loaders. IMA Copilot solves both problems: 1. Installs ima-skill to Claude Code, Codex, and OpenClaw in a single command via the vercel-labs/skills open installer. 2. Walks the user through API key setup with a live vali…