Skip to content

feat(uninstall): support --agent <tool> to uninstall a single tool (#230) - #244

Merged
jeff-r2026 merged 2 commits into
Tencent:mainfrom
m0Nst3r873:feature/uninstall-agent
Jul 29, 2026
Merged

feat(uninstall): support --agent <tool> to uninstall a single tool (#230)#244
jeff-r2026 merged 2 commits into
Tencent:mainfrom
m0Nst3r873:feature/uninstall-agent

Conversation

@m0Nst3r873

@m0Nst3r873 m0Nst3r873 commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #230. Adds --agent <tool> to teamai uninstall, mirroring init --agent, so a user can uninstall a single AI tool's teamai resources instead of everything on the machine — and makes that removal durable across pulls.

  • Tool-specific resources (removed for the target tool): hooks (settings.json / OpenClaw hook dir), CLAUDE.md teamai block, team-synced skills, rules, built-in agents (e.g. teamai-recall).
  • Shared resources (env shell-profile block, docs directory, ~/.teamai/): removed only when the target itself has teamai resources AND is the last tool still using teamai; otherwise kept for the remaining tools.
  • Durable exclusion: a single-tool uninstall records the tool in disabledAgents so the next pull (or another tool's session-start hook) does not resurrect it. init --agent clears the exclusion again — symmetric.
  • Unknown tool / --agent without config → aborts without deleting anything, lists the available tools, and exits with a non-zero status.
  • No --agent → behavior is exactly as before (full uninstall).

Implementation

  • src/uninstall.ts: extract per-tool discovery into discoverToolResources(); buildRemovalPlan(…, agentFilter?) merges only the selected tool(s) and computes includeShared (false when other tools still hold teamai resources). On a non-last-tool uninstall (shared ~/.teamai survives), the tool is recorded in disabledAgents and pruned from any existing enabledAgents whitelist. Unknown-tool and no-config --agent paths set process.exitCode = 2.
  • src/types.ts: new disabledAgents field + isAgentDisabled() predicate.
  • Honor disabledAgents on every sync write-back so an excluded tool is never re-hydrated: hooks (reconcileTeamHooksForConfig effective filter + auto-migrate inject in pull.ts), the pull.ts resource/CLAUDE.md loops (tombstone, culture, claudemd, recall, cleanup), resources/{skills,rules,agents} pullItem, and builtin-{agents,rules,skills} deploy.
  • src/init.ts: init --agent removes the tool from disabledAgents (reverses the exclusion).
  • src/hooks.ts: hasTeamaiHooks(settingsPath, tool, manifestPath?) — true iff a settings file actually contains a teamai-managed hook (built-in or team), across claude/codex/cursor formats + managed-hooks manifest. Discovery gates the hook file on this instead of mere file existence.
  • src/index.ts: register the --agent <name> option.

Note on hasTeamaiHooks and the full-uninstall path

Gating hookFiles on hasTeamaiHooks also applies to the no---agent path. It cannot under-remove (each reconcile*Format's isManaged predicate is a subset of what hasTeamaiHooks matches); the only behavior lost is the empty-camelCase-key cleanup pass no longer running on files that hold no teamai hooks. Low impact.

Note on the enabledAgents whitelist

When no whitelist existed (enabledAgents unset = "all tools"), the uninstall leaves it undefined rather than collapsing it to [] — the hook path reads [] as "whitelist nothing", which would otherwise stop hook sync for the remaining tools too. Covered by a regression test + the e2e round-trip below.

Test Plan

  • npx tsc --noEmit clean
  • npx vitest run — full suite green (1791 tests). uninstall.test.ts: single-tool full removal, keep-shared when another tool present, unknown-tool error + exitCode === 2, no-config+--agent guard, last-tool-with-stripped-hooks regression, disabledAgents persisted on non-last-tool uninstall, enabledAgents whitelist pruned (not collapsed) when it exists. skills.test.ts: pullItem skips a disabled tool but still syncs the others.
  • Real CLI e2e (built dist/index.js, isolated $HOME with claude+codex tools):
    1. uninstall --agent claude (codex present) → claude skill/rule/hooks removed, .teamai + codex kept; disabledAgents: [claude] written, enabledAgents left unset ✅
    2. uninstall --agent codex (now last tool) → codex skill and .teamai removed ✅ (regression fixed)
    3. uninstall --agent <unknown> → English error + available-tools list, nothing deleted, exit 2
    4. Durability round-trippull → uninstall --agent claude → new team-repo commit → pull with codex configured with hooks: claude's skill/rule/hooks stay gone, codex's hooks and newly-synced rule are kept (i.e. the excluded tool is not resurrected, and sync for remaining tools is unaffected) ✅
    5. uninstall (no --agent) → full removal of both tools + shared, unchanged ✅
  • Docs updated bilingually (docs/usage-guide.md + .zh-CN.md): the extra shared-removal condition, case-insensitive tool matching, and the durable-exclusion guarantee.

🤖 Generated with Claude Code

@m0Nst3r873
m0Nst3r873 force-pushed the feature/uninstall-agent branch 2 times, most recently from 70a04f5 to b48269d Compare July 27, 2026 08:18

@jeff-r2026 jeff-r2026 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jeff-r2026 jeff-r2026 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff and traced the behavior through the pull sync paths. The refactor itself is clean — extracting discoverToolResources() is the right seam, and intersecting the managed-hooks manifest with actual file content in hasTeamaiHooks (so a stale manifest entry can't produce a false positive) is a nice catch. CI is green and the new test cases cover the interesting boundaries.

One functional gap is worth deciding on before merge, plus a few smaller items.

1. A single-tool uninstall does not survive the next pull (verified by running it)

--agent X exists primarily for "other tools are still on this machine, only remove X". On exactly that path, the uninstall is undone by the next pull.

I ran the real pull() and uninstall() against a fixture (only git fetch and the logger were mocked), with claude + codex both installed and enabledAgents unset (i.e. the user never used init --agent):

[STEP1] after pull — claude settings has hooks: true
        {"SessionStart":[{"matcher":"*","hooks":[{"command":"bash -lc \"teamai hook-dispatch session-start --tool claude ...

[STEP2] after `uninstall --agent claude`
        claude skill removed: true
        claude hooks: {"SessionStart":[],"Stop":[],"PostToolUse":[],"UserPromptSubmit":[]}
        ~/.teamai kept:  true      <- correct, codex is still present

[STEP3] after the next pull
        claude skill restored:  true
        claude hooks restored:  {"SessionStart":[{"matcher":"*","hooks":[{"command":"bash -lc \"teamai hook-dispatch
                                 session-start --tool claude ...","description":"[teamai] Hook dispatch session..."

Step 2 is exactly right — the skill is gone, hooks are stripped, shared resources are kept. The problem is step 3. Two independent reasons:

  • Skills / rules / agents / CLAUDE.md sync gates only on whether the tool's root directory exists (ResourceHandler.isToolInstalled, src/resources/base.ts). ~/.claude still exists after the uninstall — it is the user's own tool directory — so everything gets written back.
  • reconcileHooksAllScopes (src/pull.ts) re-injects built-in + team hooks on every pull, filtered only by localConfig.enabledAgents. init --agent appends to enabledAgents (src/init.ts), but uninstall --agent never removes the tool from it. And when enabledAgents is unset there is no filter at all, so the tool is re-hooked regardless.

And the user does not have to run pull for this to happen — a remaining tool's SessionStart hook triggers it silently:

// src/hook-handlers.ts
{ event: 'session-start', matcher: '*', handler: pullHandler, timeoutMs: PULL_TIMEOUT_MS },

So after uninstall --agent claude, opening one codex session brings claude's teamai resources back.

Options, roughly in order of how well they match the documented intent:

  1. Persist the exclusion: drop the tool from enabledAgents, and add something like disabledAgents that the skills/rules/agents sync also honors. This is what actually makes uninstall --agent symmetric with init --agent.
  2. Keep the current behavior but print an explicit warning after a non-last-tool uninstall and document that the removal is not durable.
  3. Scope the flag down to "uninstall the last tool" only.

2. An unknown tool name still exits 0

log.error(`未知工具 "${opts.agent}"。可用工具: ${tools.join(', ')}`);
return;

log.error only prints; it does not set an exit code. So teamai uninstall --agent clade --force looks successful to a script or CI job. The --agent validation in status.ts sets process.exitCode = 2 — worth matching here.

3. hasTeamaiHooks also changes the full-uninstall path

Gating hookFiles on hasTeamaiHooks applies to the no---agent path too. I checked that this cannot under-remove: the isManaged predicate inside each reconcile*Format is a subset of what hasTeamaiHooks matches (claude: isBuiltinClaudeEntry || isTeamClaudeEntry || manifest hit; the cursor and codex branches are identical to their reconcile counterparts). The only thing lost is the "clean up empty camelCase keys left by a previous incorrect injection" pass no longer running on files that hold no teamai hooks. Low impact, but it is an incidental behavior change that the PR description does not mention — worth a line there or in the changelog.

Related, and visible in step 2 above: after the uninstall, claude's settings.json keeps {"SessionStart":[],"Stop":[],"PostToolUse":[],"UserPromptSubmit":[]}. That is pre-existing reconcileHooks(removeAll) behavior, not something this PR introduced, but it becomes user-visible here because the settings file is now meant to stay on the user's machine after a targeted uninstall — they think claude is clean while teamai-created empty event keys remain. The existing empty-key cleanup only covers the camelCase spellings, so it does not catch these.

4. The docs describe one fewer condition than the code

The docs say shared resources are removed "only when the target is the last tool still using teamai", but the code requires targetHasResources && !othersHaveResources — the target must also have resources of its own. So a user who has env/docs installed but no resources landed in the tool directory yet gets "没有需要卸载的内容" from uninstall --agent claude, while a plain uninstall would remove the shared resources. Either document the extra condition or drop it. Case-insensitive matching is tested but also undocumented. (README only carries a command table without options, so no README change needed there.)

Nits

  • The three new user-facing strings are Chinese (⚠ 仅卸载工具: …, 未知工具 "…", 未检测到有效配置…), while CLAUDE.md requires English for CLI output. The file is already entirely Chinese so it is locally consistent, but --help is English and the runtime output is not — worth cleaning up, here or in a follow-up for the whole file.
  • The test name (P2#2 regression) references internal review numbering; a descriptive name would age better.
  • discoverToolResources runs for all configured tools even when --agent is given. That is necessary to compute othersHaveResources, but it could short-circuit as soon as one non-target tool is found to have resources.

Item 1 is the decision to make; item 2 is a one-liner. 3-5 are description/docs/style.

m0Nst3r873 added a commit to m0Nst3r873/teamai-cli that referenced this pull request Jul 29, 2026
Address PR Tencent#244 review. `uninstall --agent X` was undone by the next
pull (or another tool's session-start hook): resource sync gates only on
tool-dir existence and hooks re-inject filtered by enabledAgents, which
the uninstall never updated.

- Add localConfig.disabledAgents + isAgentDisabled() and honor it on
  every sync write-back: hooks (reconcileTeamHooksForConfig + auto-migrate
  inject), the pull.ts resource/CLAUDE.md loops, resources/{skills,rules,
  agents} pullItem, and builtin-{agents,rules,skills} deploy.
- uninstall --agent records the tool in disabledAgents (and prunes it from
  an existing enabledAgents whitelist) when shared ~/.teamai survives; a
  last-tool uninstall deletes the home so nothing is persisted. Leave an
  absent whitelist undefined rather than collapsing it to [] (which the
  hook path reads as "whitelist nothing").
- init --agent clears the exclusion again (symmetry).
- Unknown/--agent-without-config paths set process.exitCode = 2.
- English-ify the new user-facing strings.
- Docs: document the extra shared-removal condition, case-insensitive
  matching, and the durable-exclusion guarantee (both languages).
- Tests: persistence + whitelist-pruning + exit-code assertions;
  a pullItem-honors-disabledAgents gate test.

--other=OSS project, no TAPD ticket

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
m0Nst3r873 and others added 2 commits July 29, 2026 14:10
…encent#230)

Mirror `init --agent`: `uninstall --agent <tool>` removes only that tool's
teamai resources (hooks, CLAUDE.md block, skills, rules, built-in agents).
Shared resources (env block, docs, ~/.teamai) are removed only when the
target itself has teamai resources AND is the last tool still using teamai;
otherwise they are kept. Targeting a tool with no teamai resources is a
no-op for shared resources.

Add hooks.hasTeamaiHooks() so a tool whose settings file remains on disk
but has had its teamai hooks stripped is no longer counted as "still using
teamai" — fixes shared resources not being removed on the true last tool.
Manifest records only count when the recorded command still exists in the
settings file, so a stale managed-hooks entry cannot cause a false positive.

Unknown tool name aborts without deleting anything and lists available
tools. Tool name matching is case-insensitive. Behavior without --agent is
unchanged (full uninstall).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address PR Tencent#244 review. `uninstall --agent X` was undone by the next
pull (or another tool's session-start hook): resource sync gates only on
tool-dir existence and hooks re-inject filtered by enabledAgents, which
the uninstall never updated.

- Add localConfig.disabledAgents + isAgentDisabled() and honor it on
  every sync write-back: hooks (reconcileTeamHooksForConfig + auto-migrate
  inject), the pull.ts resource/CLAUDE.md loops, resources/{skills,rules,
  agents} pullItem, and builtin-{agents,rules,skills} deploy.
- uninstall --agent records the tool in disabledAgents (and prunes it from
  an existing enabledAgents whitelist) when shared ~/.teamai survives; a
  last-tool uninstall deletes the home so nothing is persisted. Leave an
  absent whitelist undefined rather than collapsing it to [] (which the
  hook path reads as "whitelist nothing").
- init --agent clears the exclusion again (symmetry).
- Unknown/--agent-without-config paths set process.exitCode = 2.
- English-ify the new user-facing strings.
- Docs: document the extra shared-removal condition, case-insensitive
  matching, and the durable-exclusion guarantee (both languages).
- Tests: persistence + whitelist-pruning + exit-code assertions;
  a pullItem-honors-disabledAgents gate test.

--other=OSS project, no TAPD ticket

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@m0Nst3r873
m0Nst3r873 force-pushed the feature/uninstall-agent branch from 115382a to 4fd162c Compare July 29, 2026 06:17
@jeff-r2026
jeff-r2026 merged commit bd6aba4 into Tencent:main Jul 29, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(uninstall): 支持 --agent <tool> 只卸载指定工具的资源

2 participants