
Ima Copilot
- 1k installs
- 1.3k repo stars
- Updated August 4, 2026
- daymade/claude-code-skills
ima-copilot is a Claude Code agent skill that installs, troubleshoots, and personalizes the official Tencent IMA knowledge-base wrapper for developers who need ima.qq.com knowledge-base and note search inside coding agen
About
ima-copilot is a one-command installer and repair skill for the upstream Tencent ima-skill wrapper across Claude Code, Codex, and OpenClaw. It orchestrates ima-skill setup, diagnoses invalid SKILL.md frontmatter errors in submodule skill files, and configures IMA API credentials, fan-out search, preferred knowledge-base priority boosting, and 笔记搜索 note search. Developers reach for ima-copilot when mentions of IMA, 腾讯 IMA, ima.qq.com, 知识库搜索, or ima-skill install failures appear during agent configuration. The skill explicitly states it is a wrapper layer that orchestrates upstream ima-skill rather than replacing Tencent's official skill. Typical triggers include Skipped loading skill(s) due to invalid SKILL.md startup errors and missing YAML frontmatter bugs in nested ima-skill repositories. ima-copilot personalizes search routing so multi-agent coding workflows can query enterprise knowledge bases reliably without manual submodule debugging, credential guesswork, or repeated install retries.
- One-command installer for ima-skill via vercel-labs/skills
- Automated API key setup with live validation
- Detects and repairs missing-YAML-frontmatter bugs and invalid SKILL.md errors with user consent
- Personalization layer for fan-out search, priority boosting, and preferred knowledge bases
- Troubleshooting mode for IMA API credentials and 知识库搜索 issues
Ima Copilot by the numbers
- 1,042 all-time installs (skills.sh)
- +63 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,014 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/daymade/claude-code-skills --skill ima-copilotAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1k |
|---|---|
| repo stars | ★ 1.3k |
| Last updated | August 4, 2026 |
| Repository | daymade/claude-code-skills ↗ |
How do you install and fix Tencent ima-skill in agents?
Instantly set up, troubleshoot, and personalize the official Tencent IMA knowledge-base skill across Claude Code, Codex, and OpenClaw.
Who is it for?
Developers integrating Tencent IMA knowledge-base search into Claude Code, Codex, or OpenClaw agent workflows.
Skip if: Developers who do not use Tencent IMA or ima.qq.com knowledge bases and only need generic web search.
When should I use this skill?
User mentions IMA, ima-skill, ima.qq.com, IMA API keys, 知识库搜索, invalid SKILL.md errors, or ima-skill install or configuration problems.
What you get
Working ima-skill install, valid SKILL.md frontmatter, configured IMA API keys, and personalized knowledge-base search routing
- Configured ima-skill install
- Repaired SKILL.md frontmatter
Files
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 validation call. 3. 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. 4. 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.
Architectural principles (do not violate)
This skill is a wrapper layer around ima-skill. The wrapper contract is non-negotiable:
- Never vendor upstream files. 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.
- Repairs happen at runtime, not at ship time. If an upstream bug needs patching, this skill carries the instructions 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.
- Always ask before touching upstream files. Modifying
~/.claude/skills/ima-skill/**,~/.agents/skills/ima-skill/**, or any other upstream install directory requires explicit user consent via AskUserQuestion. No silent patching. - Teach rather than hide. 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.
What this skill does
| Capability | Entry point | Detail |
|---|---|---|
| 1. Install upstream ima-skill to 3 agents | scripts/install_ima_skill.sh | See references/installation_flow.md |
| 2. Configure API credentials (XDG style) | Inline workflow below | See references/api_key_setup.md |
| 3. Diagnose and fix known upstream issues | scripts/diagnose.sh + workflow below | See references/known_issues.md |
| 4. Fan-out search with priority boosting | scripts/search_fanout.py | See references/search_best_practices.md |
Routing
When this skill is triggered, classify the user's intent and jump to the corresponding capability:
| User says something like… | Go to |
|---|---|
| "装 ima"、"install ima-skill"、"把 ima 装一下"、"我想用 ima" | Capability 1 |
| "配 ima 的 key"、"configure ima credentials"、"ima API key" | Capability 2 |
| "ima 报错"、"SKILL.md warning"、"frontmatter 错误"、"ima 加载失败" | Capability 3 |
| "搜 X"、"在 ima 里搜 X"、"跨知识库搜索"、"扇出搜 X" | Capability 4 |
| "帮我从头跑一遍 ima" | 1 → 2 → 3 → 4 in sequence |
When in doubt, start with Capability 3 (diagnose) — it surfaces exactly which capabilities are blocked and in what order.
Capability 1: Install upstream ima-skill
The installer downloads the latest official release from https://app-dl.ima.qq.com/skills/, stages it in a temp directory, and hands off to npx skills add <local-path> to distribute it across Claude Code, Codex, and OpenClaw.
To run it:
bash scripts/install_ima_skill.shThe 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 (-g) 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 — diagnose.sh detects this sharing and dedupes its reports so you don't see the same issue multiple times.
For a version override, detection logic, troubleshooting, and the full file-by-file layout produced by the installer, read references/installation_flow.md.
Capability 2: Configure API credentials
Credentials are stored in XDG style, decoupled from any agent's skill directory:
~/.config/ima/client_id(mode600)~/.config/ima/api_key(mode600)~/.config/ima/(mode700)
Environment variables IMA_OPENAPI_CLIENTID and IMA_OPENAPI_APIKEY act as fall-back overrides — the wrapper reads the environment first, then the config file.
Step through the setup with the user:
1. Open https://ima.qq.com/agent-interface and create a new Client ID and API Key. 2. Write both values into the XDG config path (or export the environment variables). 3. Make a single liveness call against https://ima.qq.com/openapi/wiki/v1/search_knowledge_base with {"query": "", "cursor": "", "limit": 1} to confirm the credentials are accepted — a code: 0, msg: success response means ready.
The full script and the exact request/response schema lives in references/api_key_setup.md.
Capability 3: Diagnose and fix known issues
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 core contract of this skill.
Step 1 — Run the read-only diagnosis
bash scripts/diagnose.shdiagnose.sh never modifies any file. It prints a structured report with one line per check:
✅ upstream ima-skill installed (claude-code)
✅ upstream ima-skill installed (codex)
❌ upstream ima-skill NOT installed (openclaw)
✅ API credentials valid (search_knowledge_base returned 12 KBs)
⚠️ ISSUE-001: notes/SKILL.md missing YAML frontmatter (claude-code)
⚠️ ISSUE-001: knowledge-base/SKILL.md missing YAML frontmatter (claude-code)
⚠️ ISSUE-001: notes/SKILL.md missing YAML frontmatter (codex)
⚠️ ISSUE-001: knowledge-base/SKILL.md missing YAML frontmatter (codex)Step 2 — Parse the report and ask the user
For each ⚠️ or ❌ line, look up the issue in references/known_issues.md. That file is the source of truth for:
- What the issue is (symptom, root cause)
- Which repair strategies exist (
A,B,skip) - The exact shell commands for each strategy
- What files each strategy touches
- Why the upstream maintainer probably hasn't fixed it yet
Step 3 — Ask for explicit consent before touching upstream files
Use AskUserQuestion 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.
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.
Step 4 — Execute the chosen strategy
Every repair command in references/known_issues.md is written to be:
- Idempotent — rerunning after the fix is already applied does nothing harmful and prints a clear "already fixed" message.
- Backed up — the repair copies the original file to
/tmp/ima-copilot-backups/<timestamp>/<relative-path>before modifying anything, then tells the user the backup location. - Reversible — the user can restore from the backup with a single
cpcommand shown at the end.
Step 5 — Re-run diagnose to confirm
After the repair, run diagnose.sh a second time and show the user the diff. The issue should flip from ⚠️ to ✅. 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.
An important note about upstream updates
Every repair is temporary in the sense that ima-skill upgrades replace everything. 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 diagnose.sh will report ✅ with no prompt.
Capability 4: Personalized fan-out search
IMA's OpenAPI has three hard constraints that any serious search workflow must account for:
1. No cross-knowledge-base endpoint. search_knowledge requires a single knowledge_base_id per call. Cross-KB search is a client-side fan-out, not an API feature. 2. No relevance score in results. info_list items only carry media_id, title, parent_folder_id, and highlight_content. Any ranking beyond insertion order must happen on the client. 3. Silent 100-result truncation. search_knowledge returns at most 100 hits per KB with no is_end or next_cursor field in the response. High-frequency queries are silently capped.
scripts/search_fanout.py implements the full workaround:
python3 scripts/search_fanout.py "<query>"The script reads ~/.config/ima/copilot.json for personalization (priority KBs, skip list, strategy), calls search_knowledge_base to enumerate KBs, fans out search_knowledge calls in parallel, detects truncation by exact-100 length match, and renders results grouped by KB with priority groups at the top.
The personalization file is per-user and private. This skill ships only a template — see config-template/copilot.json.example. A user with no config file gets a neutral default: fan out all accessible KBs, sort groups by hit count, no boosting.
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 references/search_best_practices.md.
What this skill refuses to do
- Never vendor upstream content. This directory does not contain and will never contain a copy of
ima-skill/SKILL.md,ima-skill/notes/**,ima-skill/knowledge-base/**, or any other upstream file. Anyone adding such files to this skill should be rejected. - Never pin an upstream version in SKILL.md. 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.
- Never silently patch upstream files. Every modification path requires an explicit AskUserQuestion and the user's active choice.
- Never hardcode a user's knowledge base names. The
priority_kbsandskip_kbsfields incopilot.jsonare 100% user-configured. Example values inconfig-template/copilot.json.exampleare illustrative only. - Never skip the backup step when executing a repair, no matter how trivial the diff.
File layout
ima-copilot/
├── SKILL.md # This file — entry and routing
├── scripts/
│ ├── install_ima_skill.sh # Download → stage → npx skills add to 3 agents
│ ├── diagnose.sh # Read-only health report
│ └── search_fanout.py # Fan-out search with priority grouping
├── references/
│ ├── installation_flow.md # Capability 1 deep dive
│ ├── api_key_setup.md # Capability 2 deep dive
│ ├── known_issues.md # Issue registry — source of truth for repairs
│ └── search_best_practices.md # Capability 4 deep dive
└── config-template/
└── copilot.json.example # Template for ~/.config/ima/copilot.jsonSecurity scan passed
Scanned at: 2026-04-11T18:24:20.697988
Tool: gitleaks + pattern-based validation
Content hash: b4ef2b8f095342095dd0ea8d3c98d5346beff50afddd4ae092c8f69d4299c18b
{
"_comment_priority_kbs": "KB names whose hits should be floated to the top of every search, in the order listed. Names must match the KB name exactly (Unicode-sensitive). Leave empty for no priority groups.",
"priority_kbs": [
"your-curated-kb-name"
],
"_comment_skip_kbs": "KB names to exclude from fan-out entirely. Use this for strict subsets (e.g. a smaller KB whose contents are all duplicated in a larger one) or for KBs that never contain anything relevant to your searches. Leave empty to search everything you have access to.",
"skip_kbs": [
"your-subset-kb-name"
],
"_comment_fanout_strategy": "Reserved for future search strategy modes. Only 'parallel-then-merge' is currently implemented.",
"fanout_strategy": "parallel-then-merge"
}
API Key Setup — Deep Dive
This document covers the full flow for provisioning IMA OpenAPI credentials and verifying they work. The agent reads this when the user asks to configure credentials, or when diagnose.sh reports ❌ on API credentials.
Where credentials go
Credentials are stored XDG-style, completely decoupled from any agent's skill directory:
| Path | Purpose | Mode |
|---|---|---|
~/.config/ima/ | Config directory | 700 |
~/.config/ima/client_id | IMA Client ID | 600 |
~/.config/ima/api_key | IMA API Key | 600 |
The two files contain raw values, one per file, with an optional trailing newline. No JSON, no env wrapping, no key prefixes. This keeps the files trivially readable by both the diagnose.sh bash script and search_fanout.py.
Why not one file?
Two files are easier to rotate independently — you can update the API key without touching the client ID, and a leaked cat of one file doesn't expose the other.
Why not ~/.ima/?
~/.config/ima/ follows the XDG Base Directory Specification, which is the convention for user-level configuration on Linux and increasingly on macOS. Tools that honor $XDG_CONFIG_HOME will find the credentials in a predictable place.
Environment variable fallback
The wrapper reads environment variables first, then falls back to the config files:
| Env var | Fallback file |
|---|---|
IMA_OPENAPI_CLIENTID | ~/.config/ima/client_id |
IMA_OPENAPI_APIKEY | ~/.config/ima/api_key |
Use env vars when:
- Running in CI or ephemeral containers where persisting a file is awkward
- Rotating credentials mid-session without editing a file
- Testing with different credentials without touching the committed config
Use the files when:
- Running locally for long sessions (no need to re-export every time)
- Sharing a machine with multiple users (files have per-user permissions)
Walkthrough
Step 1 — Obtain credentials
Open https://ima.qq.com/agent-interface in a browser. If the user isn't signed in, they'll be prompted. The page shows a panel titled "API Key" with buttons to generate a new Client ID + API Key pair.
The page currently talks about "发给小龙虾以完成配置" ("send to OpenClaw to finish config") — the user can ignore that. What they need is just the Client ID value and the API Key value shown on that page.
Step 2 — Save to files
mkdir -p ~/.config/ima
chmod 700 ~/.config/ima
printf '%s' "<paste client id here>" > ~/.config/ima/client_id
printf '%s' "<paste api key here>" > ~/.config/ima/api_key
chmod 600 ~/.config/ima/client_id ~/.config/ima/api_keyprintf '%s' writes the value without a trailing newline — slightly more paranoid than echo since some tools choke on trailing whitespace when comparing credentials. The wrapper code strips newlines anyway, so either works, but this is the cleaner form.
Step 3 — Liveness test
The simplest safe call is search_knowledge_base with an empty query, limit 1. It's authenticated, returns success quickly on valid credentials, and doesn't create or modify any data.
CLIENT_ID=$(cat ~/.config/ima/client_id)
API_KEY=$(cat ~/.config/ima/api_key)
curl -sS -X POST "https://ima.qq.com/openapi/wiki/v1/search_knowledge_base" \
-H "ima-openapi-clientid: $CLIENT_ID" \
-H "ima-openapi-apikey: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "", "cursor": "", "limit": 1}'Expected response on success:
{
"code": 0,
"msg": "success",
"data": {
"info_list": [ /* 1 kb */ ],
"is_end": false,
"next_cursor": "…"
}
}Any code other than 0 indicates a problem. Common codes:
| Code | Meaning | Fix |
|---|---|---|
0 | Success | N/A — you're done |
| Non-zero with "没有权限" | Wrong API key, or key lacks scopes | Regenerate on the agent-interface page |
| Non-zero with "客户端" errors | Wrong client ID | Regenerate |
If curl returns an empty body or times out, check network reachability to ima.qq.com — this API does not respond to ICMP ping, so use curl -sS -o /dev/null -w "%{http_code}\n" https://ima.qq.com/ as a reachability probe instead.
diagnose.sh does this liveness check automatically whenever credentials are present, so after saving the files the user can simply run it and see a ✅ line.
Credential rotation
To rotate without losing the current session:
# 1. Generate new Client ID + API Key on https://ima.qq.com/agent-interface
# 2. Overwrite the files
printf '%s' "<new client id>" > ~/.config/ima/client_id
printf '%s' "<new api key>" > ~/.config/ima/api_key
# 3. Rerun diagnose.sh to confirm the new values are accepted
bash scripts/diagnose.shThe wrapper reads the files on every call, so no cache to invalidate.
Security considerations
- Both files are user-only readable (
600) and the containing directory is user-only accessible (700). Make sure this remains the case after rotation. - Do not check these files into git, not even into a private repo. The easiest way to prevent accidents is to keep them in
~/.config/ima/rather than inside any project directory. - If the user runs a backup tool that syncs
~/.config/, be aware that the credentials will be included in the backup. Consider excluding~/.config/ima/from backups that leave the local machine. - The IMA API currently does not scope credentials per-device — a leaked API key can be used from anywhere on the internet until it's rotated.
Installation Flow — Deep Dive
This document is a reference for the agent when walking a user through installing upstream ima-skill via scripts/install_ima_skill.sh. It is not part of the runtime hot path — read it when the install fails or when the user has questions about what's happening under the hood.
Why a wrapper installer exists
The upstream Tencent IMA skill ships as a zip file at https://app-dl.ima.qq.com/skills/ima-skills-<version>.zip. The official documentation at https://ima.qq.com/agent-interface expects users to paste a prompt into a specific proprietary agent that handles the install for them. There is no cross-platform installer in the upstream distribution.
ima-copilot bridges that gap by using the open vercel-labs/skills CLI as a last-mile distributor. The flow is:
ima.qq.com/skills/ima-skills-X.Y.Z.zip
↓ curl
/tmp/ima-copilot-staging/<ts>/
↓ unzip
/tmp/ima-copilot-staging/<ts>/ima-skill/
↓ npx skills add <local-path> -a ... -g -y
~/.claude/skills/ima-skill/ (Claude Code — may be a symlink)
~/.agents/skills/ima-skill/ (Codex — may be a symlink)
~/.openclaw/skills/ima-skill/ (OpenClaw, if installed — may be a symlink)The staging directory is deleted on exit. We rely on vercel skills' default symlink behavior: the first agent whose install succeeds becomes the canonical directory, and the remaining agents are symlinked to it. This is safe because vercel skills decouples the canonical location from the source path — once the install completes, nothing depends on the staging directory any more, so cleaning it up does not break any symlinks.
The key win of symlink mode is propagation: a Capability 3 repair applied to any one agent entry is immediately visible to the others through the symlink graph. When the user upgrades ima-skill, the new version replaces the canonical and all symlinks automatically point at the new content. diagnose.sh understands this graph via realpath and dedupes its issue reports so the user sees each underlying problem exactly once, not once per agent.
If you ever need the opposite behavior — fully independent agent copies, each with its own state and its own repair cycle — pass --copy to npx skills add manually by editing the install script. This is a rare requirement and the ima-copilot flow is not designed around it.
Prerequisites
The installer needs three tools on PATH:
| Tool | Why | How to install |
|---|---|---|
curl | Download the official zip | Preinstalled on macOS/Linux |
unzip | Extract the archive | Preinstalled on macOS/Linux |
npx | Run skills add from the npm registry on demand | Install Node.js 18+ — brew install node |
The installer checks for each and aborts with a clear message if any is missing.
Agent detection
The installer looks for well-known directory markers:
| Agent | Detection rule |
|---|---|
| Claude Code | ~/.claude exists |
| Codex | ~/.agents exists |
| OpenClaw | ~/.openclaw exists or openclaw is on PATH |
Only agents that are detected are passed to npx skills add -a .... A missing agent produces no install — we never silently write to a path the user hasn't already chosen to use.
If zero agents are detected, the installer defaults to claude-code as the most common case and prints a notice explaining why.
Version override
The installer hard-codes a known-good version for the default case. To pin a specific upstream release:
# via flag
bash scripts/install_ima_skill.sh --version x.y.z
# via environment variable
IMA_VERSION=x.y.z bash scripts/install_ima_skill.shIf the upstream URL 404s (most commonly because the version hasn't been released yet or has been yanked), the installer exits with a hint to try the next known version. The upstream release pattern from observation is ima-skills-<major>.<minor>.<patch>.zip — check https://ima.qq.com/agent-interface for the current version when in doubt.
What npx skills add actually does
npx -y skills add <path> -a <agent> -g -y breaks down as:
<path>— a local directory containing a SKILL.md at the root. The vercel-labs CLI treats this as a single skill source.-a <agent>— target agent identifier. Passing multiple-aflags installs to multiple agents in one call.-g— global scope. Installs to the agent's home directory instead of a project-local.claude/or.agents/folder.-y— non-interactive. Skip all prompts. Required for use from a script.
Notice what is not passed: --copy. In vercel skills' default mode, the CLI picks one agent's directory (usually the first one whose install succeeds) as the canonical copy and creates symlinks from every other agent's skills directory back to it. For ima-copilot's use case this is strictly better than --copy would be — a repair or upgrade applied to any one agent propagates to all of them through the symlink graph, eliminating the need to loop over every agent during a fix.
The CLI auto-detects installed agents as well, but we pass -a explicitly to avoid accidentally installing to the other 38 supported agents the user hasn't opted into.
File layout after install
After a successful install, the target directories contain the original upstream structure unchanged:
~/.claude/skills/ima-skill/
├── SKILL.md # root entry point
├── notes/
│ ├── SKILL.md # note operations (may trigger ISSUE-001)
│ └── references/
│ └── api.md
└── knowledge-base/
├── SKILL.md # knowledge base operations (known ISSUE-001)
├── references/
│ └── api.md
└── scripts/
├── cos-upload.cjs
└── preflight-check.cjsNo repair happens at install time. Any file modifications happen later, in Capability 3, with explicit user consent.
Uninstall
vercel-labs/skills has a remove command:
npx -y skills remove ima-skill -a claude-code -a codex -a openclaw -g -yThis removes the skill from each named agent's skill directory. It does not remove credentials from ~/.config/ima/ — those are managed independently by Capability 2 and may still be wanted even without an install.
Troubleshooting
"curl returned HTTP 404"
The upstream package for the requested version doesn't exist. Try a different version via --version.
"npx skills add failed"
Usually one of:
- No internet / npm registry unreachable — check network
- Node.js version too old —
node --versionshould report ≥18 - Permissions on the target directory — some WSL / sandboxed environments restrict writes to
~/.claude/skills/
"extract archive but no SKILL.md found"
The upstream archive layout changed. Manually list the archive contents:
unzip -l /tmp/ima-copilot-staging/*/ima-skills.zipIf the SKILL.md moved, open an issue on this skill (ima-copilot) with the new archive layout so the installer can be updated.
"Installed but diagnose.sh says not installed"
Most likely the agent detection logic put the install in a non-standard path for your agent. Check the known locations in diagnose.sh — if your agent uses a different path, the fix is to add it there as a new candidate.
Known Issues in Upstream ima-skill
This file is the source of truth for every upstream bug that ima-copilot can detect and help repair. Each issue has a stable ID, a plain-language explanation, at least one repair strategy, and exact commands the agent can execute on user consent.
How the agent should use this file
When scripts/diagnose.sh reports a ⚠️ line that mentions ISSUE-<NNN>, look up that issue below, then:
1. Explain to the user — in plain language — what's broken and why it matters to them. 2. If the issue has more than one repair strategy, use AskUserQuestion to present the choices. Describe each option by its outcome, not its mechanism. 3. After the user picks a strategy, execute the exact commands under that strategy. Every command backs up the original file to /tmp/ima-copilot-backups/<timestamp>/<relative-path> first. 4. Re-run diagnose.sh and show the before/after. The warning should flip to ✅. 5. Remind the user that upstream upgrades replace these files, so reruns after an upgrade are expected — and safe.
Issue registry
ISSUE-001 — Submodule SKILL.md files missing YAML frontmatter
Status: Observed on recent upstream releases. No public issue tracker for the upstream package, so there's no link to watch. When the diagnose.sh scanner stops flagging this issue against a freshly-installed upstream release, it has been fixed upstream and this entry can be closed.
Symptom: Running an ima-skill-enabled session on Codex produces:
⚠ Skipped loading 2 skill(s) due to invalid SKILL.md files.
⚠ <path>/ima-skill/notes/SKILL.md: missing YAML frontmatter delimited by ---
⚠ <path>/ima-skill/knowledge-base/SKILL.md: missing YAML frontmatter delimited by ---Claude Code's skill loader is more permissive and usually does not emit a warning, but the files still violate the documented SKILL.md format and will fail under any stricter loader that enters the ecosystem.
Root cause: The upstream package ships ima-skill/notes/SKILL.md and ima-skill/knowledge-base/SKILL.md that begin directly with # Notes (笔记) and # Knowledge Base (知识库) — no --- YAML frontmatter block.
Original design intent: Read the root ima-skill/SKILL.md. Its "模块决策表" explicitly says 读取 notes/SKILL.md or 读取 knowledge-base/SKILL.md — the upstream author meant these files as module documentation referenced from the root, not as independently-loadable skills. The problem is simply that they chose SKILL.md as the filename, which any standard skill loader recursively discovers and tries to register.
Impact if left unfixed:
- On Codex and other strict loaders: submodule content is silently dropped from the loaded skill, so note-search and knowledge-base-search instructions never reach the agent at runtime.
- On Claude Code: usually no user-visible error, but the skill directory still contains two files that violate the published SKILL.md format.
Why upstream probably hasn't fixed it: The upstream package is developed primarily against OpenClaw's loader, which appears to tolerate the missing frontmatter. The bug is invisible from the upstream maintainer's primary testing platform.
How to explain it to the user (plain language):
The official IMA skill package has two helper files inside (one for notes, one for knowledge base) that are missing a small technical header. On Codex, this makes the whole note-search and knowledge-base-search features silently fail to load — you won't see an error in your workflow, you'll just notice searches not working. On Claude Code it usually just prints a warning at startup. We can fix this in one of two ways; both are reversible.
Important — symlink sharing across agents:
npx skills add in its default mode installs to the first detected agent as a canonical directory and symlinks the remaining agents to it. scripts/diagnose.sh detects this sharing automatically and reports ℹ️ claude-code and codex share the same install via symlink (canonical: ...). When this happens:
- Only run the repair once, against any one of the shared agent paths. The fix propagates through the symlink graph to every other agent instantly.
- The diagnose report groups its ISSUE lines by canonical directory, so you'll see 2 warnings (one per submodule file), not 4.
- The backup step saves the canonical files once — restoring from backup also propagates to every agent via the same symlink graph.
If npx skills add was run with --copy (or if the user manually desynced the installs), each agent has its own copy and the repair must be applied separately to each. Diagnose will report this by showing the ISSUE lines without a prior "share via symlink" line.
Repair strategies:
Strategy A — Rename submodule files to MODULE.md (recommended)
Respects the upstream design intent ("these are module documentation, not sub-skills") by renaming them so no loader tries to register them as independent skills. Requires a one-line patch to the root SKILL.md so its internal references still resolve.
What this strategy changes:
<install>/notes/SKILL.md→<install>/notes/MODULE.md<install>/knowledge-base/SKILL.md→<install>/knowledge-base/MODULE.md<install>/SKILL.md— onesedto rewrite internal references fromnotes/SKILL.md→notes/MODULE.mdandknowledge-base/SKILL.md→knowledge-base/MODULE.md
Commands (agent executes after user consent; replace <install> with the specific agent path from diagnose.sh):
# Use `command cp` / `command mv` to bypass any user-defined shell aliases
# (e.g. `alias mv='mv -i'`). Interactive-mode aliases will otherwise hang the
# script on an "overwrite?" prompt since this flow runs non-interactively.
# 1. Back up originals (each cp is guarded so reruns don't emit "file not found")
BACKUP="/tmp/ima-copilot-backups/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BACKUP"
[ -f "<install>/SKILL.md" ] && \
command cp "<install>/SKILL.md" "$BACKUP/SKILL.md"
[ -f "<install>/notes/SKILL.md" ] && \
command cp "<install>/notes/SKILL.md" "$BACKUP/notes-SKILL.md"
[ -f "<install>/knowledge-base/SKILL.md" ] && \
command cp "<install>/knowledge-base/SKILL.md" "$BACKUP/knowledge-base-SKILL.md"
echo "backup saved to: $BACKUP"
# 2. Rename submodule files (skip if already renamed — idempotent)
[ -f "<install>/notes/SKILL.md" ] && \
command mv "<install>/notes/SKILL.md" "<install>/notes/MODULE.md"
[ -f "<install>/knowledge-base/SKILL.md" ] && \
command mv "<install>/knowledge-base/SKILL.md" "<install>/knowledge-base/MODULE.md"
# 3. Patch root SKILL.md references (idempotent — no-op if already patched).
# `command sed` and `command rm` bypass any user-defined shell aliases like
# `alias sed='sed -i'` or `alias rm='rm -i'` that would otherwise hang the
# script on a prompt or misinterpret the -i flag.
command sed -i.bak \
-e 's|notes/SKILL\.md|notes/MODULE.md|g' \
-e 's|knowledge-base/SKILL\.md|knowledge-base/MODULE.md|g' \
"<install>/SKILL.md"
command rm -f "<install>/SKILL.md.bak"Rollback (if the user later wants to undo):
command cp "$BACKUP/SKILL.md" "<install>/SKILL.md"
command cp "$BACKUP/notes-SKILL.md" "<install>/notes/SKILL.md"
command cp "$BACKUP/knowledge-base-SKILL.md" "<install>/knowledge-base/SKILL.md"
command rm -f "<install>/notes/MODULE.md" "<install>/knowledge-base/MODULE.md"Pros: Honors the upstream author's original design. Minimizes the total set of files in the skill namespace. No risk of loader collision between the root skill and the two submodule "sub-skills".
Cons: Diff is slightly larger (3 files touched). If upstream later decides to fix the bug with Strategy B, the user's rename will diverge from upstream's version until the next install.
Strategy B — Add minimal frontmatter to the submodule files
Leaves file names alone. Prepends a 4-line YAML frontmatter block to each submodule file so strict loaders accept them. Technically creates two "sub-skills" named ima-skill-notes and ima-skill-knowledge-base which will appear in some UIs.
What this strategy changes:
<install>/notes/SKILL.md— prepend frontmatter<install>/knowledge-base/SKILL.md— prepend frontmatter
Commands:
# Use `command cp` / `command mv` to bypass interactive-mode shell aliases
# (e.g. `alias mv='mv -i'`) that would otherwise hang the script.
BACKUP="/tmp/ima-copilot-backups/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BACKUP"
[ -f "<install>/notes/SKILL.md" ] && \
command cp "<install>/notes/SKILL.md" "$BACKUP/notes-SKILL.md"
[ -f "<install>/knowledge-base/SKILL.md" ] && \
command cp "<install>/knowledge-base/SKILL.md" "$BACKUP/knowledge-base-SKILL.md"
echo "backup saved to: $BACKUP"
# Idempotent prepend — skip if the file already starts with ---
prepend_frontmatter() {
local file="$1"
local name="$2"
local desc="$3"
if head -n 1 "$file" | grep -q '^---$'; then
echo "already has frontmatter: $file"
return 0
fi
local tmp
tmp=$(mktemp)
{
printf -- '---\n'
printf -- 'name: %s\n' "$name"
printf -- 'description: %s\n' "$desc"
printf -- '---\n\n'
cat "$file"
} > "$tmp"
command mv "$tmp" "$file"
}
prepend_frontmatter \
"<install>/notes/SKILL.md" \
"ima-skill-notes" \
"IMA notes submodule. Read via the root ima-skill module decision table."
prepend_frontmatter \
"<install>/knowledge-base/SKILL.md" \
"ima-skill-knowledge-base" \
"IMA knowledge-base submodule. Read via the root ima-skill module decision table."Rollback:
command cp "$BACKUP/notes-SKILL.md" "<install>/notes/SKILL.md"
command cp "$BACKUP/knowledge-base-SKILL.md" "<install>/knowledge-base/SKILL.md"Pros: Smallest possible diff. Exact commands that Codex's first-pass fix used. Easiest to recreate from memory if the user loses the script.
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.
Strategy skip — Leave the file alone
Valid 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.
Adding new issues to this file
When we discover a new upstream bug:
1. Assign the next sequential ISSUE-<NNN> number. 2. Fill in the same sections: symptom, root cause, impact, plain-language explanation, at least one strategy with idempotent + reversible commands. 3. Update scripts/diagnose.sh to detect it (still read-only) and print a line with the same issue ID. 4. 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.
Search Best Practices — Deep Dive
This 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.
The three hard constraints of the IMA search API
Any search workflow on top of IMA has to account for these three constraints. They are not documented by upstream; they were discovered by observation.
1. No cross-knowledge-base endpoint
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.
2. No relevance score in the response
A hit object looks like this:
{
"media_id": "wechatarticle_…",
"title": "…",
"parent_folder_id": "…",
"highlight_content": "…",
"media_type": 6
}That'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.
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.
3. Silent 100-result truncation
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.
High-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.
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.
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.
Permission model: subscribed KBs return 'no permission'
Empirically, 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.
This is not configurable on the client side — it is a server-side entitlement check tied to KB ownership.
Consequence for the wrapper:
- 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.
- Do not render denied KBs in the main results area — they are noise that drowns out real hits.
- Collect them in a separate "ℹ️ subscribed KBs (no search permission)" block at the end of the output.
search_fanout.py implements this partitioning in its rank_groups() function using the 220030 error code as the partition key.
The fan-out strategy
load credentials
load ~/.config/ima/copilot.json (optional)
enumerate all KBs via search_knowledge_base("")
filter out KBs in skip_kbs
fan out search_knowledge to the remainder, in parallel (default 12 workers)
partition results into: priority | others | denied | empty
render:
priority group first, in user-declared order
others group next, sorted by hit count descending
summary line
denied group at the bottom (as an ℹ️ note, not a result)
truncated warning (if any)Every 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.
The copilot.json configuration file
Location: ~/.config/ima/copilot.json. Override with IMA_COPILOT_CONFIG=<path> environment variable (primarily used by tests).
Shape:
{
"priority_kbs": ["kb name 1", "kb name 2"],
"skip_kbs": ["kb name 3"],
"fanout_strategy": "parallel-then-merge"
}priority_kbs (list of strings)
KB 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.
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.
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.
skip_kbs (list of strings)
KB 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.
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.
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.
fanout_strategy (string, reserved)
Currently only "parallel-then-merge" is implemented. The field is kept in the schema for forward compatibility.
Evidence-based subset detection
Before 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.
Verification procedure:
1. 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. 2. For each query, run the two KBs separately via search_knowledge and collect the set of title values from each. 3. 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. 4. If truncated results show up (exactly 100 hits), discard that query — the set difference will be a truncation artifact, not a real content difference.
The 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.
Rendering details
Text mode (the default):
- Each KB group is a header line with an emoji (
🥇for priority,📚for others) and a hit count. - The first
--max-resultshits per KB are listed with title and highlight snippet (truncated to 120 chars). - "N more" is printed if hits exceed the limit.
- A separator line precedes the summary.
- The summary shows total hits and total KBs with results.
- Denied KBs are printed as an
ℹ️block at the bottom so they never drown out real results. - Truncated KBs are printed as a
⚠️block with guidance to narrow the query.
JSON mode (--json): emits {priority, others, denied, skipped_by_config} arrays with full hit metadata for downstream tools.
When the agent should use this capability
Trigger on explicit search intents:
- "搜一下 XXX"
- "search for XXX in my IMA notes"
- "find articles about XXX"
- "在 ima 里搜 XXX"
- "知识库里有没有 XXX"
Also 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:
- "what was that RAG framework the author of the HyDE paper mentioned?"
- "I read something last month about Qwen3 fine-tuning, what was the key takeaway?"
For 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.
When the agent should refuse
Refuse (or at least suggest alternatives) when the user asks to:
- Fuzzy-search across all KBs with a single vague word — this will likely truncate and give poor results. Suggest narrower queries first.
- Rank results by recency — the API doesn't return timestamps; any such ranking would be a lie.
- 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.
#!/usr/bin/env bash
#
# diagnose.sh — Read-only health check for upstream ima-skill installs.
#
# Prints one status line per check, then a summary.
#
# Exit codes:
# 0 — all checks passed
# 1 — one or more issues need user action
# 2 — diagnostic itself failed (network error, missing tooling)
#
# This script is strictly read-only. It will never modify, create, or delete
# any file outside its own stdout. Safe to run as many times as you want.
set -uo pipefail
PASS=0
WARN=0
FAIL=0
status_ok() { echo "✅ $1"; PASS=$((PASS + 1)); }
status_warn() { echo "⚠️ $1"; WARN=$((WARN + 1)); }
status_fail() { echo "❌ $1"; FAIL=$((FAIL + 1)); }
echo "=== ima-copilot diagnostic report ==="
echo
# ==========================================================================
# 1. Upstream ima-skill install presence
# ==========================================================================
echo "--- Upstream ima-skill installs ---"
# Agent target path resolution. Each agent has a short list of known
# candidate install paths; the first one with a SKILL.md wins.
find_install() {
local agent="$1"; shift
local path
for path in "$@"; do
if [ -f "$path/SKILL.md" ]; then
echo "$path"
return 0
fi
done
return 1
}
# Resolve a path to its canonical realpath so we can detect when two agent
# entries point at the same underlying directory via symlink. `npx skills add`
# in its default mode promotes the first agent's install to canonical and
# symlinks the rest to it — reporting issues four times when there are only
# two real files is noisy and confuses the repair step counting.
canonical() {
python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$1" 2>/dev/null || echo "$1"
}
CLAUDE_PATH=""
CODEX_PATH=""
OPENCLAW_PATH=""
INSTALLED_AGENTS=""
# Claude Code
if CLAUDE_PATH=$(find_install claude-code \
"$HOME/.claude/skills/ima-skill"); then
status_ok "ima-skill installed (claude-code) at $CLAUDE_PATH"
INSTALLED_AGENTS="$INSTALLED_AGENTS claude-code"
else
status_warn "ima-skill NOT installed (claude-code) — run install_ima_skill.sh"
fi
# Codex
if CODEX_PATH=$(find_install codex \
"$HOME/.agents/skills/ima-skill" \
"$HOME/.codex/skills/ima-skill"); then
status_ok "ima-skill installed (codex) at $CODEX_PATH"
INSTALLED_AGENTS="$INSTALLED_AGENTS codex"
else
status_warn "ima-skill NOT installed (codex) — run install_ima_skill.sh"
fi
# OpenClaw — multiple candidate paths because the standard hasn't stabilized
if OPENCLAW_PATH=$(find_install openclaw \
"$HOME/.openclaw/skills/ima-skill" \
"$HOME/.config/openclaw/skills/ima-skill" \
"$HOME/.local/share/openclaw/skills/ima-skill"); then
status_ok "ima-skill installed (openclaw) at $OPENCLAW_PATH"
INSTALLED_AGENTS="$INSTALLED_AGENTS openclaw"
else
status_warn "ima-skill NOT installed (openclaw) — run install_ima_skill.sh"
fi
# Detect whether multiple agents share the same underlying directory via
# symlink. This matters for the issue scanner: we don't want to report the
# same ISSUE-001 four times when there are really only two files behind
# symlinks.
CLAUDE_REAL=""
CODEX_REAL=""
OPENCLAW_REAL=""
[ -n "$CLAUDE_PATH" ] && CLAUDE_REAL=$(canonical "$CLAUDE_PATH")
[ -n "$CODEX_PATH" ] && CODEX_REAL=$(canonical "$CODEX_PATH")
[ -n "$OPENCLAW_PATH" ] && OPENCLAW_REAL=$(canonical "$OPENCLAW_PATH")
# Report sharing if any two agents resolve to the same canonical directory
if [ -n "$CLAUDE_REAL" ] && [ -n "$CODEX_REAL" ] && [ "$CLAUDE_REAL" = "$CODEX_REAL" ]; then
echo "ℹ️ claude-code and codex share the same install via symlink (canonical: $CLAUDE_REAL)"
fi
if [ -n "$CLAUDE_REAL" ] && [ -n "$OPENCLAW_REAL" ] && [ "$CLAUDE_REAL" = "$OPENCLAW_REAL" ]; then
echo "ℹ️ claude-code and openclaw share the same install via symlink (canonical: $CLAUDE_REAL)"
fi
if [ -n "$CODEX_REAL" ] && [ -n "$OPENCLAW_REAL" ] && [ "$CODEX_REAL" = "$OPENCLAW_REAL" ] && [ "$CODEX_REAL" != "$CLAUDE_REAL" ]; then
echo "ℹ️ codex and openclaw share the same install via symlink (canonical: $CODEX_REAL)"
fi
if [ -z "$INSTALLED_AGENTS" ]; then
echo
echo "No installs found across any supported agent."
echo "Start with: bash \"\$(dirname \"\$0\")/install_ima_skill.sh\""
exit 1
fi
echo
# ==========================================================================
# 2. API credentials presence and liveness
# ==========================================================================
echo "--- API credentials ---"
CLIENT_ID="${IMA_OPENAPI_CLIENTID:-}"
API_KEY="${IMA_OPENAPI_APIKEY:-}"
if [ -z "$CLIENT_ID" ] && [ -f "$HOME/.config/ima/client_id" ]; then
CLIENT_ID=$(tr -d '\n' < "$HOME/.config/ima/client_id")
fi
if [ -z "$API_KEY" ] && [ -f "$HOME/.config/ima/api_key" ]; then
API_KEY=$(tr -d '\n' < "$HOME/.config/ima/api_key")
fi
if [ -z "$CLIENT_ID" ] || [ -z "$API_KEY" ]; then
status_fail "API credentials missing (expected env vars or ~/.config/ima/{client_id,api_key})"
else
status_ok "API credentials present"
if ! command -v curl >/dev/null 2>&1; then
status_warn "curl not on PATH — skipping liveness check"
else
response=$(curl -sS -X POST "https://ima.qq.com/openapi/wiki/v1/search_knowledge_base" \
-H "ima-openapi-clientid: $CLIENT_ID" \
-H "ima-openapi-apikey: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "", "cursor": "", "limit": 1}' 2>/dev/null || true)
if echo "$response" | grep -q '"code"[[:space:]]*:[[:space:]]*0'; then
status_ok "API credentials verified by live liveness call"
elif [ -z "$response" ]; then
status_fail "API liveness call returned no response (network issue?)"
else
status_fail "API liveness call failed — server rejected credentials or returned error"
# Show a short snippet for debugging without dumping everything
snippet=$(printf '%s' "$response" | head -c 200)
echo " response: $snippet"
fi
fi
fi
echo
# ==========================================================================
# 3. Known issue scan
# ==========================================================================
echo "--- Known upstream issues ---"
# ISSUE-001 — submodule SKILL.md missing YAML frontmatter
#
# Symptom: loaders like Codex's ~/.agents scanner skip the submodule SKILL.md
# files and log "missing YAML frontmatter delimited by ---". Claude Code is
# more lenient and usually loads them anyway, but the official design intent
# is still that these files are module documentation, and fixing them removes
# the loader warning universally.
#
# The check has to understand three post-install states:
# - Untouched upstream: SKILL.md exists, starts with "#" (broken) or "---" (fixed upstream).
# - Strategy A applied: SKILL.md is renamed to MODULE.md, so SKILL.md no longer exists.
# - Strategy B applied: SKILL.md exists, now begins with "---".
#
# Return codes:
# 0 — OK (either upstream-original good or Strategy B applied)
# 1 — broken (file exists but lacks frontmatter)
# 2 — submodule not present at all (legitimate for a future upstream layout change)
# 3 — Strategy A applied (renamed to MODULE.md)
# 4 — conflicted: both SKILL.md and MODULE.md exist simultaneously
# (happens if a user switched repair strategies mid-session or
# restored a partial backup — the agent needs to pick a winning state)
check_submodule() {
local dir="$1"
local skill_md="$dir/SKILL.md"
local module_md="$dir/MODULE.md"
# Check dual-state first — if both files exist, the install is in a
# conflicted state that neither Strategy A nor Strategy B can claim cleanly.
if [ -f "$skill_md" ] && [ -f "$module_md" ]; then
return 4
fi
if [ -f "$skill_md" ]; then
local first_line
first_line=$(head -n 1 "$skill_md" 2>/dev/null || echo "")
if [ "$first_line" = "---" ]; then
return 0
fi
return 1
fi
if [ -f "$module_md" ]; then
return 3
fi
return 2
}
scan_issue_001() {
local agent="$1"
local base="$2"
local sub dir rc
for sub in notes knowledge-base; do
dir="$base/$sub"
check_submodule "$dir"
rc=$?
case "$rc" in
0) status_ok "ISSUE-001 clear ($agent: $sub/SKILL.md has frontmatter)" ;;
3) status_ok "ISSUE-001 clear ($agent: $sub/MODULE.md — Strategy A applied)" ;;
4) status_warn "ISSUE-001 CONFLICTED ($agent: both $sub/SKILL.md and $sub/MODULE.md exist — pick one; see known_issues.md)" ;;
2) echo "ℹ️ $agent: $sub submodule not present (post-upstream-layout-change?)" ;;
*) status_warn "ISSUE-001 TRIGGERED ($agent: $sub/SKILL.md missing YAML frontmatter)" ;;
esac
done
}
# Scan each unique canonical directory exactly once. When multiple agents
# share the same underlying install via symlink, scanning one represents all.
SCANNED_REALS=""
scan_agent() {
local agent="$1"
local path="$2"
local real="$3"
if [ -z "$path" ]; then
return
fi
case " $SCANNED_REALS " in
*" $real "*)
# Already scanned via another agent entry
return
;;
esac
SCANNED_REALS="$SCANNED_REALS $real"
scan_issue_001 "$agent" "$path"
}
scan_agent "claude-code" "$CLAUDE_PATH" "$CLAUDE_REAL"
scan_agent "codex" "$CODEX_PATH" "$CODEX_REAL"
scan_agent "openclaw" "$OPENCLAW_PATH" "$OPENCLAW_REAL"
echo
# ==========================================================================
# 4. Summary and exit code
# ==========================================================================
echo "--- Summary ---"
echo " ✅ ${PASS} pass ⚠️ ${WARN} warn ❌ ${FAIL} fail"
echo
if [ "$FAIL" -gt 0 ] || [ "$WARN" -gt 0 ]; then
echo "Next step: open references/known_issues.md and walk the agent through"
echo "the warnings above. Each issue ID maps to a concrete repair procedure."
exit 1
fi
exit 0
#!/usr/bin/env bash
#
# install_ima_skill.sh — Install the upstream Tencent ima-skill to Claude Code,
# Codex, and OpenClaw in one shot.
#
# Flow:
# 1. Download the official zip from ima.qq.com
# 2. Stage it in a temp directory
# 3. Detect which of the three target agents are installed locally
# 4. Delegate to `npx skills add <local-path>` (vercel-labs/skills) in its
# default symlink mode so that the three agents share a single canonical
# copy — a repair or upgrade applied once propagates to every agent.
# 5. Clean up the staging dir on exit (safe: vercel skills promotes the
# first agent's install to canonical and symlinks the rest to it,
# independent of the staging source)
#
# Re-run safely — every step is idempotent. `npx skills add` will overwrite
# existing ima-skill installs with the new version, and the symlink graph
# gets rebuilt on every run.
set -euo pipefail
IMA_VERSION="${IMA_VERSION:-1.1.2}"
BASE_URL="https://app-dl.ima.qq.com/skills"
STAGING_ROOT="/tmp/ima-copilot-staging"
STAGING_DIR="${STAGING_ROOT}/$(date +%s)-$$"
cleanup() {
if [ -n "${STAGING_DIR:-}" ] && [ -d "$STAGING_DIR" ]; then
rm -rf "$STAGING_DIR"
fi
}
trap cleanup EXIT
usage() {
cat <<'EOF'
Usage: install_ima_skill.sh [--version <x.y.z>]
Downloads the upstream Tencent ima-skill and installs it globally to the
supported coding agents (Claude Code, Codex, OpenClaw) that are detected on
this machine. Uses vercel-labs/skills CLI (`npx skills add`) as the
distribution mechanism, in vercel's default symlink mode so that a repair or
upgrade applied to any one of the three agent directories propagates through
the symlink graph to every other agent automatically.
Environment overrides:
IMA_VERSION Upstream version to install (default: 1.1.2)
Examples:
install_ima_skill.sh
install_ima_skill.sh --version 1.1.2
IMA_VERSION=1.2.0 install_ima_skill.sh
Find the latest upstream version at https://ima.qq.com/agent-interface
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
--version)
IMA_VERSION="$2"
shift 2
;;
--version=*)
IMA_VERSION="${1#*=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
# Require basic tools
for tool in curl unzip npx; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "✗ Required tool not found on PATH: $tool" >&2
exit 1
fi
done
# Require Node.js >= 18 — `npx -y skills add` from vercel-labs/skills needs
# a modern Node runtime. The error is otherwise opaque if it fires on an
# ancient Node version.
if command -v node >/dev/null 2>&1; then
node_major=$(node --version 2>/dev/null | sed -E 's/^v([0-9]+).*/\1/')
if [ -n "$node_major" ] && [ "$node_major" -lt 18 ] 2>/dev/null; then
echo "✗ Node.js 18+ required for 'npx skills add' — found: $(node --version)" >&2
echo " Upgrade via your package manager (brew/apt/nvm) and retry." >&2
exit 1
fi
fi
echo "▶ Staging upstream ima-skill v${IMA_VERSION}"
mkdir -p "$STAGING_DIR"
ZIP_URL="${BASE_URL}/ima-skills-${IMA_VERSION}.zip"
ZIP_PATH="${STAGING_DIR}/ima-skills.zip"
echo " Downloading ${ZIP_URL}"
http_code=$(curl -sS -L --fail -o "$ZIP_PATH" -w "%{http_code}" "$ZIP_URL" || echo "000")
if [ "$http_code" != "200" ]; then
echo "" >&2
echo "✗ Download failed (HTTP ${http_code})" >&2
echo "" >&2
echo "If IMA has released a newer version, pass it explicitly:" >&2
echo " IMA_VERSION=x.y.z bash $0" >&2
echo "" >&2
echo "or find the latest version at https://ima.qq.com/agent-interface" >&2
exit 1
fi
actual_size=$(wc -c < "$ZIP_PATH" | tr -d ' ')
echo " Downloaded ${actual_size} bytes"
if [ "$actual_size" -lt 1000 ]; then
echo "✗ Downloaded file is suspiciously small — aborting before extraction" >&2
exit 1
fi
echo " Extracting…"
unzip -q -o "$ZIP_PATH" -d "$STAGING_DIR"
# Locate the root ima-skill directory inside the extracted archive.
#
# This matters more than it looks. The upstream 1.1.2 archive contains SKILL.md
# at three depths (root, notes/, knowledge-base/) because ISSUE-001 exists —
# notes/SKILL.md and knowledge-base/SKILL.md are documented as "module files"
# but happen to use the same filename as the real root. A naive "first SKILL.md
# we find" strategy will pick up the shallowest one if we're lucky and a
# submodule if we're not, breaking the install non-deterministically.
#
# Resolution: prefer the well-known layout (<staging>/ima-skill/SKILL.md), and
# only fall back to a recursive scan if that layout has changed in a future
# release. The fallback picks the shallowest candidate, which is the root by
# construction of every legal SKILL.md tree.
SKILL_SRC=""
if [ -f "$STAGING_DIR/ima-skill/SKILL.md" ]; then
SKILL_SRC="$STAGING_DIR/ima-skill"
else
shallowest_depth=999
while IFS= read -r candidate; do
# Count slashes in the relative portion to compare depths uniformly
rel="${candidate#$STAGING_DIR/}"
depth=$(awk -F/ '{print NF}' <<< "$rel")
if [ "$depth" -lt "$shallowest_depth" ]; then
shallowest_depth="$depth"
SKILL_SRC=$(dirname "$candidate")
fi
done < <(find "$STAGING_DIR" -maxdepth 4 -type f -name SKILL.md -print)
fi
if [ -z "$SKILL_SRC" ]; then
echo "✗ Could not locate SKILL.md in extracted archive" >&2
echo " Archive contents:" >&2
find "$STAGING_DIR" -maxdepth 4 -type f -print >&2 || true
exit 1
fi
echo " Found root SKILL.md at: ${SKILL_SRC}"
# Detect which target agents are installed. Being present is a proxy for
# "the user wants things installed here"; absence means skip silently rather
# than install anywhere they haven't opted in.
AGENTS=()
[ -d "$HOME/.claude" ] && AGENTS+=("claude-code")
[ -d "$HOME/.agents" ] && AGENTS+=("codex")
if [ -d "$HOME/.openclaw" ] || command -v openclaw >/dev/null 2>&1; then
AGENTS+=("openclaw")
fi
if [ ${#AGENTS[@]} -eq 0 ]; then
echo "" >&2
echo "⚠ No supported agent detected on this machine." >&2
echo " Looked for: ~/.claude (Claude Code), ~/.agents (Codex), openclaw command." >&2
echo " Defaulting to claude-code as the most common case." >&2
echo "" >&2
AGENTS=("claude-code")
fi
echo "▶ Targeting agents: ${AGENTS[*]}"
AGENT_FLAGS=()
for a in "${AGENTS[@]}"; do
AGENT_FLAGS+=("-a" "$a")
done
echo " Running: npx -y skills add \"${SKILL_SRC}\" -g -y ${AGENT_FLAGS[*]}"
if ! npx -y skills add "$SKILL_SRC" -g -y "${AGENT_FLAGS[@]}"; then
echo "✗ npx skills add failed" >&2
echo " Make sure Node.js is installed and the npm registry is reachable." >&2
exit 1
fi
echo ""
echo "✓ Upstream ima-skill v${IMA_VERSION} installed successfully"
echo ""
echo "Next steps:"
echo " 1. Configure API credentials"
echo " Save your Client ID and API Key from https://ima.qq.com/agent-interface"
echo " into ~/.config/ima/client_id and ~/.config/ima/api_key (mode 600)."
echo ""
echo " 2. Run the diagnostic"
echo " bash \"\$(dirname \"\$0\")/diagnose.sh\""
echo ""
echo " 3. Let the agent drive repairs"
echo " The diagnostic flags known upstream issues — rerun via your agent"
echo " and it will walk you through the fixes, asking consent for each."
#!/usr/bin/env python3
"""
search_fanout.py — Fan-out search across all IMA knowledge bases with
user-defined priority boosting and subset-KB skipping.
Rationale for this shape:
IMA's OpenAPI has three constraints that force every serious search tool
into the same pattern:
1. No cross-KB endpoint — search_knowledge takes a single knowledge_base_id.
2. No relevance score in results — ranking must be client-side.
3. Silent 100-hit truncation with no cursor — queries that saturate a KB
are invisibly capped.
Given those constraints, the most useful thing a personal wrapper can do
is (a) fan out to every KB in parallel, (b) warn the user about truncated
KBs, and (c) group results by KB with user-declared priority groups
floated to the top. That's what this script does.
Config:
~/.config/ima/copilot.json — optional. Shape:
{
"priority_kbs": ["kb name 1", "kb name 2"], # hits from these go first
"skip_kbs": ["kb name 3"], # silently skip (e.g. strict
# subset of a priority KB)
"fanout_strategy": "parallel-then-merge" # reserved for future modes
}
Credentials:
Env vars IMA_OPENAPI_CLIENTID and IMA_OPENAPI_APIKEY take precedence.
Falls back to ~/.config/ima/client_id and ~/.config/ima/api_key.
Usage:
python3 search_fanout.py "your query here"
python3 search_fanout.py --max-results 5 "your query"
python3 search_fanout.py --json "your query"
"""
import argparse
import concurrent.futures
import json
import os
import sys
import urllib.error
import urllib.request
from pathlib import Path
API_BASE = "https://ima.qq.com/openapi"
HARD_HIT_CAP = 100 # search_knowledge silently caps at 100, no cursor
def load_credentials():
client_id = os.environ.get("IMA_OPENAPI_CLIENTID", "").strip()
api_key = os.environ.get("IMA_OPENAPI_APIKEY", "").strip()
config_dir = Path.home() / ".config" / "ima"
if not client_id:
p = config_dir / "client_id"
if p.is_file():
client_id = p.read_text().strip()
if not api_key:
p = config_dir / "api_key"
if p.is_file():
api_key = p.read_text().strip()
if not client_id or not api_key:
sys.exit(
"error: credentials not found.\n"
" set IMA_OPENAPI_CLIENTID and IMA_OPENAPI_APIKEY, or write them to\n"
" ~/.config/ima/client_id and ~/.config/ima/api_key (mode 600)."
)
return client_id, api_key
def load_config():
"""
Return (priority_kbs, skip_kbs, strategy). Missing config → empty preferences.
Config path is resolved in this order:
1. $IMA_COPILOT_CONFIG if set — used by tests and advanced users
2. ~/.config/ima/copilot.json — the default XDG location
"""
env_override = os.environ.get("IMA_COPILOT_CONFIG", "").strip()
if env_override:
p = Path(env_override)
else:
p = Path.home() / ".config" / "ima" / "copilot.json"
if not p.is_file():
return [], [], "parallel-then-merge"
try:
data = json.loads(p.read_text())
except json.JSONDecodeError as e:
sys.exit(f"error: ~/.config/ima/copilot.json is not valid JSON: {e}")
return (
list(data.get("priority_kbs", []) or []),
list(data.get("skip_kbs", []) or []),
data.get("fanout_strategy", "parallel-then-merge"),
)
def api_post(path, body, client_id, api_key):
"""POST JSON to the IMA API. Returns parsed dict on success, raises on failure."""
url = f"{API_BASE}/{path}"
req = urllib.request.Request(
url,
data=json.dumps(body).encode("utf-8"),
method="POST",
headers={
"ima-openapi-clientid": client_id,
"ima-openapi-apikey": api_key,
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
payload = resp.read().decode("utf-8")
except urllib.error.HTTPError as e:
raise RuntimeError(f"HTTP {e.code}: {e.read().decode('utf-8', 'replace')[:200]}")
except urllib.error.URLError as e:
raise RuntimeError(f"network error: {e.reason}")
data = json.loads(payload)
if data.get("code") != 0:
raise RuntimeError(f"API error code={data.get('code')} msg={data.get('msg')}")
return data.get("data", {})
def list_all_kbs(client_id, api_key):
"""Paginate through search_knowledge_base with query='' to enumerate every KB."""
kbs = []
cursor = ""
while True:
data = api_post(
"wiki/v1/search_knowledge_base",
{"query": "", "cursor": cursor, "limit": 50},
client_id,
api_key,
)
kbs.extend(data.get("info_list", []))
if data.get("is_end") or not data.get("next_cursor"):
break
cursor = data["next_cursor"]
if len(kbs) > 500: # defensive upper bound
break
return kbs
def search_one_kb(kb, query, client_id, api_key):
"""Returns a dict with kb info + hits + truncation flag. Never raises; errors captured."""
try:
data = api_post(
"wiki/v1/search_knowledge",
{"query": query, "knowledge_base_id": kb["kb_id"], "cursor": ""},
client_id,
api_key,
)
except RuntimeError as e:
return {"kb": kb, "hits": [], "truncated": False, "error": str(e)}
hits = data.get("info_list", []) or []
# Silent truncation detection: exact 100 hits with no is_end/next_cursor
# in the response is the upstream tell-tale.
truncated = len(hits) >= HARD_HIT_CAP and not data.get("is_end") and not data.get("next_cursor")
return {"kb": kb, "hits": hits, "truncated": truncated, "error": None}
PERMISSION_DENIED_MARKER = "220030"
def is_permission_denied(result):
return result["error"] is not None and PERMISSION_DENIED_MARKER in result["error"]
def rank_groups(results, priority_kbs, skip_kbs):
"""
Partition the per-KB results into four buckets:
- priority: kbs named in priority_kbs (in that order), with >0 hits
- others: every other searchable kb with >0 hits, sorted by hit count
- denied: kbs that came back with "no permission" — this is common for
subscribed (read-only) KBs where the user doesn't own search
access. Collected separately so the user sees the list but
it doesn't drown out real results.
- empty: searchable kbs that simply had 0 hits for this query
(silenced in the text renderer to keep output tight)
"""
skip_set = set(skip_kbs)
priority_order = list(priority_kbs)
by_name = {r["kb"]["kb_name"]: r for r in results}
priority, denied, others, empty = [], [], [], []
# Priority group — walk in user-declared order so the output matches intent
for name in priority_order:
r = by_name.pop(name, None)
if r is None or name in skip_set:
continue
if is_permission_denied(r):
denied.append(r)
elif r["hits"]:
priority.append(r)
else:
empty.append(r)
# Everyone else
for name, r in by_name.items():
if name in skip_set:
continue
if is_permission_denied(r):
denied.append(r)
elif r["hits"]:
others.append(r)
else:
empty.append(r)
# Sort primarily by hit count descending, secondarily by KB name ascending
# for stable deterministic output. Without the secondary key, tied KBs
# would be ordered by the concurrent.futures.ThreadPoolExecutor.map
# completion order, which depends on network timing and is not reproducible
# across runs. The kb_name tiebreaker makes the output byte-identical for
# identical query + identical KB set regardless of network timing.
others.sort(key=lambda r: (-len(r["hits"]), r["kb"]["kb_name"]))
return priority, others, denied, empty
def truncate(s, n=120):
s = s.replace("\n", " ")
return s if len(s) <= n else s[: n - 1] + "…"
def render_text(query, priority, others, denied, skipped_cfg, total_kbs_listed, max_per_kb):
searchable_with_hits = len(priority) + len(others)
total_hits = sum(len(r["hits"]) for r in priority + others)
truncated_kbs = [r["kb"]["kb_name"] for r in priority + others if r["truncated"]]
print(f'\n🔍 Searched "{query}" across {total_kbs_listed} knowledge bases')
if skipped_cfg:
names = ", ".join(r["kb"]["kb_name"] for r in skipped_cfg)
print(f" skipped via config: {names}")
print()
def render_group(prefix, group, note=""):
for r in group:
kb = r["kb"]
hits = r["hits"]
label = f"{prefix} {kb['kb_name']} — {len(hits)} hit{'s' if len(hits) != 1 else ''}"
if note:
label += f" {note}"
if r["truncated"]:
label += " (⚠️ truncated at 100)"
print(label)
for i, hit in enumerate(hits[:max_per_kb], 1):
title = hit.get("title", "(no title)")
print(f" {i}. {title}")
snippet = hit.get("highlight_content", "") or ""
if snippet:
print(f" {truncate(snippet)}")
if len(hits) > max_per_kb:
print(f" … ({len(hits) - max_per_kb} more)")
print()
if priority:
render_group("🥇", priority, "(priority)")
if others:
render_group("📚", others)
if not priority and not others:
print("(no hits in any searchable knowledge base)\n")
print("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
print(f"Total: {total_hits} hits across {searchable_with_hits} kb(s) with results")
if denied:
print(
f"\nℹ️ {len(denied)} kb(s) returned 'no permission' (typical for subscribed/read-only KBs):"
)
for r in denied:
print(f" - {r['kb']['kb_name']}")
if truncated_kbs:
print(
f"\n⚠️ {len(truncated_kbs)} kb(s) hit the 100-result ceiling; try a narrower query:"
)
for name in truncated_kbs:
print(f" - {name}")
def render_json(query, priority, others, denied, skipped_cfg):
def export(group):
return [
{
"kb_id": r["kb"]["kb_id"],
"kb_name": r["kb"]["kb_name"],
"hit_count": len(r["hits"]),
"truncated": r["truncated"],
"error": r["error"],
"hits": [
{
"title": h.get("title"),
"media_id": h.get("media_id"),
"parent_folder_id": h.get("parent_folder_id"),
"highlight_content": h.get("highlight_content"),
}
for h in r["hits"]
],
}
for r in group
]
out = {
"query": query,
"priority": export(priority),
"others": export(others),
"denied": [r["kb"]["kb_name"] for r in denied],
"skipped_by_config": [r["kb"]["kb_name"] for r in skipped_cfg],
}
print(json.dumps(out, ensure_ascii=False, indent=2))
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__.split("\n")[1])
ap.add_argument("query", help="Search query string")
ap.add_argument(
"--max-results",
type=int,
default=5,
help="Max hits to render per KB in text mode (default: 5)",
)
ap.add_argument(
"--workers",
type=int,
default=12,
help="Parallel worker count for fan-out calls (default: 12)",
)
ap.add_argument("--json", action="store_true", help="Output JSON instead of text")
args = ap.parse_args(argv)
client_id, api_key = load_credentials()
priority_kbs, skip_kbs, _strategy = load_config()
try:
all_kbs = list_all_kbs(client_id, api_key)
except RuntimeError as e:
sys.exit(f"error listing knowledge bases: {e}")
if not all_kbs:
sys.exit("no knowledge bases accessible with these credentials")
to_search = [kb for kb in all_kbs if kb["kb_name"] not in set(skip_kbs)]
with concurrent.futures.ThreadPoolExecutor(max_workers=args.workers) as pool:
results = list(
pool.map(
lambda kb: search_one_kb(kb, args.query, client_id, api_key),
to_search,
)
)
# Enumerate config-skipped KBs separately so the user sees which ones
# were intentionally filtered out vs which ones genuinely had no results.
skipped_cfg_results = [
{"kb": kb, "hits": [], "truncated": False, "error": None}
for kb in all_kbs
if kb["kb_name"] in set(skip_kbs)
]
priority, others, denied, _empty = rank_groups(results, priority_kbs, skip_kbs)
if args.json:
render_json(args.query, priority, others, denied, skipped_cfg_results)
else:
render_text(
args.query,
priority,
others,
denied,
skipped_cfg_results,
total_kbs_listed=len(all_kbs),
max_per_kb=args.max_results,
)
if __name__ == "__main__":
main()
Related skills
How it compares
Pick ima-copilot when IMA or ima-skill setup fails inside coding agents; use generic skill-creator skills for authoring new skills unrelated to Tencent IMA.
FAQ
Does ima-copilot replace ima-skill?
ima-copilot is a wrapper layer that orchestrates the upstream ima-skill install and configuration. It installs, troubleshoots, and personalizes ima-skill rather than replacing Tencent's official skill.
Which coding agents does ima-copilot support?
ima-copilot targets Claude Code, Codex, and OpenClaw. The skill description explicitly lists those three surfaces for IMA knowledge-base setup and repair.
What error triggers ima-copilot repair?
ima-copilot handles missing YAML frontmatter in ima-skill submodule SKILL.md files and messages like Skipped loading skill(s) due to invalid SKILL.md during agent startup.