# `augment uninstall` Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add `augment uninstall <codex|claude|grok|all>` that removes every agent-side artifact `augment install` wrote, without ever touching memory data.

**Architecture:** Path helpers, marker/TOML text helpers, and small fs/JSON helpers move out of the 961-line `src/install.ts` into a shared `src/install-paths.ts`. A new `src/uninstall.ts` imports those helpers and reverses each target (delete the plugin tree, prune the marketplace entry, prune `mcpServers.augment`, strip the marked markdown block, strip the Grok TOML tables). `src/cli.ts` gains an `uninstall` command with an injectable daemon-stop seam.

**Tech Stack:** TypeScript (ESM, `"type": "module"`, `.js` import specifiers), Node built-ins only (`node:fs/promises`, `node:path`, `node:os`), `node:test` + `node:assert/strict` run through tsx.

## Global Constraints

- Design spec: `docs/superpowers/specs/2026-08-03-uninstall-design.md`. Read it before Task 1.
- Never delete or modify `memoryRoot`, `<home>/.augment/config.json`, `<home>/.augment/npm-exec`, the state dir, or the database. Uninstall removes wiring, not data.
- Never delete `~/.claude.json` — prune keys only. It holds unrelated user state.
- Every operation is idempotent: a missing artifact is skipped, never an error. A second run must exit 0.
- Merge-writes preserve unrelated keys, unrelated marketplace plugins, unrelated TOML tables, and surrounding markdown prose.
- Imports inside `src/` use `.js` specifiers (`./install-paths.js`) even though the sources are `.ts`.
- Plugin name constant is `augment`; markers are `<!-- augment:start -->` / `<!-- augment:end -->`.
- Run a single test file with: `node --import tsx --test test/<name>.test.ts`
- Run everything with: `npm run check` (builds with tsc, then runs all tests). It must stay green at every commit.
- Commit after every task.

---

### Task 1: Extract shared install/uninstall primitives

Pure move — no behavior change. Existing tests are the regression net.

**Files:**
- Create: `src/install-paths.ts`
- Modify: `src/install.ts` (delete the moved code, import it instead)
- Test: `test/install.test.ts` (unchanged; must still pass)

**Interfaces:**
- Consumes: nothing from earlier tasks.
- Produces, all exported from `src/install-paths.ts`:
  - `type InstallScope = "user" | "repo"`
  - `interface PathContext { cwd: string; home: string; scope: InstallScope }`
  - `const PLUGIN_NAME: string`, `const SKILL_NAME: string`, `const PLUGIN_DESCRIPTION: string`, `const PLUGIN_AUTHOR: string`, `const PLUGIN_HOMEPAGE: string`, `const BEGIN_MARKER: string`, `const END_MARKER: string`
  - `interface ClaudePaths`, `interface CodexPaths`, `interface GrokPaths`
  - `function claudePaths(context: PathContext): ClaudePaths`
  - `function codexPaths(context: PathContext): CodexPaths`
  - `function grokPaths(context: PathContext): GrokPaths`
  - `function sharedNpmExecPrefix(home: string): string`
  - `function packageRoot(): string`
  - `function bundledSkillNames(): Promise<string[]>`
  - `function readOptional(file: string): Promise<string | undefined>`
  - `function readOptionalJson<T>(file: string): Promise<T | undefined>`
  - `function writeFileEnsured(file: string, content: string): Promise<void>`
  - `function jsonText(value: unknown): string`
  - `function stripBom(value: string): string`
  - `function upsertMarkedBlock(existing: string | undefined, block: string): string`
  - `function removeTomlTables(source: string, shouldRemove: (header: string) => boolean): string`
  - `function isGrokAugmentMcpTable(header: string): boolean`

- [ ] **Step 1: Create `src/install-paths.ts` with the moved code**

Move these declarations verbatim out of `src/install.ts` and export each one. `claudePaths`, `codexPaths`, and `grokPaths` currently take `ResolvedInstallOptions`; change their parameter type to the new `PathContext` (they only read `cwd`, `home`, and `scope`, so existing call sites keep compiling — `ResolvedInstallOptions` is structurally assignable).

```ts
import { readdir, readFile, mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

export type InstallScope = "user" | "repo";

/** Everything the path builders need. `ResolvedInstallOptions` satisfies this structurally. */
export interface PathContext {
  cwd: string;
  home: string;
  scope: InstallScope;
}

export const PLUGIN_NAME = "augment";
export const SKILL_NAME = "augment-context";
export const PLUGIN_DESCRIPTION = "Local RAG memory for coding agents.";
export const PLUGIN_AUTHOR = "fingerskier";
export const PLUGIN_HOMEPAGE = "https://www.npmjs.com/package/@fingerskier/augment";
export const BEGIN_MARKER = "<!-- augment:start -->";
export const END_MARKER = "<!-- augment:end -->";

export interface ClaudePaths {
  root: string;
  pluginRoot: string;
  pluginJsonPath: string;
  mcpConfigPath: string;
  hooksJsonPath: string;
  marketplacePath: string;
  marketplaceName: string;
  directConfigPath: string;
  claudeMdPath: string;
  npmExecPrefix: string;
}

export interface CodexPaths {
  pluginRoot: string;
  pluginJsonPath: string;
  mcpConfigPath: string;
  skillPath: string;
  hooksJsonPath: string;
  marketplacePath: string;
  marketplaceName: string;
  npmExecPrefix: string;
}

export interface GrokPaths {
  configTomlPath: string;
  skillsRoot: string;
  hooksJsonPath: string;
  agentsMdPath: string;
  npmExecPrefix: string;
}
```

Then move, unchanged except for the `export` keyword and the `PathContext` parameter type: `claudePaths`, `codexPaths` (give it the `CodexPaths` return type), `grokPaths`, `sharedNpmExecPrefix`, `packageRoot`, `readOptional`, `readOptionalJson`, `stripBom`, `writeFileEnsured`, `jsonText`, `upsertMarkedBlock`, `removeTomlTables`, `isGrokAugmentMcpTable`.

Add one genuinely new helper — the skill-name enumeration both installers already do inline:

```ts
/** Directory names of the skills this package bundles (`integrations/claude/skills/<name>`). */
export async function bundledSkillNames(): Promise<string[]> {
  const entries = await readdir(path.join(packageRoot(), "integrations", "claude", "skills"), {
    withFileTypes: true,
  });
  return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
}
```

- [ ] **Step 2: Rewire `src/install.ts`**

Delete the moved declarations from `src/install.ts` and add the import at the top:

