From 71b951d4250e4480c3a2bf869fe8c22ee6aa777a Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 27 Apr 2026 14:53:46 +0200 Subject: [PATCH 1/3] feat(genesis-tools): add gt:research skill with MCP-availability protocol Ports the personal research dispatcher into the public genesis-tools plugin. Skill detects which MCPs (jina, brave-search, reddit-mcp-server, gh_grep, context7-mcp) are live, degrades gracefully when missing, and offers a one-time install prompt with bun add + config snippet from references/mcps.md. Removes hardcoded Obsidian vault path and personal API keys; save behavior defaults to project-scoped .claude/work/research/ with optional Obsidian copy when OBSIDIAN_VAULT_PATH is set. Bumps marketplace + plugin version to 1.0.21. --- .../genesis-tools/skills/research/SKILL.md | 268 ++++++++++++++++++ .../skills/research/references/mcps.md | 154 ++++++++++ 2 files changed, 422 insertions(+) create mode 100644 plugins/genesis-tools/skills/research/SKILL.md create mode 100644 plugins/genesis-tools/skills/research/references/mcps.md diff --git a/plugins/genesis-tools/skills/research/SKILL.md b/plugins/genesis-tools/skills/research/SKILL.md new file mode 100644 index 0000000000..8d08fb2108 --- /dev/null +++ b/plugins/genesis-tools/skills/research/SKILL.md @@ -0,0 +1,268 @@ +--- +name: gt:research +description: Use when user says "research X", "look up X online", "what's the current state of X", "what are people saying about X", "find examples of X on github", "compare options for X", "is X worth using", "what do people complain about with X", or otherwise asks for information from outside the local codebase. +context: fork +--- + +# Research + +Dispatcher for internet research. Main Claude stays in charge: classifies the query, **checks which MCPs are actually available this session**, spawns narrowly-scoped subagents with explicit tool preferences and hard source minimums, then stitches their reports into a cited answer. Main Claude writes the final file; agents return raw material. + +The skill is **tolerant of missing MCPs**: if a preferred MCP isn't installed, the agent uses what's available, the gap is recorded in `Confidence & Gaps`, and the user is offered a one-time install path (see "MCP availability" below). + +## When to use + +- "research X", "look up X online" +- "what's the current state of X", "is X still maintained" +- "what are people saying about X", "what do people complain about with X" +- "find examples of X on github", "how do people implement Y" +- "compare options for X", "is X worth using vs Y" +- Anything needing information from outside the local codebase. + +## When NOT to use + +- Local codebase questions → use Grep/Glob/Read or an `Explore` agent. +- Git history / blame → use `git log` / `git blame`. +- File contents in the repo → read the file directly. +- If ambiguous (the term appears in cwd but the query sounds online-ish), call `AskUserQuestion` once before dispatching. + +## Flow + +1. **Classify** the query into one of six categories (see below) and produce: `{category, depth, importance, needsSave, savePath, confidence}`. +2. **MCP availability check** — for each MCP listed in the category preset, check whether `mcp____` is exposed this session. Drop missing ones from the agent's allowlist; record the gap. If a more capable MCP is live that isn't in the preset, use it. **Optionally offer install** for the most-impactful missing MCP — see "MCP availability" below. +3. **Clarify if needed** — call `AskUserQuestion` once before dispatch when confidence is low (~<0.6), depth is unstated for `deep_technical`, or local-vs-online / project-vs-general is ambiguous. +4. **Dispatch** per category preset. Single agent for `factual` / `news` / `code_hunt`; parallel fan-out (2–3 agents) for `comparison` / `deep_technical` / `sentiment`. Mix Sonnet + Haiku in parallel when non-critical. Concurrency cap = 4. +5. **Stitch** — merge agent reports, dedupe sources, write file (if saving), return inline summary with citations + file pointer. + +## MCP availability + +This skill works best with `jina`, `brave-search`, `reddit-mcp-server`, `gh_grep`, and `context7-mcp` installed. None are required. + +### Detect + +Before dispatch, look at the available `mcp____` tools to know which servers are live. Do NOT ask the user — verify yourself. + +### Degrade + +When a preferred MCP is missing, drop it from the agent's allowlist and substitute: + +| Missing MCP | Fallback | +|---|---| +| `jina` (search + read) | Built-in `WebSearch` + `WebFetch` | +| `brave-search` | Whatever search MCP is live; otherwise `WebSearch` | +| `reddit-mcp-server` | Search `site:reddit.com` via `WebSearch` / `jina` (lower fidelity, no thread structure) | +| `gh_grep` | Search via `WebSearch`; or skip the github-code angle and note the gap | +| `context7-mcp` | Read official docs URLs directly via `jina` / `WebFetch` | + +Always record what was missing and what substitution was used in `Confidence & Gaps`. + +### Offer install (at most once per run) + +If a missing MCP would materially improve the run AND the user didn't say "quick"/"temporary"/"just tell me", call `AskUserQuestion` ONCE before dispatch: + +- **Question:** "I'd dispatch this with `` for ``, but it's not installed. Want install instructions, or proceed without?" +- **Options:** `Install now`, `Skip and proceed`, `Don't ask again this run`. + +If the user picks **Install now**, read `references/mcps.md` from this skill directory and output: +1. The `bun add --global ` command (if local) or "no install — hosted HTTP MCP" (if remote). +2. The exact `mcpServers` JSON snippet for the user to paste into their Claude config (or the equivalent `claude mcp add` one-liner). +3. Note that the user must restart Claude Code for the new server to become callable, and the skill will proceed without it for THIS run. + +The skill does **not** execute install commands itself. The user runs the install. The skill then proceeds with whatever is currently available. + +If the user picks **Skip** or **Don't ask again**, dispatch immediately with the degraded toolset. + +## Categories (6) + +`factual` · `news` · `comparison` · `deep_technical` · `code_hunt` · `sentiment` + +## Category presets + +Tool lists are **strong nudges**, not bans: agents should prefer these; missing ones are dropped per the availability protocol; agents may use other live MCPs when clearly better. + +### factual +"what version of X", "does Y support Z" +- **Agent:** single `general-purpose`, Haiku +- **Tools:** `mcp__jina__search_web`, `mcp__jina__read_url`, `mcp__brave-search__brave_web_search` — fall back to `WebSearch` + `WebFetch` if all missing +- **Min sources:** 3 +- **Save:** inline only unless user asks + +### news +"what happened with X recently", time-sensitive +- **Agent:** single `gt:explore` (or `general-purpose` if `gt:explore` not installed), Haiku +- **Tools:** `mcp__brave-search__brave_web_search`, `mcp__jina__parallel_read_url`; if `obsidian:defuddle` skill is available use it for clean article capture +- **Min sources:** 4 +- **Save:** if user asked or said "save to obsidian", default to `.claude/work/research/YYYY-MM-DD-HHMM-.md` (time in name because news is a dated snapshot); add an Obsidian copy ONLY if the user has set `OBSIDIAN_VAULT_PATH` env var or explicitly named the vault path + +### comparison +"compare X vs Y vs Z", "what's the best library for" +- **Agents (parallel, 2–3):** + - A — Sonnet, `general-purpose`, docs + official sources (`mcp__jina__search_web`, `mcp__jina__parallel_read_url`, `mcp__brave-search__brave_web_search`) + - B — Haiku, `general-purpose`, Reddit (`mcp__reddit-mcp-server__search_reddit`, `mcp__reddit-mcp-server__get_post_comments`, `mcp__reddit-mcp-server__get_top_posts`) — drop entire angle if `reddit-mcp-server` missing AND user declined install; substitute `site:reddit.com` web search if proceeding degraded + - C — Haiku, `general-purpose`, GitHub issues/discussions via `gt:github` skill if available, sorted by reactions/comments count +- **Min sources:** 8 combined +- **Save:** always + +### deep_technical +"how does X work under the hood" +- **Depth gate:** if user didn't indicate depth, call `AskUserQuestion` once: "skim / normal / deep-dive". +- **Agents (parallel, 2):** + - A — Sonnet, `gt:explore` if available else `general-purpose`, official docs + deep-reads (`mcp__jina__parallel_read_url`; if a library is named AND `context7-mcp` is live, `mcp__context7-mcp__resolve-library-id` then `mcp__context7-mcp__get-library-docs`) + - B — Haiku, `general-purpose`, blog posts / writeups (`mcp__brave-search__brave_web_search`, `mcp__jina__search_web`, `mcp__jina__read_url`) +- **Min sources:** 6 (more for deep-dive) +- **Save:** always + +### code_hunt +"find examples of X on github", "how do people implement Y" +- **Agent:** single `general-purpose`, Sonnet (precision matters for code) +- **Tools:** `mcp__gh_grep__searchGitHub` (primary), `gt:github` skill for issues/PRs if available, `mcp__jina__read_url` for specific files +- **Min sources:** 3 real code examples, each cited with repo + path +- **Save:** default `.claude/work/research/YYYY-MM-DD-.md` (project-scoped) +- **Nudge the user** if the query is vague — "find auth examples" is too broad; ask what language/framework/approach before dispatching. + +### sentiment +"what do people complain about with X", "is X worth using" +- **Agents (parallel, 2, both Haiku):** + - A — Reddit angle (`mcp__reddit-mcp-server__search_reddit` + `get_post_comments` + `get_top_posts`); fall back to `site:reddit.com` web search if missing + - B — GitHub issue threads via `gt:github` skill (sorted by reactions); fall back to `WebSearch` for `site:github.com` issues if missing +- **Min sources:** 6 distinct posts/threads +- **Save:** optional; inline unless asked + +## Source minimums (hard) + +| Category | Min | +|---|---| +| factual | 3 | +| news | 4 | +| comparison | 8 combined | +| deep_technical | 6 | +| code_hunt | 3 (with repo + path) | +| sentiment | 6 | + +Agents must either meet the count or return **"Under-count: found X of N required"**. No invention. No padding with low-quality links. Main Claude surfaces under-counts in the inline summary and the "Confidence & Gaps" section of the file. + +## Save rules + +Main Claude decides the path **before** dispatching and passes the explicit absolute path to any agent that saves raw material. Agents never guess paths. + +| Trigger | Path | +|---|---| +| User says "save to Obsidian" / "braindump this" AND has `$OBSIDIAN_VAULT_PATH` set OR specified a vault path | `$OBSIDIAN_VAULT_PATH/Braindump/YYYY-MM-DD-.md` (or user-specified subfolder) | +| Category = `news` | `.claude/work/research/YYYY-MM-DD-HHMM-.md` | +| Category = `code_hunt`, or query is project-scoped (mentions current repo/feature) | `.claude/work/research/YYYY-MM-DD-.md` in cwd | +| Category = `comparison` / `deep_technical` and broadly useful | `.claude/work/research/YYYY-MM-DD-.md`; copy to Obsidian if vault path set | +| User says "temporary" / "just tell me" / "don't save" / "quick" | inline only, no file | +| `factual` / small `sentiment` | inline only unless user asked | + +**CamelCase topic slug:** generate from the query, max 6 words, strip stopwords. "what's the current state of server components in react?" → `ReactServerComponentsState`. Never kebab-case. + +## Output file template + +Main Claude writes this. Every Findings sub-section cites its sources inline; the final `## Sources` section is the deduped master list with retrieval dates. + +```markdown +# + +**Date:** YYYY-MM-DD HH:MM +**Category:** +**Query:** +**Agents:** +**MCPs used:** +**MCPs unavailable:** + +## TL;DR +<2–4 sentences> + +## Findings + +### + + +_Sources: [title](url), [title](url)_ + +### + + +_Sources: [title](url)_ + +## Sources +- [Title](url) — one-line takeaway — _retrieved YYYY-MM-DD_ +- ... + +## Confidence & Gaps + +``` + +**Inline summary (always returned to user):** 2–4 sentence TL;DR + top 3 sources with links + file pointer (if saved) + one-line note if any MCPs were missing. + +## Agent prompt template + +Main Claude fills in per agent. Keep each agent narrow to one angle. + +``` +ROLE: + +QUERY: + +ANGLE: + +PREFERRED TOOLS (prefer these; avoid other MCPs unless you hit a dead end and another is clearly better): +- +- +- ... + +FALLBACKS IF PREFERRED TOOLS UNAVAILABLE: +- → +- ... + +MINIMUM SOURCES: +If you cannot reach N, return what you have and state exactly: +"Under-count: found X of N required" +Do not invent sources. Do not pad with low-quality links. + +RETURN FORMAT: +- Summary (2–4 sentences) +- Findings, grouped by sub-topic, each followed by inline source citations +- Source list: URL + one-line takeaway + retrieval date +- Confidence note (conflicting info, dead ends, MCPs that didn't respond) + + +SAVE RAW REPORT TO: +``` + +## Guardrails + +- **Source minimums are hard** (see table above). No invention, no padding. +- **`AskUserQuestion` fires at most twice per research run**: once for the optional install offer (only if useful and user-stakes-permit), once for ambiguity/depth clarification. Never more. +- **Tool-availability fallback.** Missing MCP → drop from allowlist, substitute per the table, note in "Confidence & Gaps". +- **Install offer is opt-in**, never automatic. Default to Skip when in doubt. Never run install commands on behalf of the user — show the command and config snippet only. +- **"Temporary" override.** Query contains "temporary", "just tell me", "don't save", "quick", "no need to save" → skip file write, skip install offer, inline only. Source minimums still enforced. +- **Concurrency cap = 4 parallel agents** per research run. If a preset would spawn more, drop the lowest-priority angle. +- **Honest under-count reporting** surfaces in both the inline summary and "Confidence & Gaps". +- **Mixed-model parallel** is fine when non-critical (e.g., Sonnet for docs + Haiku for Reddit in the same fan-out). + +## Examples + +**Example 1 — factual, all MCPs available** +User: "what's the current stable version of Bun?" +→ category=`factual`, single Haiku, jina + brave, 3 sources, inline only. + +**Example 2 — comparison, reddit MCP missing, user installs** +User: "research state management options for React in 2026" +→ category=`comparison`, detect `reddit-mcp-server` missing, ask once: install / skip / don't ask. User picks Install. Skill outputs `bun add --global reddit-mcp-server` and the config snippet from `references/mcps.md`. User restarts later. Skill proceeds with Sonnet docs + Haiku GitHub (Reddit angle dropped this run, gap recorded). 8 sources combined, save to `.claude/work/research/2026-04-27-ReactStateManagement2026.md`. + +**Example 3 — sentiment, user declined install** +User: "what do people complain about with tRPC, quick" +→ "quick" trigger: skip install offer. Reddit MCP missing → use `site:reddit.com` web search fallback. 6 sources, inline only. Gap noted. + +**Example 4 — code_hunt, gh_grep available** +User: "find examples of how people use Effect.ts for HTTP clients on github" +→ category=`code_hunt`, single Sonnet, gh_grep + jina read, 3 cited examples, save to `.claude/work/research/2026-04-27-EffectTsHttpClient.md`. + +**Example 5 — news, no Obsidian vault configured** +User: "what happened with React Compiler recently, save it" +→ category=`news`, single Haiku on `gt:explore` (or `general-purpose` if missing), 4 sources, save to `.claude/work/research/2026-04-27-1430-ReactCompilerUpdate.md` (no Obsidian copy because `OBSIDIAN_VAULT_PATH` is unset). + +**Example 6 — deep_technical, depth unstated** +User: "research how Postgres MVCC works" +→ depth not stated, ask once skim/normal/deep-dive. User says deep-dive. Parallel (Sonnet docs + context7 if live + Haiku blogs), 6+ sources, save to `.claude/work/research/`. diff --git a/plugins/genesis-tools/skills/research/references/mcps.md b/plugins/genesis-tools/skills/research/references/mcps.md new file mode 100644 index 0000000000..26d500561e --- /dev/null +++ b/plugins/genesis-tools/skills/research/references/mcps.md @@ -0,0 +1,154 @@ +# Optional MCPs for `gt:research` + +This skill dispatches research subagents with explicit MCP preferences. None of these are required — the skill degrades gracefully and reports the gap. Use this reference when the skill offers to help the user install one. + +## Detection + +Check whether an MCP is available in the current session by looking at the system reminder listing `mcp____` tools, or by attempting a no-op call. Do NOT ask the user "is X installed?" — verify yourself. + +## Install offer protocol + +When the skill decides an MCP would materially improve a research run and it's missing, it MUST: + +1. Tell the user **what the MCP adds** to this run, in one sentence ("brave-search would let me cross-check facts against a second search index"). +2. Offer install via `AskUserQuestion` with three choices: + - **Install now** (recommended) — show the install command and config snippet, then continue once they confirm install. + - **Skip** — proceed without it, note the gap in `Confidence & Gaps`. + - **Don't ask again this run** — proceed and don't re-prompt for any other missing MCP this run. +3. If the user picks Install now, output the **install command** AND the **config snippet** the user pastes into their Claude Code MCP config (the user runs the install themselves; the skill does not auto-run install commands). + +Default to **Skip** if the run is small, low-stakes, or the user typed "quick"/"just tell me"/"temporary". + +## Install commands and config snippets + +Each entry below has the install command and the **mcpServers** snippet for `~/.claude.json` (or run `tools mcp-manager show ` in this repo to see how it's configured locally). The user pastes the snippet under `mcpServers` in their Claude config, or uses `claude mcp add` per the [Claude Code MCP docs](https://docs.claude.com/en/docs/claude-code/mcp). + +### jina (web search + URL read) + +Tools used by this skill: `mcp__jina__search_web`, `mcp__jina__read_url`, `mcp__jina__parallel_read_url`. + +Jina is a hosted HTTP MCP — no local install. The user needs a Jina API key from . + +```json +{ + "mcpServers": { + "jina": { + "type": "http", + "url": "https://mcp.jina.ai/v1", + "headers": { + "Authorization": "Bearer " + } + } + } +} +``` + +CLI install: `claude mcp add --transport http jina https://mcp.jina.ai/v1 --header "Authorization: Bearer "` + +### brave-search + +Tools used by this skill: `mcp__brave-search__brave_web_search`, `mcp__brave-search__brave_local_search`. + +Local stdio server. User needs a Brave Search API key from . + +Install: +```bash +bun add --global @modelcontextprotocol/server-brave-search +``` + +Config snippet: +```json +{ + "mcpServers": { + "brave-search": { + "type": "stdio", + "command": "mcp-server-brave-search", + "env": { + "BRAVE_API_KEY": "" + } + } + } +} +``` + +### reddit-mcp-server + +Tools used by this skill: `mcp__reddit-mcp-server__search_reddit`, `get_post_comments`, `get_top_posts`. + +Local stdio server. Read-only mode works without credentials; for write actions the user adds `REDDIT_USERNAME` / `REDDIT_PASSWORD` env vars (this skill never writes). + +Install: +```bash +bun add --global reddit-mcp-server +``` + +Config snippet: +```json +{ + "mcpServers": { + "reddit-mcp-server": { + "type": "stdio", + "command": "reddit-mcp-server", + "args": [] + } + } +} +``` + +### gh_grep (GitHub code search) + +Tools used by this skill: `mcp__gh_grep__searchGitHub`. + +Hosted HTTP MCP — no local install, no API key. + +```json +{ + "mcpServers": { + "gh_grep": { + "type": "http", + "url": "https://mcp.grep.app" + } + } +} +``` + +CLI install: `claude mcp add --transport http gh_grep https://mcp.grep.app` + +### context7-mcp (library docs) + +Tools used by this skill (only when a named library is being researched): `mcp__context7-mcp__resolve-library-id`, `mcp__context7-mcp__get-library-docs`. + +Local stdio server. User needs a Context7 API key from . + +Install: +```bash +bun add --global @upstash/context7-mcp +``` + +Config snippet: +```json +{ + "mcpServers": { + "context7-mcp": { + "type": "stdio", + "command": "context7-mcp", + "env": { + "CONTEXT7_API_KEY": "" + } + } + } +} +``` + +## After install + +Once the user finishes adding the MCP and restarts Claude Code, the new tools appear under `mcp____*`. The skill should re-check availability before relying on the new server in the same run — if the user installed mid-run without a restart, the tools are still unavailable; treat as Skip and note in Gaps. + +## Sister skills (optional, ship in same plugin) + +These are not MCPs but are referenced by the skill for richer behavior. They live in the same `genesis-tools` plugin, so they are present whenever this skill is. + +- `gt:github` — GitHub issue/PR/discussion fetcher used by `comparison` and `sentiment` categories. +- `gt:explore` — code/web exploration used by `news` and `deep_technical` categories. + +If the plugin install was partial and these are missing, fall back to `general-purpose` agent with the same MCP preferences and note in Gaps. From f038a7c24681564fa9834d4717ed6f04dac16a3a Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 28 Apr 2026 17:47:32 +0200 Subject: [PATCH 2/3] chore(lint): fix preexisting biome violations on master - src/cmux: organize imports, useImportType, format, swap JSON for SafeJSON, drop unused TREE_GAP, fix useless regex escape - src/doctor: organize imports, format --- src/cmux/commands/profiles/index.ts | 6 ++-- src/cmux/commands/profiles/list.ts | 7 +++-- src/cmux/commands/profiles/path.ts | 2 +- src/cmux/commands/profiles/save.ts | 4 +-- src/cmux/commands/profiles/view.ts | 5 +-- src/cmux/index.ts | 2 +- src/cmux/lib/__tests__/restore-tree.test.ts | 2 +- src/cmux/lib/__tests__/shell-probe.test.ts | 2 +- src/cmux/lib/__tests__/store.test.ts | 4 +-- src/cmux/lib/focus-guard.ts | 2 +- src/cmux/lib/format.ts | 14 ++++----- src/cmux/lib/restore.ts | 31 ++++++------------- src/cmux/lib/shell-probe.ts | 16 +++------- src/cmux/lib/snapshot.ts | 4 +-- src/cmux/lib/socket.ts | 27 +++++++--------- src/doctor/__tests__/integration.test.ts | 24 ++++++++------ src/doctor/ui/tui/ErrorBoundary.tsx | 6 +--- src/doctor/ui/tui/TrashTray.tsx | 5 +-- src/doctor/ui/tui/stores/engine-store.ts | 4 ++- .../tui/views/__tests__/battery-view.test.ts | 2 +- .../ui/tui/views/__tests__/brew-view.test.ts | 2 +- .../views/__tests__/dev-caches-view.test.ts | 2 +- .../views/__tests__/disk-space-view.test.ts | 2 +- .../tui/views/__tests__/generic-view.test.ts | 2 +- .../tui/views/__tests__/memory-view.test.ts | 2 +- .../tui/views/__tests__/network-view.test.ts | 2 +- .../views/__tests__/processes-view.test.ts | 2 +- .../tui/views/__tests__/security-view.test.ts | 2 +- .../tui/views/__tests__/startup-view.test.ts | 2 +- .../__tests__/system-caches-view.test.ts | 2 +- src/doctor/ui/tui/views/shared.ts | 6 +--- 31 files changed, 87 insertions(+), 108 deletions(-) diff --git a/src/cmux/commands/profiles/index.ts b/src/cmux/commands/profiles/index.ts index f6a7c26fc4..f4310e3214 100644 --- a/src/cmux/commands/profiles/index.ts +++ b/src/cmux/commands/profiles/index.ts @@ -1,4 +1,3 @@ -import type { Command } from "commander"; import { registerDeleteCommand } from "@app/cmux/commands/profiles/delete"; import { registerEditCommand } from "@app/cmux/commands/profiles/edit"; import { registerListCommand } from "@app/cmux/commands/profiles/list"; @@ -6,11 +5,10 @@ import { registerPathCommand } from "@app/cmux/commands/profiles/path"; import { registerRestoreCommand } from "@app/cmux/commands/profiles/restore"; import { registerSaveCommand } from "@app/cmux/commands/profiles/save"; import { registerViewCommand } from "@app/cmux/commands/profiles/view"; +import type { Command } from "commander"; export function registerProfilesCommand(program: Command): void { - const profiles = program - .command("profiles") - .description("Manage saved cmux workspace profiles"); + const profiles = program.command("profiles").description("Manage saved cmux workspace profiles"); registerSaveCommand(profiles); registerListCommand(profiles); diff --git a/src/cmux/commands/profiles/list.ts b/src/cmux/commands/profiles/list.ts index 3be4add48e..c90f8a9ce1 100644 --- a/src/cmux/commands/profiles/list.ts +++ b/src/cmux/commands/profiles/list.ts @@ -1,7 +1,8 @@ -import { Command } from "commander"; -import pc from "picocolors"; import { renderProfileList } from "@app/cmux/lib/format"; import { ProfileStore } from "@app/cmux/lib/store"; +import { SafeJSON } from "@app/utils/json"; +import type { Command } from "commander"; +import pc from "picocolors"; export function registerListCommand(parent: Command): void { parent @@ -13,7 +14,7 @@ export function registerListCommand(parent: Command): void { const store = new ProfileStore(); const summaries = store.list(); if (opts.json) { - console.log(JSON.stringify(summaries, null, 2)); + console.log(SafeJSON.stringify(summaries, null, 2)); return; } console.log(renderProfileList(summaries)); diff --git a/src/cmux/commands/profiles/path.ts b/src/cmux/commands/profiles/path.ts index 4e1ed0887f..6576bfa664 100644 --- a/src/cmux/commands/profiles/path.ts +++ b/src/cmux/commands/profiles/path.ts @@ -1,5 +1,5 @@ -import { Command } from "commander"; import { ProfileStore } from "@app/cmux/lib/store"; +import type { Command } from "commander"; export function registerPathCommand(parent: Command): void { parent diff --git a/src/cmux/commands/profiles/save.ts b/src/cmux/commands/profiles/save.ts index dc19598a4e..4443ecd7b7 100644 --- a/src/cmux/commands/profiles/save.ts +++ b/src/cmux/commands/profiles/save.ts @@ -29,11 +29,11 @@ export function registerSaveCommand(parent: Command): void { .option("--no-cwd", "Skip per-pane cwd capture") .option( "--no-screen", - "Skip per-pane visible-screen capture (default on: each terminal pane's rendered content is captured so restore can paint it back)", + "Skip per-pane visible-screen capture (default on: each terminal pane's rendered content is captured so restore can paint it back)" ) .option( "--no-history", - "Skip per-pane last-shell-command capture (default on: scrollback is parsed for the most recent shell prompt+command, e.g. `claude --resume `, and that command is pre-typed at the new prompt on restore)", + "Skip per-pane last-shell-command capture (default on: scrollback is parsed for the most recent shell prompt+command, e.g. `claude --resume `, and that command is pre-typed at the new prompt on restore)" ) .option("--note ", "Free-form note stored on the profile") .option("-f, --force", "Overwrite an existing profile of the same name") diff --git a/src/cmux/commands/profiles/view.ts b/src/cmux/commands/profiles/view.ts index 3ab863c760..28049154b0 100644 --- a/src/cmux/commands/profiles/view.ts +++ b/src/cmux/commands/profiles/view.ts @@ -1,6 +1,7 @@ -import { Command } from "commander"; import { renderProfileTree } from "@app/cmux/lib/format"; import { ProfileNotFoundError, ProfileStore } from "@app/cmux/lib/store"; +import { SafeJSON } from "@app/utils/json"; +import type { Command } from "commander"; export function registerViewCommand(parent: Command): void { parent @@ -13,7 +14,7 @@ export function registerViewCommand(parent: Command): void { try { const profile = store.read(name); if (opts.json) { - console.log(JSON.stringify(profile, null, 2)); + console.log(SafeJSON.stringify(profile, null, 2)); return; } console.log(renderProfileTree(profile)); diff --git a/src/cmux/index.ts b/src/cmux/index.ts index 41d3d84a79..d1c4652e54 100644 --- a/src/cmux/index.ts +++ b/src/cmux/index.ts @@ -13,10 +13,10 @@ * tools cmux profiles path */ -import { Command } from "commander"; import { registerProfilesCommand } from "@app/cmux/commands/profiles"; import { enhanceHelp } from "@app/utils/cli"; import { handleReadmeFlag } from "@app/utils/readme"; +import { Command } from "commander"; handleReadmeFlag(import.meta.url); diff --git a/src/cmux/lib/__tests__/restore-tree.test.ts b/src/cmux/lib/__tests__/restore-tree.test.ts index 7239205cc6..be5b6f300b 100644 --- a/src/cmux/lib/__tests__/restore-tree.test.ts +++ b/src/cmux/lib/__tests__/restore-tree.test.ts @@ -85,7 +85,7 @@ describe("buildSplitTree", () => { pane(1, 600, 0, 400, 600), pane(2, 400, 300, 600, 300), pane(3, 0, 300, 400, 600), - ]), + ]) ).toThrow(); }); }); diff --git a/src/cmux/lib/__tests__/shell-probe.test.ts b/src/cmux/lib/__tests__/shell-probe.test.ts index 9ceb626172..b362e2875c 100644 --- a/src/cmux/lib/__tests__/shell-probe.test.ts +++ b/src/cmux/lib/__tests__/shell-probe.test.ts @@ -1,6 +1,6 @@ +import { describe, expect, it } from "bun:test"; import { homedir } from "node:os"; import { join } from "node:path"; -import { describe, expect, it } from "bun:test"; import { cwdFromTitle, lastCommandFromCapture } from "@app/cmux/lib/shell-probe"; describe("cwdFromTitle", () => { diff --git a/src/cmux/lib/__tests__/store.test.ts b/src/cmux/lib/__tests__/store.test.ts index ad5ea1cc54..b4ec7ed594 100644 --- a/src/cmux/lib/__tests__/store.test.ts +++ b/src/cmux/lib/__tests__/store.test.ts @@ -1,9 +1,9 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { ProfileExistsError, ProfileNotFoundError, ProfileStore } from "@app/cmux/lib/store"; -import { type Profile, PROFILE_VERSION } from "@app/cmux/lib/types"; +import { PROFILE_VERSION, type Profile } from "@app/cmux/lib/types"; const sampleProfile: Profile = { version: PROFILE_VERSION, diff --git a/src/cmux/lib/focus-guard.ts b/src/cmux/lib/focus-guard.ts index dc68a181d9..85e62d8649 100644 --- a/src/cmux/lib/focus-guard.ts +++ b/src/cmux/lib/focus-guard.ts @@ -1,5 +1,5 @@ -import logger from "@app/logger"; import { runCmuxJSON, runCmuxOk } from "@app/cmux/lib/cli"; +import logger from "@app/logger"; interface IdentifyFocused { workspace_ref?: string; diff --git a/src/cmux/lib/format.ts b/src/cmux/lib/format.ts index fbbf315a99..54d6d1b764 100644 --- a/src/cmux/lib/format.ts +++ b/src/cmux/lib/format.ts @@ -1,12 +1,11 @@ -import pc from "picocolors"; +import type { Pane, Profile, ProfileSummary, Surface } from "@app/cmux/lib/types"; import { formatBytes } from "@app/utils/format"; import { stripAnsi } from "@app/utils/string"; -import type { Pane, Profile, ProfileSummary, Surface } from "@app/cmux/lib/types"; +import pc from "picocolors"; const TREE_VERTICAL = pc.dim("│"); const TREE_BRANCH = pc.dim("├─"); const TREE_LAST = pc.dim("└─"); -const TREE_GAP = pc.dim(" "); export function renderProfileList(summaries: ProfileSummary[]): string { if (summaries.length === 0) { @@ -56,7 +55,7 @@ export function renderProfileTree(profile: Profile): string { const isLastWindow = wi === profile.windows.length - 1; const windowPrefix = isLastWindow ? TREE_LAST : TREE_BRANCH; lines.push( - `${windowPrefix} ${pc.bold(window.title || window.ref)} ${pc.dim(`${window.container_frame.width.toFixed(0)}×${window.container_frame.height.toFixed(0)} px`)}`, + `${windowPrefix} ${pc.bold(window.title || window.ref)} ${pc.dim(`${window.container_frame.width.toFixed(0)}×${window.container_frame.height.toFixed(0)} px`)}` ); const childIndent = isLastWindow ? " " : `${TREE_VERTICAL} `; @@ -65,7 +64,7 @@ export function renderProfileTree(profile: Profile): string { const wsPrefix = isLastWs ? TREE_LAST : TREE_BRANCH; const wsBadge = ws.selected ? pc.green(" ★") : ""; lines.push( - `${childIndent}${wsPrefix} ${pc.cyan(ws.title)}${wsBadge} ${pc.dim(`(${ws.panes.length} pane${ws.panes.length === 1 ? "" : "s"})`)}`, + `${childIndent}${wsPrefix} ${pc.cyan(ws.title)}${wsBadge} ${pc.dim(`(${ws.panes.length} pane${ws.panes.length === 1 ? "" : "s"})`)}` ); const wsChildIndent = childIndent + (isLastWs ? " " : `${TREE_VERTICAL} `); ws.panes.forEach((pane, pi) => { @@ -89,7 +88,7 @@ export function renderProfileTree(profile: Profile): string { function formatPaneHeader(pane: Pane): string { const dims = pc.dim(`${pane.columns}×${pane.rows}`); const px = pc.dim( - `@(${pane.pixel_frame.x.toFixed(0)},${pane.pixel_frame.y.toFixed(0)}) ${pane.pixel_frame.width.toFixed(0)}×${pane.pixel_frame.height.toFixed(0)}px`, + `@(${pane.pixel_frame.x.toFixed(0)},${pane.pixel_frame.y.toFixed(0)}) ${pane.pixel_frame.width.toFixed(0)}×${pane.pixel_frame.height.toFixed(0)}px` ); return `${pc.bold(`pane ${pane.index}`)} ${dims} ${px}`; } @@ -149,8 +148,7 @@ function renderTable(rows: string[][]): string { const visible = stripAnsi(cell).length; return cell + " ".repeat(widths[i] - visible); }) - .join(" "), + .join(" ") ) .join("\n"); } - diff --git a/src/cmux/lib/restore.ts b/src/cmux/lib/restore.ts index af51f619ad..cc9956daf0 100644 --- a/src/cmux/lib/restore.ts +++ b/src/cmux/lib/restore.ts @@ -303,7 +303,7 @@ async function resizeNewBorder( tree: Exclude, anchorSurface: string, newPaneRef: string, - workspaceRef: string, + workspaceRef: string ): Promise { const MAX_ATTEMPTS = 8; let lastDelta = Number.POSITIVE_INFINITY; @@ -314,7 +314,7 @@ async function resizeNewBorder( if (!oldPane || !newPane) { logger.warn( { anchorSurface, newPaneRef, kind: tree.kind }, - "[restore] could not locate split pair — aborting resize", + "[restore] could not locate split pair — aborting resize" ); return; } @@ -340,7 +340,7 @@ async function resizeNewBorder( if (Math.abs(deltaCells) >= lastDelta) { logger.warn( { newPane: newPane.ref, deltaCells, lastDelta, attempt }, - "[restore] vsplit resize made no progress — bailing", + "[restore] vsplit resize made no progress — bailing" ); return; } @@ -360,7 +360,7 @@ async function resizeNewBorder( targetCols: targetOldCols, fraction: tree.leftFraction, }, - "[restore] vsplit resize", + "[restore] vsplit resize" ); const moved = await tryResize(workspaceRef, target.ref, dir, amountPx); if (!moved) { @@ -378,7 +378,7 @@ async function resizeNewBorder( if (Math.abs(deltaCells) >= lastDelta) { logger.warn( { newPane: newPane.ref, deltaCells, lastDelta, attempt }, - "[restore] hsplit resize made no progress — bailing", + "[restore] hsplit resize made no progress — bailing" ); return; } @@ -398,7 +398,7 @@ async function resizeNewBorder( targetRows: targetOldRows, fraction: tree.topFraction, }, - "[restore] hsplit resize", + "[restore] hsplit resize" ); const moved = await tryResize(workspaceRef, target.ref, dir, amountPx); if (!moved) { @@ -412,7 +412,7 @@ async function tryResize( workspaceRef: string, paneRef: string, direction: "-L" | "-R" | "-U" | "-D", - amount: number, + amount: number ): Promise { try { await runCmuxOk([ @@ -427,10 +427,7 @@ async function tryResize( ]); return true; } catch (error) { - logger.warn( - { error, paneRef, direction, amount }, - "[restore] split-time resize failed", - ); + logger.warn({ error, paneRef, direction, amount }, "[restore] split-time resize failed"); return false; } } @@ -460,15 +457,7 @@ async function populatePane( // currently focused pane (same routing-bug class as `surface.split`). The CLI // routes through V2 and honors the params, so additional tabs land in the // correct pane instead of all stacking into the anchor. - const args = [ - "new-surface", - "--workspace", - workspaceRef, - "--pane", - paneRef, - "--type", - nextSavedSurface.type, - ]; + const args = ["new-surface", "--workspace", workspaceRef, "--pane", paneRef, "--type", nextSavedSurface.type]; if (nextSavedSurface.type === "browser" && nextSavedSurface.url) { args.push("--url", nextSavedSurface.url); } @@ -476,7 +465,7 @@ async function populatePane( if (created.pane_ref !== paneRef) { logger.warn( { requested: paneRef, got: created.pane_ref, surfaceRef: created.surface_ref }, - "[restore] new-surface landed in unexpected pane", + "[restore] new-surface landed in unexpected pane" ); } surfaceRefs.push(created.surface_ref); diff --git a/src/cmux/lib/shell-probe.ts b/src/cmux/lib/shell-probe.ts index bfecc87688..bdd98dae21 100644 --- a/src/cmux/lib/shell-probe.ts +++ b/src/cmux/lib/shell-probe.ts @@ -1,8 +1,8 @@ import { homedir } from "node:os"; import { join } from "node:path"; -import logger from "@app/logger"; import { runCmux } from "@app/cmux/lib/cli"; import type { CommandSource, ScreenSnapshot } from "@app/cmux/lib/types"; +import logger from "@app/logger"; /** * cmux exposes no PID/tty for surfaces, but it does set tab titles from OSC-7 cwd escapes. @@ -69,7 +69,7 @@ const PROMPT_PATTERNS: RegExp[] = [ // [user@host dir]$ /^\[[^\]]+\]\s*[$#%]\s+(\S.*?)\s*$/u, // user@host:dir$ (typical bash) - /^[\w.\-]+@[\w.\-]+:\S*[$#%]\s+(\S.*?)\s*$/u, + /^[\w.-]+@[\w.-]+:\S*[$#%]\s+(\S.*?)\s*$/u, ]; interface CommandHint { @@ -121,7 +121,7 @@ export interface SurfaceCaptureResult { export async function captureSurfaceState( workspaceRef: string, surfaceRef: string, - options: { screen: boolean; history: boolean }, + options: { screen: boolean; history: boolean } ): Promise { if (!options.screen && !options.history) { return { command: NO_COMMAND }; @@ -144,17 +144,11 @@ export async function captureSurfaceState( async function runCaptureSafely(workspaceRef: string, surfaceRef: string): Promise { try { - const result = await runCmux([ - "capture-pane", - "--workspace", - workspaceRef, - "--surface", - surfaceRef, - ]); + const result = await runCmux(["capture-pane", "--workspace", workspaceRef, "--surface", surfaceRef]); if (result.code !== 0) { logger.debug( { workspaceRef, surfaceRef, stderr: result.stderr.trim() }, - "[shell-probe] capture-pane non-zero exit", + "[shell-probe] capture-pane non-zero exit" ); return undefined; } diff --git a/src/cmux/lib/snapshot.ts b/src/cmux/lib/snapshot.ts index fe725b4c3a..8d1e64f709 100644 --- a/src/cmux/lib/snapshot.ts +++ b/src/cmux/lib/snapshot.ts @@ -193,7 +193,7 @@ function filterWorkspaces( async function capturePanes( workspaceRef: string, options: SnapshotOptions, - callerSurfaceRef: string | undefined, + callerSurfaceRef: string | undefined ): Promise { const layout = await paneList(workspaceRef); const panes: Pane[] = []; @@ -235,7 +235,7 @@ async function captureSurface( entry: SurfaceListEntry, workspaceRef: string, options: SnapshotOptions, - callerSurfaceRef: string | undefined, + callerSurfaceRef: string | undefined ): Promise { const title = entry.title ?? ""; if (entry.type === "browser") { diff --git a/src/cmux/lib/socket.ts b/src/cmux/lib/socket.ts index de1fc01f02..5b54013593 100644 --- a/src/cmux/lib/socket.ts +++ b/src/cmux/lib/socket.ts @@ -1,6 +1,7 @@ import { connect } from "node:net"; -import logger from "@app/logger"; import { runCmuxJSON } from "@app/cmux/lib/cli"; +import logger from "@app/logger"; +import { SafeJSON } from "@app/utils/json"; interface IdentifyResponse { socket_path: string; @@ -38,11 +39,11 @@ let requestCounter = 0; export async function rpc( method: string, params: Record = {}, - opts: { timeoutMs?: number } = {}, + opts: { timeoutMs?: number } = {} ): Promise { const path = await getSocketPath(); const id = `cmux-${Date.now()}-${++requestCounter}`; - const payload = `${JSON.stringify({ id, method, params })}\n`; + const payload = `${SafeJSON.stringify({ id, method, params })}\n`; const timeoutMs = opts.timeoutMs ?? 5_000; return await new Promise((resolve, reject) => { @@ -85,15 +86,15 @@ export async function rpc( const line = buffer.slice(0, newlineIndex); clearTimeout(timer); try { - const parsed = JSON.parse(line) as JsonRpcResponse; + const parsed = SafeJSON.parse(line) as JsonRpcResponse; if (!parsed.ok) { logger.error({ method, params, error: parsed.error }, "[socket] RPC error"); settle(() => reject( new Error( - `cmux RPC ${method} failed: ${parsed.error?.code ?? "unknown"} ${parsed.error?.message ?? ""}`.trim(), - ), - ), + `cmux RPC ${method} failed: ${parsed.error?.code ?? "unknown"} ${parsed.error?.message ?? ""}`.trim() + ) + ) ); return; } @@ -176,13 +177,9 @@ export interface WorkspaceCreateResult { window_id: string; } -export async function workspaceCreate(opts: { - name?: string; - description?: string; - cwd?: string; - command?: string; - window?: string; -} = {}): Promise { +export async function workspaceCreate( + opts: { name?: string; description?: string; cwd?: string; command?: string; window?: string } = {} +): Promise { const params: Record = {}; if (opts.name) { params.name = opts.name; @@ -213,7 +210,7 @@ export interface SurfaceSplitResult { export async function surfaceSplit( direction: "left" | "right" | "up" | "down", surfaceRef: string, - workspaceRef: string, + workspaceRef: string ): Promise { return rpc("surface.split", { direction, diff --git a/src/doctor/__tests__/integration.test.ts b/src/doctor/__tests__/integration.test.ts index 4aa61756d1..8ec2d091d4 100644 --- a/src/doctor/__tests__/integration.test.ts +++ b/src/doctor/__tests__/integration.test.ts @@ -1,9 +1,9 @@ +import { describe, expect, it, setDefaultTimeout } from "bun:test"; import { DiskSpaceAnalyzer } from "@app/doctor/analyzers/disk-space"; import { MemoryAnalyzer } from "@app/doctor/analyzers/memory"; import { ProcessesAnalyzer } from "@app/doctor/analyzers/processes"; import { Engine } from "@app/doctor/lib/engine"; import { classifyCachePath, classifyProcess } from "@app/doctor/lib/safety"; -import { describe, expect, it, setDefaultTimeout } from "bun:test"; setDefaultTimeout(60_000); @@ -18,10 +18,13 @@ describe("integration smoke (dry-run, live machine)", () => { } }); - const results = await engine.run( - [new DiskSpaceAnalyzer(), new MemoryAnalyzer(), new ProcessesAnalyzer()], - { concurrency: 4, thorough: false, fresh: true, runId: "smoke", dryRun: true } - ); + const results = await engine.run([new DiskSpaceAnalyzer(), new MemoryAnalyzer(), new ProcessesAnalyzer()], { + concurrency: 4, + thorough: false, + fresh: true, + runId: "smoke", + dryRun: true, + }); expect(errors).toEqual([]); expect(results.get("memory")?.findings.length).toBeGreaterThan(0); @@ -29,10 +32,13 @@ describe("integration smoke (dry-run, live machine)", () => { it("no non-blocked finding references a blacklisted cache path", async () => { const engine = new Engine(); - const results = await engine.run( - [new DiskSpaceAnalyzer(), new MemoryAnalyzer(), new ProcessesAnalyzer()], - { concurrency: 4, thorough: false, fresh: true, runId: "smoke", dryRun: true } - ); + const results = await engine.run([new DiskSpaceAnalyzer(), new MemoryAnalyzer(), new ProcessesAnalyzer()], { + concurrency: 4, + thorough: false, + fresh: true, + runId: "smoke", + dryRun: true, + }); for (const result of results.values()) { for (const finding of result.findings) { diff --git a/src/doctor/ui/tui/ErrorBoundary.tsx b/src/doctor/ui/tui/ErrorBoundary.tsx index 1f6d52f4f0..910dd489f0 100644 --- a/src/doctor/ui/tui/ErrorBoundary.tsx +++ b/src/doctor/ui/tui/ErrorBoundary.tsx @@ -9,11 +9,7 @@ interface ErrorBoundaryProps { } export function ErrorBoundary(props: ErrorBoundaryProps) { - return ( - renderError(err)}> - {props.children} - - ); + return renderError(err)}>{props.children}; } function renderError(err: Error) { diff --git a/src/doctor/ui/tui/TrashTray.tsx b/src/doctor/ui/tui/TrashTray.tsx index 0eebdaa46d..d4aa2747c8 100644 --- a/src/doctor/ui/tui/TrashTray.tsx +++ b/src/doctor/ui/tui/TrashTray.tsx @@ -31,12 +31,13 @@ export function TrashTray(props: TrashTrayProps) { {(item) => ( - {" "}{item.label ?? item.path} · {formatBytes(item.bytes)} + {" "} + {item.label ?? item.path} · {formatBytes(item.bytes)} )} 5}> - and {props.items.length - 5} more + and {props.items.length - 5} more diff --git a/src/doctor/ui/tui/stores/engine-store.ts b/src/doctor/ui/tui/stores/engine-store.ts index e3b2a1545a..d4aab4e37a 100644 --- a/src/doctor/ui/tui/stores/engine-store.ts +++ b/src/doctor/ui/tui/stores/engine-store.ts @@ -23,7 +23,9 @@ export interface EngineState { reset(focusedAnalyzerId: string): void; } -function initialState(focusedAnalyzerId = ""): Omit< +function initialState( + focusedAnalyzerId = "" +): Omit< EngineState, | "applyEvent" | "toggleFinding" diff --git a/src/doctor/ui/tui/views/__tests__/battery-view.test.ts b/src/doctor/ui/tui/views/__tests__/battery-view.test.ts index 00ee62abbb..65c49277e8 100644 --- a/src/doctor/ui/tui/views/__tests__/battery-view.test.ts +++ b/src/doctor/ui/tui/views/__tests__/battery-view.test.ts @@ -1,5 +1,5 @@ -import type { Finding } from "@app/doctor/lib/types"; import { describe, expect, it } from "bun:test"; +import type { Finding } from "@app/doctor/lib/types"; import { batteryView } from "../battery-view"; const summary: Finding = { diff --git a/src/doctor/ui/tui/views/__tests__/brew-view.test.ts b/src/doctor/ui/tui/views/__tests__/brew-view.test.ts index a297aa83e8..6fb917e8ea 100644 --- a/src/doctor/ui/tui/views/__tests__/brew-view.test.ts +++ b/src/doctor/ui/tui/views/__tests__/brew-view.test.ts @@ -1,5 +1,5 @@ -import type { Action, Finding } from "@app/doctor/lib/types"; import { describe, expect, it } from "bun:test"; +import type { Action, Finding } from "@app/doctor/lib/types"; import { brewView } from "../brew-view"; const upgradeAction: Action = { diff --git a/src/doctor/ui/tui/views/__tests__/dev-caches-view.test.ts b/src/doctor/ui/tui/views/__tests__/dev-caches-view.test.ts index c6219ebf49..487eb886ad 100644 --- a/src/doctor/ui/tui/views/__tests__/dev-caches-view.test.ts +++ b/src/doctor/ui/tui/views/__tests__/dev-caches-view.test.ts @@ -1,5 +1,5 @@ -import type { Action, Finding } from "@app/doctor/lib/types"; import { describe, expect, it } from "bun:test"; +import type { Action, Finding } from "@app/doctor/lib/types"; import { devCachesView } from "../dev-caches-view"; const noopAction: Action = { diff --git a/src/doctor/ui/tui/views/__tests__/disk-space-view.test.ts b/src/doctor/ui/tui/views/__tests__/disk-space-view.test.ts index 40b6563be8..03f935e616 100644 --- a/src/doctor/ui/tui/views/__tests__/disk-space-view.test.ts +++ b/src/doctor/ui/tui/views/__tests__/disk-space-view.test.ts @@ -1,5 +1,5 @@ -import type { Action, Finding } from "@app/doctor/lib/types"; import { describe, expect, it } from "bun:test"; +import type { Action, Finding } from "@app/doctor/lib/types"; import { diskSpaceView } from "../disk-space-view"; const noopAction: Action = { diff --git a/src/doctor/ui/tui/views/__tests__/generic-view.test.ts b/src/doctor/ui/tui/views/__tests__/generic-view.test.ts index 4ddca9b310..51f1b5a283 100644 --- a/src/doctor/ui/tui/views/__tests__/generic-view.test.ts +++ b/src/doctor/ui/tui/views/__tests__/generic-view.test.ts @@ -1,5 +1,5 @@ -import type { Action, Finding } from "@app/doctor/lib/types"; import { describe, expect, it } from "bun:test"; +import type { Action, Finding } from "@app/doctor/lib/types"; import { genericView } from "../generic-view"; const noopAction: Action = { diff --git a/src/doctor/ui/tui/views/__tests__/memory-view.test.ts b/src/doctor/ui/tui/views/__tests__/memory-view.test.ts index 478682634a..0d3c4d7fda 100644 --- a/src/doctor/ui/tui/views/__tests__/memory-view.test.ts +++ b/src/doctor/ui/tui/views/__tests__/memory-view.test.ts @@ -1,5 +1,5 @@ -import type { Action, Finding } from "@app/doctor/lib/types"; import { describe, expect, it } from "bun:test"; +import type { Action, Finding } from "@app/doctor/lib/types"; import { memoryView } from "../memory-view"; const killAction: Action = { diff --git a/src/doctor/ui/tui/views/__tests__/network-view.test.ts b/src/doctor/ui/tui/views/__tests__/network-view.test.ts index 5684611cd0..48cb0a8381 100644 --- a/src/doctor/ui/tui/views/__tests__/network-view.test.ts +++ b/src/doctor/ui/tui/views/__tests__/network-view.test.ts @@ -1,5 +1,5 @@ -import type { Action, Finding } from "@app/doctor/lib/types"; import { describe, expect, it } from "bun:test"; +import type { Action, Finding } from "@app/doctor/lib/types"; import { networkView } from "../network-view"; const flushAction: Action = { diff --git a/src/doctor/ui/tui/views/__tests__/processes-view.test.ts b/src/doctor/ui/tui/views/__tests__/processes-view.test.ts index bded534017..a143d9fa0a 100644 --- a/src/doctor/ui/tui/views/__tests__/processes-view.test.ts +++ b/src/doctor/ui/tui/views/__tests__/processes-view.test.ts @@ -1,5 +1,5 @@ -import type { Action, Finding } from "@app/doctor/lib/types"; import { describe, expect, it } from "bun:test"; +import type { Action, Finding } from "@app/doctor/lib/types"; import { processesView } from "../processes-view"; const killAction: Action = { diff --git a/src/doctor/ui/tui/views/__tests__/security-view.test.ts b/src/doctor/ui/tui/views/__tests__/security-view.test.ts index d2feca994b..0f023045fd 100644 --- a/src/doctor/ui/tui/views/__tests__/security-view.test.ts +++ b/src/doctor/ui/tui/views/__tests__/security-view.test.ts @@ -1,5 +1,5 @@ -import type { Finding } from "@app/doctor/lib/types"; import { describe, expect, it } from "bun:test"; +import type { Finding } from "@app/doctor/lib/types"; import { securityView } from "../security-view"; const filevaultOn: Finding = { diff --git a/src/doctor/ui/tui/views/__tests__/startup-view.test.ts b/src/doctor/ui/tui/views/__tests__/startup-view.test.ts index acd7a669df..f2cb91ed2b 100644 --- a/src/doctor/ui/tui/views/__tests__/startup-view.test.ts +++ b/src/doctor/ui/tui/views/__tests__/startup-view.test.ts @@ -1,5 +1,5 @@ -import type { Action, Finding } from "@app/doctor/lib/types"; import { describe, expect, it } from "bun:test"; +import type { Action, Finding } from "@app/doctor/lib/types"; import { startupView } from "../startup-view"; const removeAction: Action = { diff --git a/src/doctor/ui/tui/views/__tests__/system-caches-view.test.ts b/src/doctor/ui/tui/views/__tests__/system-caches-view.test.ts index 70064b5055..ef953bee84 100644 --- a/src/doctor/ui/tui/views/__tests__/system-caches-view.test.ts +++ b/src/doctor/ui/tui/views/__tests__/system-caches-view.test.ts @@ -1,5 +1,5 @@ -import type { Action, Finding } from "@app/doctor/lib/types"; import { describe, expect, it } from "bun:test"; +import type { Action, Finding } from "@app/doctor/lib/types"; import { systemCachesView } from "../system-caches-view"; const noopAction: Action = { diff --git a/src/doctor/ui/tui/views/shared.ts b/src/doctor/ui/tui/views/shared.ts index 7a29a0d113..42f80b1b00 100644 --- a/src/doctor/ui/tui/views/shared.ts +++ b/src/doctor/ui/tui/views/shared.ts @@ -31,11 +31,7 @@ export function selectionCell(finding: Finding, selected: Set, bg?: stri return [{ text: on ? "[x]" : "[ ]", fg: on ? THEME.success : THEME.fgDim, bg }]; } -export function sliceAroundCursor( - items: T[], - cursor: number, - rows: number, -): { rows: T[]; startIndex: number } { +export function sliceAroundCursor(items: T[], cursor: number, rows: number): { rows: T[]; startIndex: number } { if (items.length <= rows) { return { rows: items, startIndex: 0 }; } From 9334d3d3946a8ccc06281f4d2a1d654791373290 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 28 Apr 2026 19:53:09 +0200 Subject: [PATCH 3/3] fix(gt:research): address PR #153 CodeRabbit review feedback - mcps.md: blank lines around fenced code blocks (MD031) - mcps.md: qualify Reddit tool names with mcp__reddit-mcp-server__ prefix - SKILL.md: blank lines after category headings (MD022) - SKILL.md: qualify Reddit tool names in sentiment preset - SKILL.md: add 'text' language tag to template fenced block (MD040) --- plugins/genesis-tools/skills/research/SKILL.md | 16 ++++++++++++++-- .../skills/research/references/mcps.md | 8 +++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/plugins/genesis-tools/skills/research/SKILL.md b/plugins/genesis-tools/skills/research/SKILL.md index 8d08fb2108..01d3e0e7e0 100644 --- a/plugins/genesis-tools/skills/research/SKILL.md +++ b/plugins/genesis-tools/skills/research/SKILL.md @@ -81,21 +81,27 @@ If the user picks **Skip** or **Don't ask again**, dispatch immediately with the Tool lists are **strong nudges**, not bans: agents should prefer these; missing ones are dropped per the availability protocol; agents may use other live MCPs when clearly better. ### factual + "what version of X", "does Y support Z" + - **Agent:** single `general-purpose`, Haiku - **Tools:** `mcp__jina__search_web`, `mcp__jina__read_url`, `mcp__brave-search__brave_web_search` — fall back to `WebSearch` + `WebFetch` if all missing - **Min sources:** 3 - **Save:** inline only unless user asks ### news + "what happened with X recently", time-sensitive + - **Agent:** single `gt:explore` (or `general-purpose` if `gt:explore` not installed), Haiku - **Tools:** `mcp__brave-search__brave_web_search`, `mcp__jina__parallel_read_url`; if `obsidian:defuddle` skill is available use it for clean article capture - **Min sources:** 4 - **Save:** if user asked or said "save to obsidian", default to `.claude/work/research/YYYY-MM-DD-HHMM-.md` (time in name because news is a dated snapshot); add an Obsidian copy ONLY if the user has set `OBSIDIAN_VAULT_PATH` env var or explicitly named the vault path ### comparison + "compare X vs Y vs Z", "what's the best library for" + - **Agents (parallel, 2–3):** - A — Sonnet, `general-purpose`, docs + official sources (`mcp__jina__search_web`, `mcp__jina__parallel_read_url`, `mcp__brave-search__brave_web_search`) - B — Haiku, `general-purpose`, Reddit (`mcp__reddit-mcp-server__search_reddit`, `mcp__reddit-mcp-server__get_post_comments`, `mcp__reddit-mcp-server__get_top_posts`) — drop entire angle if `reddit-mcp-server` missing AND user declined install; substitute `site:reddit.com` web search if proceeding degraded @@ -104,7 +110,9 @@ Tool lists are **strong nudges**, not bans: agents should prefer these; missing - **Save:** always ### deep_technical + "how does X work under the hood" + - **Depth gate:** if user didn't indicate depth, call `AskUserQuestion` once: "skim / normal / deep-dive". - **Agents (parallel, 2):** - A — Sonnet, `gt:explore` if available else `general-purpose`, official docs + deep-reads (`mcp__jina__parallel_read_url`; if a library is named AND `context7-mcp` is live, `mcp__context7-mcp__resolve-library-id` then `mcp__context7-mcp__get-library-docs`) @@ -113,7 +121,9 @@ Tool lists are **strong nudges**, not bans: agents should prefer these; missing - **Save:** always ### code_hunt + "find examples of X on github", "how do people implement Y" + - **Agent:** single `general-purpose`, Sonnet (precision matters for code) - **Tools:** `mcp__gh_grep__searchGitHub` (primary), `gt:github` skill for issues/PRs if available, `mcp__jina__read_url` for specific files - **Min sources:** 3 real code examples, each cited with repo + path @@ -121,9 +131,11 @@ Tool lists are **strong nudges**, not bans: agents should prefer these; missing - **Nudge the user** if the query is vague — "find auth examples" is too broad; ask what language/framework/approach before dispatching. ### sentiment + "what do people complain about with X", "is X worth using" + - **Agents (parallel, 2, both Haiku):** - - A — Reddit angle (`mcp__reddit-mcp-server__search_reddit` + `get_post_comments` + `get_top_posts`); fall back to `site:reddit.com` web search if missing + - A — Reddit angle (`mcp__reddit-mcp-server__search_reddit` + `mcp__reddit-mcp-server__get_post_comments` + `mcp__reddit-mcp-server__get_top_posts`); fall back to `site:reddit.com` web search if missing - B — GitHub issue threads via `gt:github` skill (sorted by reactions); fall back to `WebSearch` for `site:github.com` issues if missing - **Min sources:** 6 distinct posts/threads - **Save:** optional; inline unless asked @@ -199,7 +211,7 @@ _Sources: [title](url)_ Main Claude fills in per agent. Keep each agent narrow to one angle. -``` +```text ROLE: QUERY: diff --git a/plugins/genesis-tools/skills/research/references/mcps.md b/plugins/genesis-tools/skills/research/references/mcps.md index 26d500561e..84193b901d 100644 --- a/plugins/genesis-tools/skills/research/references/mcps.md +++ b/plugins/genesis-tools/skills/research/references/mcps.md @@ -52,11 +52,13 @@ Tools used by this skill: `mcp__brave-search__brave_web_search`, `mcp__brave-sea Local stdio server. User needs a Brave Search API key from . Install: + ```bash bun add --global @modelcontextprotocol/server-brave-search ``` Config snippet: + ```json { "mcpServers": { @@ -73,16 +75,18 @@ Config snippet: ### reddit-mcp-server -Tools used by this skill: `mcp__reddit-mcp-server__search_reddit`, `get_post_comments`, `get_top_posts`. +Tools used by this skill: `mcp__reddit-mcp-server__search_reddit`, `mcp__reddit-mcp-server__get_post_comments`, `mcp__reddit-mcp-server__get_top_posts`. Local stdio server. Read-only mode works without credentials; for write actions the user adds `REDDIT_USERNAME` / `REDDIT_PASSWORD` env vars (this skill never writes). Install: + ```bash bun add --global reddit-mcp-server ``` Config snippet: + ```json { "mcpServers": { @@ -121,11 +125,13 @@ Tools used by this skill (only when a named library is being researched): `mcp__ Local stdio server. User needs a Context7 API key from . Install: + ```bash bun add --global @upstash/context7-mcp ``` Config snippet: + ```json { "mcpServers": {