```ts
import {
  BEGIN_MARKER,
  END_MARKER,
  PLUGIN_AUTHOR,
  PLUGIN_DESCRIPTION,
  PLUGIN_HOMEPAGE,
  PLUGIN_NAME,
  SKILL_NAME,
  claudePaths,
  codexPaths,
  grokPaths,
  jsonText,
  packageRoot,
  readOptional,
  readOptionalJson,
  sharedNpmExecPrefix,
  upsertMarkedBlock,
  writeFileEnsured,
  type InstallScope,
} from "./install-paths.js";
```

Keep `export type { InstallScope }` flowing from `src/install.ts` so `src/cli.ts` (which imports `InstallScope` from `./install.js`) keeps compiling — add:

```ts
export type { InstallScope } from "./install-paths.js";
```

and delete the old local `export type InstallScope = ...` line. Leave `installIntegration`, `upsertGrokMcpToml`, `defaultClaudeRunner`, `McpServerConfig`, and every other public export of `src/install.ts` exactly where they are — `test/install.test.ts` and `src/cli.ts` import them from there.

- [ ] **Step 3: Verify nothing changed**

Run: `npm run check`
Expected: build clean, all tests pass (same count as before the move).

- [ ] **Step 4: Commit**

```bash
git add src/install-paths.ts src/install.ts
git commit -m "refactor: extract shared install path and text helpers"
```

---

### Task 2: Removal helpers (marked block, Grok TOML)

**Files:**
- Modify: `src/install-paths.ts`
- Modify: `src/install.ts` (delegate `upsertGrokMcpToml`'s strip step — optional, see Step 3)
- Test: `test/uninstall-helpers.test.ts` (create)

**Interfaces:**
- Consumes: `BEGIN_MARKER`, `END_MARKER`, `removeTomlTables`, `isGrokAugmentMcpTable` from Task 1.
- Produces:
  - `function removeMarkedBlock(existing: string): string` — returns the input with the `<!-- augment:start -->…<!-- augment:end -->` span (and the blank lines it leaves behind) removed. Input without both markers is returned unchanged. A file that was only the block returns `""`.
  - `function removeGrokMcpToml(existing: string): string` — returns the TOML with every `[mcp_servers.augment*]` table removed, `""` when nothing else remains.

- [ ] **Step 1: Write the failing tests**

Create `test/uninstall-helpers.test.ts`:

```ts
import assert from "node:assert/strict";
import { test } from "node:test";
import { removeGrokMcpToml, removeMarkedBlock } from "../src/install-paths.js";

test("removeMarkedBlock strips the block and keeps surrounding prose", () => {
  const source = [
    "# My notes",
    "",
    "Keep this line.",
    "",
    "<!-- augment:start -->",
    "generated guidance",
    "<!-- augment:end -->",
    "",
    "And keep this trailer.",
    "",
  ].join("\n");

  const result = removeMarkedBlock(source);

  assert.match(result, /# My notes/);
  assert.match(result, /Keep this line\./);
  assert.match(result, /And keep this trailer\./);
  assert.doesNotMatch(result, /augment:start/);
  assert.doesNotMatch(result, /generated guidance/);
  assert.doesNotMatch(result, /augment:end/);
});

test("removeMarkedBlock returns empty string when the block was the whole file", () => {
  const source = ["<!-- augment:start -->", "only me", "<!-- augment:end -->", ""].join("\n");
  assert.equal(removeMarkedBlock(source).trim(), "");
});

test("removeMarkedBlock leaves unmarked content untouched", () => {
  const source = "# Just prose\n\nNo markers here.\n";
  assert.equal(removeMarkedBlock(source), source);
});

test("removeGrokMcpToml drops the augment tables and keeps the rest", () => {
  const source = [
    "[settings]",
    'theme = "dark"',
    "",
    "[mcp_servers.augment]",
    'command = "node"',
    'args = ["C:/x/augment-mcp.js"]',
    "",
    "[mcp_servers.augment.env]",
    'AUGMENT_MEMORY_ROOT = "C:/memories"',
    "",
    "[mcp_servers.other]",
    'command = "other"',
    "",
  ].join("\n");

  const result = removeGrokMcpToml(source);

  assert.match(result, /\[settings\]/);
  assert.match(result, /theme = "dark"/);
  assert.match(result, /\[mcp_servers\.other\]/);
  assert.doesNotMatch(result, /mcp_servers\.augment/);
  assert.doesNotMatch(result, /AUGMENT_MEMORY_ROOT/);
});

test("removeGrokMcpToml returns empty string when only augment was configured", () => {
  const source = ['[mcp_servers.augment]', 'command = "node"', ""].join("\n");
  assert.equal(removeGrokMcpToml(source).trim(), "");
});
```

- [ ] **Step 2: Run the tests to verify they fail**

Run: `node --import tsx --test test/uninstall-helpers.test.ts`
Expected: FAIL — `removeGrokMcpToml` and `removeMarkedBlock` are not exported from `../src/install-paths.js`.

- [ ] **Step 3: Implement the helpers**

Append to `src/install-paths.ts`:

```ts
/**
 * Inverse of {@link upsertMarkedBlock}: removes the augment-owned span from a markdown file the
 * user also edits by hand. Content before and after the markers survives verbatim; a file that
 * held nothing but the block collapses to "" so the caller can delete it.
 */
export function removeMarkedBlock(existing: string): string {
  const start = existing.indexOf(BEGIN_MARKER);
  const end = existing.indexOf(END_MARKER);
  if (start < 0 || end <= start) {
    return existing;
  }

  const before = existing.slice(0, start).trimEnd();
  const after = existing.slice(end + END_MARKER.length).trimStart();
  if (!before) {
    return after ? `${after.trimEnd()}\n` : "";
  }
  return after ? `${before}\n\n${after.trimEnd()}\n` : `${before}\n`;
}

/**
 * Inverse of {@link upsertGrokMcpToml}: strips `[mcp_servers.augment]` and its nested tables from
 * Grok's config.toml, preserving every unrelated section. Returns "" when nothing else remains.
 */
export function removeGrokMcpToml(existing: string): string {
  return removeTomlTables(existing, isGrokAugmentMcpTable);
}
```

- [ ] **Step 4: Run the tests to verify they pass**

Run: `node --import tsx --test test/uninstall-helpers.test.ts`
Expected: PASS (5 tests).

Then run: `npm run check`
Expected: everything still green.

- [ ] **Step 5: Commit**

```bash
git add src/install-paths.ts test/uninstall-helpers.test.ts
git commit -m "feat: add marked-block and Grok TOML removal helpers"
```

---

### Task 3: Uninstall primitives + Claude target

**Files:**
- Create: `src/uninstall.ts`
- Test: `test/uninstall.test.ts` (create)

**Interfaces:**
- Consumes: everything exported in Tasks 1–2, plus `type ClaudeCommandRunner` from `./install.js`.
- Produces, from `src/uninstall.ts`:
  - `type UninstallTarget = "codex" | "claude" | "grok" | "all"`
  - `interface UninstallOptions { target; scope; dryRun?; cwd?; home?; noPlugin?; keepDaemon?; runClaude?; stopDaemon? }`
  - `interface UninstallPlan { summary: string[]; removed: string[]; modified: string[] }`
  - `function uninstallIntegration(options: UninstallOptions): Promise<UninstallPlan>` — Task 3 handles the `claude` target only; Tasks 4–6 extend it.

- [ ] **Step 1: Write the failing tests**

Create `test/uninstall.test.ts`:

```ts
import assert from "node:assert/strict";
import { mkdtemp, readFile, rm, stat, writeFile, mkdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { test } from "node:test";
import { installIntegration } from "../src/install.js";
import { uninstallIntegration } from "../src/uninstall.js";

async function exists(target: string): Promise<boolean> {
  try {
    await stat(target);
    return true;
  } catch {
    return false;
  }
}

test("uninstall claude user scope removes every artifact install wrote", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-claude-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  try {
    await installIntegration({ target: "claude", scope: "user", home, cwd, noPlugin: true });

    const plan = await uninstallIntegration({
      target: "claude",
      scope: "user",
      home,
      cwd,
      noPlugin: true,
      keepDaemon: true,
    });

    assert.equal(await exists(path.join(home, "plugins", "augment")), false);
    assert.equal(await exists(path.join(home, ".claude-plugin", "marketplace.json")), false);

    const direct = JSON.parse(await readFile(path.join(home, ".claude.json"), "utf8")) as Record<string, unknown>;
    assert.equal("mcpServers" in direct, false);

    assert.equal(await exists(path.join(home, ".claude", "CLAUDE.md")), false);
    assert.ok(plan.removed.length > 0);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("uninstall claude preserves unrelated user state", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-keep-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  try {
    await mkdir(path.join(home, ".claude"), { recursive: true });
    await writeFile(
      path.join(home, ".claude.json"),
      JSON.stringify({ theme: "dark", mcpServers: { other: { command: "other" } } }, null, 2),
      "utf8",
    );
    await writeFile(path.join(home, ".claude", "CLAUDE.md"), "# My rules\n\nKeep me.\n", "utf8");

    await installIntegration({ target: "claude", scope: "user", home, cwd, noPlugin: true });
    await uninstallIntegration({
      target: "claude",
      scope: "user",
      home,
      cwd,
      noPlugin: true,
      keepDaemon: true,
    });

    const direct = JSON.parse(await readFile(path.join(home, ".claude.json"), "utf8")) as {
      theme: string;
      mcpServers: Record<string, unknown>;
    };
    assert.equal(direct.theme, "dark");
    assert.ok(direct.mcpServers.other);
    assert.equal("augment" in direct.mcpServers, false);

    const claudeMd = await readFile(path.join(home, ".claude", "CLAUDE.md"), "utf8");
    assert.match(claudeMd, /# My rules/);
    assert.match(claudeMd, /Keep me\./);
    assert.doesNotMatch(claudeMd, /augment:start/);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("uninstall claude repo scope deletes an emptied .mcp.json", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-repo-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  try {
    await installIntegration({ target: "claude", scope: "repo", home, cwd, noPlugin: true });
    await uninstallIntegration({
      target: "claude",
      scope: "repo",
      home,
      cwd,
      noPlugin: true,
      keepDaemon: true,
    });

    assert.equal(await exists(path.join(cwd, ".mcp.json")), false);
    assert.equal(await exists(path.join(cwd, "plugins", "augment")), false);
    assert.equal(await exists(path.join(cwd, "CLAUDE.md")), false);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("uninstall never touches the augment home or memories", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-safety-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  const memoryRoot = path.join(root, "memories");
  try {
    await mkdir(memoryRoot, { recursive: true });
    await writeFile(path.join(memoryRoot, "note.md"), "precious\n", "utf8");

    await installIntegration({ target: "claude", scope: "user", home, cwd, memoryRoot, noPlugin: true });
    await uninstallIntegration({
      target: "claude",
      scope: "user",
      home,
      cwd,
      noPlugin: true,
      keepDaemon: true,
    });

    assert.equal(await exists(path.join(home, ".augment", "config.json")), true);
    assert.equal(await readFile(path.join(memoryRoot, "note.md"), "utf8"), "precious\n");
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("uninstall is idempotent and reports nothing to remove", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-idem-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  try {
    await installIntegration({ target: "claude", scope: "user", home, cwd, noPlugin: true });
    await uninstallIntegration({ target: "claude", scope: "user", home, cwd, noPlugin: true, keepDaemon: true });

    const second = await uninstallIntegration({
      target: "claude",
      scope: "user",
      home,
      cwd,
      noPlugin: true,
      keepDaemon: true,
    });

    assert.equal(second.removed.length, 0);
    assert.equal(second.modified.length, 0);
    assert.ok(second.summary.some((line) => /Nothing to remove/i.test(line)));
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("uninstall dry run reports paths but writes nothing", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-dry-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  try {
    await installIntegration({ target: "claude", scope: "user", home, cwd, noPlugin: true });

    const plan = await uninstallIntegration({
      target: "claude",
      scope: "user",
      home,
      cwd,
      dryRun: true,
      noPlugin: true,
      keepDaemon: true,
    });

    assert.ok(plan.removed.some((file) => file.includes(path.join("plugins", "augment"))));
    assert.equal(await exists(path.join(home, "plugins", "augment")), true);
    assert.equal(await exists(path.join(home, ".claude-plugin", "marketplace.json")), true);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("uninstall claude deregisters the plugin through the claude CLI", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-cli-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  const calls: string[][] = [];
  try {
    await installIntegration({ target: "claude", scope: "user", home, cwd, noPlugin: true });

    await uninstallIntegration({
      target: "claude",
      scope: "user",
      home,
      cwd,
      keepDaemon: true,
      runClaude: async (_command, args) => {
        calls.push(args);
        return { available: true, ok: true };
      },
    });

    assert.ok(calls.some((args) => args.includes("uninstall")));
    assert.ok(calls.some((args) => args.join(" ").includes("marketplace")));
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("uninstall leaves malformed JSON untouched and says so", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-badjson-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  try {
    await installIntegration({ target: "claude", scope: "user", home, cwd, noPlugin: true });
    await writeFile(path.join(home, ".claude.json"), "{ not json", "utf8");

    const plan = await uninstallIntegration({
      target: "claude",
      scope: "user",
      home,
      cwd,
      noPlugin: true,
      keepDaemon: true,
    });

    assert.equal(await readFile(path.join(home, ".claude.json"), "utf8"), "{ not json");
    assert.ok(plan.summary.some((line) => /not valid JSON/i.test(line)));
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});
```

- [ ] **Step 2: Run the tests to verify they fail**

Run: `node --import tsx --test test/uninstall.test.ts`
Expected: FAIL — `../src/uninstall.js` cannot be resolved.

- [ ] **Step 3: Implement `src/uninstall.ts`**

```ts
import { rm, stat } from "node:fs/promises";
import { homedir } from "node:os";
import path from "node:path";
import type { ClaudeCommandRunner } from "./install.js";
import {
  BEGIN_MARKER,
  PLUGIN_NAME,
  claudePaths,
  jsonText,
  readOptional,
  removeMarkedBlock,
  stripBom,
  writeFileEnsured,
  type ClaudePaths,
  type InstallScope,
} from "./install-paths.js";

export type UninstallTarget = "codex" | "claude" | "grok" | "all";

export interface UninstallOptions {
  target: UninstallTarget;
  scope: InstallScope;
  dryRun?: boolean;
  cwd?: string;
  home?: string;
  /** Skip the best-effort `claude plugin uninstall` / `marketplace remove` calls. */
  noPlugin?: boolean;
  /** Leave a running daemon alone. */
  keepDaemon?: boolean;
  /** Runner used to invoke the `claude` CLI. Omitted -> manual instruction only. */
  runClaude?: ClaudeCommandRunner;
  /** Best-effort daemon stop. Injected so tests never touch a real process. */
  stopDaemon?: () => Promise<string | undefined>;
}

export interface UninstallPlan {
  summary: string[];
  /** Paths deleted outright. */
  removed: string[];
  /** Paths rewritten in place with the augment parts pruned out. */
  modified: string[];
}

interface UninstallContext {
  cwd: string;
  home: string;
  dryRun: boolean;
}

/** Accumulated effects of one removal step. */
interface Removal {
  summary: string[];
  removed: string[];
  modified: string[];
}

const NOTHING: Removal = { summary: [], removed: [], modified: [] };

export async function uninstallIntegration(options: UninstallOptions): Promise<UninstallPlan> {
  const context: UninstallContext = {
    cwd: path.resolve(options.cwd ?? process.cwd()),
    home: path.resolve(options.home ?? homedir()),
    dryRun: options.dryRun ?? false,
  };

  const effects: Removal[] = [];

  if (options.target === "claude" || options.target === "all") {
    effects.push(await uninstallClaude(options, context));
  }

  const plan = merge(effects);

  if (plan.removed.length === 0 && plan.modified.length === 0) {
    plan.summary.push("Nothing to remove — augment is not installed here.");
  }

  return plan;
}

async function uninstallClaude(options: UninstallOptions, context: UninstallContext): Promise<Removal> {
  const paths = claudePaths({ cwd: context.cwd, home: context.home, scope: options.scope });
  const effects: Removal[] = [
    await removeTree(paths.pluginRoot, context),
    await pruneMarketplace(paths.marketplacePath, context),
    // Repo `.mcp.json` is ours to delete when it empties; `~/.claude.json` is shared user state.
    await pruneMcpServer(paths.directConfigPath, context, options.scope === "repo"),
    await stripBlock(paths.claudeMdPath, context),
  ];

  const merged = merge(effects);
  merged.summary.unshift(
    `${context.dryRun ? "Would remove" : "Removed"} the Claude integration (${options.scope} scope).`,
  );
  merged.summary.push(...(await deregisterClaudePlugin(options, context, paths)));
  return merged;
}

/**
 * Best-effort inverse of the installer's `claude plugin` registration. Never fatal: a missing CLI
 * or a non-zero exit degrades to a manual instruction, because the on-disk artifacts are already
 * gone by the time this runs.
 */
async function deregisterClaudePlugin(
  options: UninstallOptions,
  context: UninstallContext,
  paths: ClaudePaths,
): Promise<string[]> {
  const claudeScope = options.scope === "repo" ? "project" : "user";
  const uninstallArgs = ["plugin", "uninstall", `${PLUGIN_NAME}@${paths.marketplaceName}`, "--scope", claudeScope];
  const marketplaceArgs = ["plugin", "marketplace", "remove", paths.marketplaceName, "--scope", claudeScope];
  const manual =
    `To finish deregistering: /plugin uninstall ${PLUGIN_NAME}@${paths.marketplaceName} ` +
    `then /plugin marketplace remove ${paths.marketplaceName}.`;

  if (options.noPlugin || !options.runClaude) {
    return [manual];
  }
  if (context.dryRun) {
    return [`Would deregister via claude CLI: claude ${uninstallArgs.join(" ")} && claude ${marketplaceArgs.join(" ")}.`];
  }

  const uninstalled = await options.runClaude("claude", uninstallArgs);
  if (!uninstalled.available) {
    return [`claude CLI not found on PATH — ${manual}`];
  }
  const removedMarketplace = await options.runClaude("claude", marketplaceArgs);
  if (uninstalled.ok && removedMarketplace.ok) {
    return [`Deregistered the augment plugin via claude CLI (marketplace '${paths.marketplaceName}').`];
  }
  return [
    `Plugin deregistration via claude CLI incomplete ` +
      `(uninstall ${uninstalled.ok ? "ok" : "failed"}, marketplace remove ${removedMarketplace.ok ? "ok" : "failed"}) — ${manual}`,
  ];
}

/** Deletes a directory tree we authored (plugin roots). Missing path is a no-op. */
async function removeTree(target: string, context: UninstallContext): Promise<Removal> {
  if (!(await pathExists(target))) {
    return NOTHING;
  }
  if (!context.dryRun) {
    await rm(target, { recursive: true, force: true });
  }
  return {
    summary: [`${context.dryRun ? "Would delete" : "Deleted"} ${target}.`],
    removed: [target],
    modified: [],
  };
}

/** Deletes a single file we authored. Missing path is a no-op. */
async function removeFile(target: string, context: UninstallContext): Promise<Removal> {
  if (!(await pathExists(target))) {
    return NOTHING;
  }
  if (!context.dryRun) {
    await rm(target, { force: true });
  }
  return {
    summary: [`${context.dryRun ? "Would delete" : "Deleted"} ${target}.`],
    removed: [target],
    modified: [],
  };
}

/**
 * Drops `mcpServers.augment` from a host config. Unrelated servers and top-level keys survive.
 * `deleteWhenEmpty` is true only for files the installer creates outright (repo `.mcp.json`);
 * `~/.claude.json` is shared user state and is never deleted.
 */
async function pruneMcpServer(
  file: string,
  context: UninstallContext,
  deleteWhenEmpty: boolean,
): Promise<Removal> {
  const parsed = await readJsonForPrune(file);
  if (parsed.kind !== "ok") {
    return parsed.removal;
  }

  const config = parsed.value;
  const servers = isRecord(config.mcpServers) ? { ...(config.mcpServers as Record<string, unknown>) } : undefined;
  if (!servers || !(PLUGIN_NAME in servers)) {
    return NOTHING;
  }

  delete servers[PLUGIN_NAME];
  const next: Record<string, unknown> = { ...config };
  if (Object.keys(servers).length === 0) {
    delete next.mcpServers;
  } else {
    next.mcpServers = servers;
  }

  if (deleteWhenEmpty && Object.keys(next).length === 0) {
    return removeFile(file, context);
  }

  if (!context.dryRun) {
    await writeFileEnsured(file, jsonText(next));
  }
  return {
    summary: [`${context.dryRun ? "Would remove" : "Removed"} the augment MCP server from ${file}.`],
    removed: [],
    modified: [file],
  };
}

/**
 * Drops the augment entry from a marketplace manifest (same `plugins[].name` shape for Claude and
 * Codex). The file is deleted once no plugins remain — the installer created it.
 */
async function pruneMarketplace(file: string, context: UninstallContext): Promise<Removal> {
  const parsed = await readJsonForPrune(file);
  if (parsed.kind !== "ok") {
    return parsed.removal;
  }

  const config = parsed.value;
  const plugins = Array.isArray(config.plugins) ? (config.plugins as Array<{ name?: string }>) : undefined;
  if (!plugins) {
    return NOTHING;
  }

  const kept = plugins.filter((plugin) => plugin?.name !== PLUGIN_NAME);
  if (kept.length === plugins.length) {
    return NOTHING;
  }

  if (kept.length === 0) {
    return removeFile(file, context);
  }

  if (!context.dryRun) {
    await writeFileEnsured(file, jsonText({ ...config, plugins: kept }));
  }
  return {
    summary: [`${context.dryRun ? "Would remove" : "Removed"} the augment plugin entry from ${file}.`],
    removed: [],
    modified: [file],
  };
}

/**
 * Strips the augment-owned block from a markdown file the user also edits. The file is deleted
 * only when the block was all it contained.
 */
async function stripBlock(file: string, context: UninstallContext): Promise<Removal> {
  const raw = await readOptional(file);
  if (raw === undefined || !raw.includes(BEGIN_MARKER)) {
    return NOTHING;
  }

  const next = removeMarkedBlock(raw);
  if (next.trim().length === 0) {
    return removeFile(file, context);
  }

  if (!context.dryRun) {
    await writeFileEnsured(file, next);
  }
  return {
    summary: [`${context.dryRun ? "Would remove" : "Removed"} the augment block from ${file}.`],
    removed: [],
    modified: [file],
  };
}

type ParsedJson =
  | { kind: "ok"; value: Record<string, unknown> }
  | { kind: "skip"; removal: Removal };

/**
 * Reads a JSON config for pruning. A missing file is a silent no-op; a malformed one is reported
 * and left alone rather than clobbered, because it holds user state we cannot safely rewrite.
 */
async function readJsonForPrune(file: string): Promise<ParsedJson> {
  const raw = await readOptional(file);
  if (raw === undefined) {
    return { kind: "skip", removal: NOTHING };
  }
  try {
    const value = JSON.parse(stripBom(raw)) as unknown;
    if (!isRecord(value)) {
      return { kind: "skip", removal: NOTHING };
    }
    return { kind: "ok", value };
  } catch {
    return {
      kind: "skip",
      removal: {
        summary: [`Left ${file} untouched — it is not valid JSON. Remove the augment entry by hand.`],
        removed: [],
        modified: [],
      },
    };
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

async function pathExists(target: string): Promise<boolean> {
  try {
    await stat(target);
    return true;
  } catch {
    return false;
  }
}

function merge(effects: Removal[]): UninstallPlan {
  return {
    summary: effects.flatMap((effect) => effect.summary),
    removed: effects.flatMap((effect) => effect.removed),
    modified: effects.flatMap((effect) => effect.modified),
  };
}
```

- [ ] **Step 4: Run the tests to verify they pass**

Run: `node --import tsx --test test/uninstall.test.ts`
Expected: PASS (8 tests).

Then run: `npm run check`
Expected: green.

- [ ] **Step 5: Commit**

```bash
git add src/uninstall.ts test/uninstall.test.ts
git commit -m "feat: uninstall the Claude integration"
```

---

### Task 4: Codex target

**Files:**
- Modify: `src/uninstall.ts`
- Test: `test/uninstall.test.ts`

**Interfaces:**
- Consumes: `codexPaths` from Task 1; `removeTree`, `pruneMarketplace`, `merge`, `Removal` from Task 3.
- Produces: `uninstallIntegration` now handles `target: "codex"`.

- [ ] **Step 1: Write the failing test**

Append to `test/uninstall.test.ts`:

```ts
test("uninstall codex removes the plugin tree and marketplace entry", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-codex-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  try {
    await installIntegration({ target: "codex", scope: "user", home, cwd });

    const plan = await uninstallIntegration({
      target: "codex",
      scope: "user",
      home,
      cwd,
      keepDaemon: true,
    });

    assert.equal(await exists(path.join(home, "plugins", "augment")), false);
    assert.equal(await exists(path.join(home, ".agents", "plugins", "marketplace.json")), false);
    assert.ok(plan.summary.some((line) => /Codex/.test(line)));
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("uninstall codex keeps a foreign marketplace plugin", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-codex-keep-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  const marketplacePath = path.join(home, ".agents", "plugins", "marketplace.json");
  try {
    await installIntegration({ target: "codex", scope: "user", home, cwd });

    const before = JSON.parse(await readFile(marketplacePath, "utf8")) as { plugins: unknown[] };
    before.plugins.push({ name: "other", source: { source: "local", path: "./plugins/other" } });
    await writeFile(marketplacePath, `${JSON.stringify(before, null, 2)}\n`, "utf8");

    await uninstallIntegration({ target: "codex", scope: "user", home, cwd, keepDaemon: true });

    const after = JSON.parse(await readFile(marketplacePath, "utf8")) as {
      plugins: Array<{ name: string }>;
    };
    assert.deepEqual(
      after.plugins.map((plugin) => plugin.name),
      ["other"],
    );
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});
```

- [ ] **Step 2: Run the tests to verify they fail**

Run: `node --import tsx --test test/uninstall.test.ts`
Expected: FAIL — the codex artifacts still exist because `uninstallIntegration` ignores the `codex` target.

- [ ] **Step 3: Implement the codex branch**

In `src/uninstall.ts`, add `codexPaths` to the `./install-paths.js` import, add the branch in `uninstallIntegration` **before** the claude branch (matching install order):

```ts
  if (options.target === "codex" || options.target === "all") {
    effects.push(await uninstallCodex(options, context));
  }
```

and add the function:

```ts
async function uninstallCodex(options: UninstallOptions, context: UninstallContext): Promise<Removal> {
  const paths = codexPaths({ cwd: context.cwd, home: context.home, scope: options.scope });
  const merged = merge([
    // Skills, hooks, plugin.json, and .mcp.json all live inside the plugin root.
    await removeTree(paths.pluginRoot, context),
    await pruneMarketplace(paths.marketplacePath, context),
  ]);
  merged.summary.unshift(
    `${context.dryRun ? "Would remove" : "Removed"} the Codex integration (${options.scope} scope).`,
  );
  return merged;
}
```

Note `merge` returns an `UninstallPlan`, which is structurally identical to `Removal` — declare `uninstallCodex`'s return type as `Promise<Removal>` and it compiles.

- [ ] **Step 4: Run the tests to verify they pass**

Run: `node --import tsx --test test/uninstall.test.ts`
Expected: PASS (10 tests).

- [ ] **Step 5: Commit**

```bash
git add src/uninstall.ts test/uninstall.test.ts
git commit -m "feat: uninstall the Codex integration"
```

---

### Task 5: Grok target

**Files:**
- Modify: `src/uninstall.ts`
- Test: `test/uninstall.test.ts`

**Interfaces:**
- Consumes: `grokPaths`, `bundledSkillNames`, `removeGrokMcpToml`, `readOptional` from Tasks 1–2; `removeTree`, `removeFile`, `stripBlock`, `merge` from Task 3.
- Produces: `uninstallIntegration` now handles `target: "grok"`.

- [ ] **Step 1: Write the failing test**

Append to `test/uninstall.test.ts`:

```ts
test("uninstall grok strips the mcp table, hooks, and bundled skills", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-grok-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  const configTomlPath = path.join(home, ".grok", "config.toml");
  try {
    await installIntegration({ target: "grok", scope: "user", home, cwd });

    // A hand-written section and a hand-written skill must both survive.
    const installed = await readFile(configTomlPath, "utf8");
    await writeFile(configTomlPath, `${installed}\n[settings]\ntheme = "dark"\n`, "utf8");
    await mkdir(path.join(home, ".grok", "skills", "mine"), { recursive: true });
    await writeFile(path.join(home, ".grok", "skills", "mine", "SKILL.md"), "mine\n", "utf8");

    await uninstallIntegration({ target: "grok", scope: "user", home, cwd, keepDaemon: true });

    const toml = await readFile(configTomlPath, "utf8");
    assert.doesNotMatch(toml, /mcp_servers\.augment/);
    assert.match(toml, /\[settings\]/);

    assert.equal(await exists(path.join(home, ".grok", "hooks", "augment.json")), false);
    assert.equal(await exists(path.join(home, ".grok", "skills", "augment-context")), false);
    assert.equal(await exists(path.join(home, ".grok", "skills", "mine", "SKILL.md")), true);
    assert.equal(await exists(path.join(home, ".grok", "AGENTS.md")), false);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("uninstall grok deletes a config.toml that held only augment", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-grok-empty-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  try {
    await installIntegration({ target: "grok", scope: "user", home, cwd });
    await uninstallIntegration({ target: "grok", scope: "user", home, cwd, keepDaemon: true });

    assert.equal(await exists(path.join(home, ".grok", "config.toml")), false);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});
```

- [ ] **Step 2: Run the tests to verify they fail**

Run: `node --import tsx --test test/uninstall.test.ts`
Expected: FAIL — grok artifacts remain.

- [ ] **Step 3: Implement the grok branch**

Add `bundledSkillNames`, `grokPaths`, and `removeGrokMcpToml` to the `./install-paths.js` import in `src/uninstall.ts`, add the branch after the claude branch:

```ts
  if (options.target === "grok" || options.target === "all") {
    effects.push(await uninstallGrok(options, context));
  }
```

and add:

```ts
async function uninstallGrok(options: UninstallOptions, context: UninstallContext): Promise<Removal> {
  const paths = grokPaths({ cwd: context.cwd, home: context.home, scope: options.scope });
  // Only the skills this package ships — never the whole skills root, which is user territory.
  const skillNames = await bundledSkillNames();
  const skillEffects: Removal[] = [];
  for (const name of skillNames) {
    skillEffects.push(await removeTree(path.join(paths.skillsRoot, name), context));
  }

  const merged = merge([
    await pruneGrokToml(paths.configTomlPath, context),
    await removeFile(paths.hooksJsonPath, context),
    ...skillEffects,
    await stripBlock(paths.agentsMdPath, context),
  ]);
  merged.summary.unshift(
    `${context.dryRun ? "Would remove" : "Removed"} the Grok Build integration (${options.scope} scope).`,
  );
  return merged;
}

/** Strips `[mcp_servers.augment*]` from Grok's config.toml; deletes the file when nothing is left. */
async function pruneGrokToml(file: string, context: UninstallContext): Promise<Removal> {
  const raw = await readOptional(file);
  if (raw === undefined || !raw.includes("mcp_servers.augment")) {
    return NOTHING;
  }

  const next = removeGrokMcpToml(raw);
  if (next.trim().length === 0) {
    return removeFile(file, context);
  }

  if (!context.dryRun) {
    await writeFileEnsured(file, next);
  }
  return {
    summary: [`${context.dryRun ? "Would remove" : "Removed"} the augment MCP server from ${file}.`],
    removed: [],
    modified: [file],
  };
}
```

- [ ] **Step 4: Run the tests to verify they pass**

Run: `node --import tsx --test test/uninstall.test.ts`
Expected: PASS (12 tests).

- [ ] **Step 5: Commit**

```bash
git add src/uninstall.ts test/uninstall.test.ts
git commit -m "feat: uninstall the Grok Build integration"
```

---

### Task 6: `all` target and daemon stop

**Files:**
- Modify: `src/uninstall.ts`
- Test: `test/uninstall.test.ts`

**Interfaces:**
- Consumes: the three target functions from Tasks 3–5.
- Produces: `uninstallIntegration` honors `target: "all"` and calls `options.stopDaemon` once unless `keepDaemon` is set.

- [ ] **Step 1: Write the failing tests**

Append to `test/uninstall.test.ts`:

```ts
test("uninstall all reverses every target", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-all-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  try {
    await installIntegration({ target: "all", scope: "user", home, cwd, noPlugin: true });

    await uninstallIntegration({
      target: "all",
      scope: "user",
      home,
      cwd,
      noPlugin: true,
      keepDaemon: true,
    });

    assert.equal(await exists(path.join(home, "plugins", "augment")), false);
    assert.equal(await exists(path.join(home, ".claude-plugin", "marketplace.json")), false);
    assert.equal(await exists(path.join(home, ".agents", "plugins", "marketplace.json")), false);
    assert.equal(await exists(path.join(home, ".grok", "config.toml")), false);
    assert.equal(await exists(path.join(home, ".grok", "hooks", "augment.json")), false);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("uninstall stops the daemon once through the injected seam", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-daemon-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  let calls = 0;
  try {
    await installIntegration({ target: "claude", scope: "user", home, cwd, noPlugin: true });

    const plan = await uninstallIntegration({
      target: "claude",
      scope: "user",
      home,
      cwd,
      noPlugin: true,
      stopDaemon: async () => {
        calls += 1;
        return "Stopped augment daemon pid 42.";
      },
    });

    assert.equal(calls, 1);
    assert.ok(plan.summary.some((line) => line.includes("pid 42")));
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("uninstall survives a daemon stop that throws", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-daemon-fail-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  try {
    await installIntegration({ target: "claude", scope: "user", home, cwd, noPlugin: true });

    const plan = await uninstallIntegration({
      target: "claude",
      scope: "user",
      home,
      cwd,
      noPlugin: true,
      stopDaemon: async () => {
        throw new Error("timed out");
      },
    });

    assert.ok(plan.summary.some((line) => /timed out/.test(line)));
    assert.equal(await exists(path.join(home, "plugins", "augment")), false);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("uninstall --keep-daemon never calls the seam", async () => {
  const root = await mkdtemp(path.join(tmpdir(), "augment-uninstall-keep-daemon-"));
  const home = path.join(root, "home");
  const cwd = path.join(root, "repo");
  let calls = 0;
  try {
    await installIntegration({ target: "claude", scope: "user", home, cwd, noPlugin: true });

    await uninstallIntegration({
      target: "claude",
      scope: "user",
      home,
      cwd,
      noPlugin: true,
      keepDaemon: true,
      stopDaemon: async () => {
        calls += 1;
        return undefined;
      },
    });

    assert.equal(calls, 0);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});
```

- [ ] **Step 2: Run the tests to verify they fail**

Run: `node --import tsx --test test/uninstall.test.ts`
Expected: FAIL — the daemon seam is never called (`calls` is 0 where 1 is expected).

The `uninstall all` test may already pass from Tasks 3–5; that is fine, it is the regression guard for the combined path.

- [ ] **Step 3: Implement the daemon stop**

In `src/uninstall.ts`, after building `plan` and before the "Nothing to remove" line, add:

```ts
  if (!options.keepDaemon) {
    plan.summary.push(...(await stopDaemonBestEffort(options, context)));
  }
```

and add the function:

```ts
/**
 * Best-effort daemon stop so no stale process keeps serving a config we just tore down. Never
 * fatal — the wiring is already gone, and a stuck daemon is a nuisance, not a failed uninstall.
 */
async function stopDaemonBestEffort(options: UninstallOptions, context: UninstallContext): Promise<string[]> {
  if (context.dryRun) {
    return ["Would stop the augment daemon."];
  }
  if (!options.stopDaemon) {
    return ["Stop the augment daemon manually if one is running."];
  }
  try {
    return [(await options.stopDaemon()) ?? "Stopped the augment daemon."];
  } catch (error) {
    return [
      `Could not stop the augment daemon (${(error as Error).message}) — stop it manually.`,
    ];
  }
}
```

- [ ] **Step 4: Run the tests to verify they pass**

Run: `node --import tsx --test test/uninstall.test.ts`
Expected: PASS (16 tests).

Then run: `npm run check`
Expected: green.

- [ ] **Step 5: Commit**

```bash
git add src/uninstall.ts test/uninstall.test.ts
git commit -m "feat: uninstall all targets and stop the daemon"
```

---

### Task 7: CLI wiring

**Files:**
- Modify: `src/cli.ts`
- Test: `test/cli.test.ts`

**Interfaces:**
- Consumes: `uninstallIntegration`, `UninstallTarget`, `UninstallOptions` from Task 6; `stopRecordedDaemon` from `./daemon/restart.js`; `loadConfig` from `./config.js`.
- Produces:
  - `function parseUninstallArgs(args: string[]): { target: UninstallTarget; scope: InstallScope; dryRun?: boolean; noPlugin?: boolean; keepDaemon?: boolean }` (exported)
  - `augment uninstall …` command dispatch
  - `helpText()` gains the uninstall command line and an `UNINSTALL` section.

- [ ] **Step 1: Write the failing tests**

Append to `test/cli.test.ts` (match the file's existing import style; it already imports from `../src/cli.js`):

```ts
test("parseUninstallArgs round-trips every flag", () => {
  const parsed = parseUninstallArgs(["all", "--scope", "repo", "--dry-run", "--no-plugin", "--keep-daemon"]);
  assert.deepEqual(parsed, {
    target: "all",
    scope: "repo",
    dryRun: true,
    noPlugin: true,
    keepDaemon: true,
  });
});

test("parseUninstallArgs defaults to user scope with no flags", () => {
  const parsed = parseUninstallArgs(["claude"]);
  assert.equal(parsed.scope, "user");
  assert.equal(parsed.dryRun, false);
  assert.equal(parsed.noPlugin, false);
  assert.equal(parsed.keepDaemon, false);
});

test("parseUninstallArgs rejects a bad target and a bad flag", () => {
  assert.throws(() => parseUninstallArgs(["nope"]), /Usage: augment uninstall/);
  assert.throws(() => parseUninstallArgs(["claude", "--wat"]), /Unknown uninstall option/);
});

test("help text documents uninstall", () => {
  const text = helpText();
  assert.match(text, /uninstall <target>/);
  assert.match(text, /augment uninstall <codex\|claude\|grok\|all>/);
  assert.match(text, /--keep-daemon/);
});
```

Add `parseUninstallArgs` to the existing `../src/cli.js` import in that file (`helpText` is already imported there; if it is not, add it too).

- [ ] **Step 2: Run the tests to verify they fail**

Run: `node --import tsx --test test/cli.test.ts`
Expected: FAIL — `parseUninstallArgs` is not exported from `../src/cli.js`.

- [ ] **Step 3: Implement the CLI command**

In `src/cli.ts`, extend the imports:

```ts
import { stopRecordedDaemon } from "./daemon/restart.js";
import { uninstallIntegration, type UninstallTarget } from "./uninstall.js";
```

(`restartDaemon` is already imported from `./daemon/restart.js` — add `stopRecordedDaemon` to that same import.)

Add the command handler right after the existing `install` block:

```ts
  if (command === "uninstall") {
    const parsed = parseUninstallArgs(args.slice(1));
    const plan = await uninstallIntegration({
      ...parsed,
      runClaude: defaultClaudeRunner,
      stopDaemon: deps.stopDaemon ?? defaultStopDaemon,
    });
    for (const line of plan.summary) {
      console.log(line);
    }
    for (const file of [...plan.removed, ...plan.modified]) {
      console.log(`- ${file}`);
    }
    return;
  }
```

Add the seam to `RunCliDeps`:

```ts
  /** Best-effort daemon stop used by `uninstall`. */
  stopDaemon?: () => Promise<string | undefined>;
```

Add the default implementation next to `defaultOpenUrl`:

```ts
/** Stops the recorded daemon so a torn-down install leaves no stale process behind. */
async function defaultStopDaemon(): Promise<string | undefined> {
  const info = await stopRecordedDaemon(await loadConfig());
  return info ? `Stopped augment daemon pid ${info.pid}.` : "No augment daemon was running.";
}
```

Add the parser next to `parseInstallArgs`:

```ts
export function parseUninstallArgs(args: string[]): {
  target: UninstallTarget;
  scope: InstallScope;
  dryRun: boolean;
  noPlugin: boolean;
  keepDaemon: boolean;
} {
  const target = args[0] as UninstallTarget | undefined;
  if (!target || !["codex", "claude", "grok", "all"].includes(target)) {
    throw new Error(
      "Usage: augment uninstall <codex|claude|grok|all> [--scope user|repo] [--dry-run] " +
        "[--no-plugin] [--keep-daemon]",
    );
  }

  let scope: InstallScope = "user";
  let dryRun = false;
  let noPlugin = false;
  let keepDaemon = false;

  for (let index = 1; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--scope") {
      const value = args[++index] as InstallScope | undefined;
      if (value !== "user" && value !== "repo") {
        throw new Error("--scope must be user or repo");
      }
      scope = value;
    } else if (arg === "--dry-run") {
      dryRun = true;
    } else if (arg === "--no-plugin") {
      noPlugin = true;
    } else if (arg === "--keep-daemon") {
      keepDaemon = true;
    } else {
      throw new Error(`Unknown uninstall option: ${arg}`);
    }
  }

  return { target, scope, dryRun, noPlugin, keepDaemon };
}
```

In `helpText()`, add to the `COMMANDS` block right after the install line:

```ts
    "  uninstall <target> Remove an agent integration (see below).",
```

and add this section immediately after the `INSTALL` section's options list (before the `RUN FROM npm` block):

```ts
    "",
    "UNINSTALL",
    "  augment uninstall <codex|claude|grok|all> [options]",
    "",
    "  Removes the plugin tree, marketplace entry, MCP registration, hooks, skills, and the",
    "  marked CLAUDE.md / AGENTS.md block. Memories are never touched, and neither is",
    "  ~/.augment (config, runtime, state).",
    "",
    "  Options:",
    "    --scope user|repo     Reverse the user (home) or repo install. Default: user.",
    "    --dry-run             Print what would be removed without writing.",
    "    --no-plugin           Skip the best-effort `claude plugin` deregistration (claude only).",
    "    --keep-daemon         Leave a running augment daemon alone.",
```

- [ ] **Step 4: Run the tests to verify they pass**

Run: `node --import tsx --test test/cli.test.ts`
Expected: PASS.

Then run: `npm run check`
Expected: green.

- [ ] **Step 5: Manual smoke test**

```bash
npm run build
node ./dist/bin/augment.js uninstall all --scope repo --dry-run --keep-daemon
```

Expected: a plan listing repo-scope paths, and no files changed (`git status` clean).

- [ ] **Step 6: Commit**

```bash
git add src/cli.ts test/cli.test.ts
git commit -m "feat: add the uninstall command to the CLI"
```

---

### Task 8: Documentation

**Files:**
- Modify: `README.md`
- Modify: `docs/FEATURES.md`
- Test: `test/packaging.test.ts` (unchanged; must still pass)

**Interfaces:**
- Consumes: the CLI surface from Task 7.
- Produces: user-facing docs for `uninstall`.

- [ ] **Step 1: Add the README section**

Find the existing install section in `README.md` and add directly after it:

````markdown
### Uninstall

```bash
npx -y @fingerskier/augment uninstall all --scope user
```

Removes the plugin tree, marketplace entry, MCP server registration, hooks,
skills, and the marked `CLAUDE.md` / `AGENTS.md` block for each target
(`codex`, `claude`, `grok`, or `all`), then stops the shared daemon.

Your memories are never touched. Neither is `~/.augment` — the config, the
provisioned runtime, and the local database all survive, so a later
`augment install` picks up exactly where you left off. To delete memories,
remove the `memoryRoot` directory yourself.

| Flag | Effect |
| --- | --- |
| `--scope user\|repo` | Reverse the home install or the current repo's. Default: `user`. |
| `--dry-run` | Print what would be removed without writing. |
| `--no-plugin` | Skip the best-effort `claude plugin` deregistration. |
| `--keep-daemon` | Leave a running daemon alone. |
````

- [ ] **Step 2: Mirror it in `docs/FEATURES.md`**

Find the installer section in `docs/FEATURES.md` and add a matching paragraph after it: name the command (`augment uninstall <codex|claude|grok|all>`), list the four flags (`--scope`, `--dry-run`, `--no-plugin`, `--keep-daemon`), state what is removed (plugin tree, marketplace entry, MCP registration, hooks, skills, marked markdown block), and state the guarantee that memories and `~/.augment` are never touched. Match the surrounding heading level and prose style.

- [ ] **Step 3: Verify**

Run: `npm run check`
Expected: green (packaging tests included).

- [ ] **Step 4: Commit**

```bash
git add README.md FEATURES.md
git commit -m "docs: document augment uninstall"
```

---

## Verification (whole feature)

Run after Task 8:

```bash
npm run check
```

Expected: TypeScript build clean, every test passing, including the ~16 new
uninstall tests, the 5 helper tests, and the 4 new CLI tests, with the existing
install suite unchanged.
