fapony 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +144 -156
  2. package/package.json +5 -6
  3. package/skill/define-convention/SKILL.md +3 -3
  4. package/skill/lookup-before-edit/SKILL.md +2 -1
  5. package/skill/move-to-done/SKILL.md +18 -22
  6. package/skill/plan-with-pony/SKILL.md +4 -4
  7. package/skill/review-pony/SKILL.md +10 -12
  8. package/src/adapters/cli.ts +11 -52
  9. package/src/adapters/hooks/index.ts +0 -67
  10. package/src/adapters/hooks/mv-guard.ts +2 -2
  11. package/src/analyze/discover.ts +1 -1
  12. package/src/analyze/index.ts +0 -1
  13. package/src/commands.ts +8 -39
  14. package/src/core/config.ts +0 -8
  15. package/src/core/fapony-dir.ts +71 -0
  16. package/src/core/hook-helpers.ts +2 -11
  17. package/src/debt/load.ts +3 -5
  18. package/src/debt/promotion.ts +2 -2
  19. package/src/debt/scan.ts +1 -1
  20. package/src/digest/collect.ts +6 -7
  21. package/src/fael.ts +139 -0
  22. package/src/hook.ts +1 -81
  23. package/src/init.ts +30 -90
  24. package/src/install/antigravity.ts +8 -99
  25. package/src/install/claude.ts +66 -200
  26. package/src/install/codex.ts +6 -170
  27. package/src/install/detect.ts +4 -14
  28. package/src/install/opencode.ts +50 -486
  29. package/src/install/types.ts +1 -39
  30. package/src/install/zcode.ts +5 -91
  31. package/src/install.ts +9 -35
  32. package/src/memory.ts +10 -69
  33. package/src/plan/index.ts +34 -0
  34. package/src/plan/next.ts +181 -0
  35. package/src/plan/store.ts +41 -0
  36. package/src/{mem/commands/plan.ts → plan/sweep.ts} +66 -46
  37. package/src/seed/plan-seed.ts +10 -9
  38. package/src/setup.ts +4 -22
  39. package/src/update.ts +1 -1
  40. package/templates/PLAN.md +1 -1
  41. package/src/adapters/hooks/bug-markers.ts +0 -60
  42. package/src/adapters/hooks/context-data.ts +0 -211
  43. package/src/adapters/hooks/edit-hint.ts +0 -215
  44. package/src/adapters/hooks/read-hint.ts +0 -400
  45. package/src/adapters/hooks/session-start.ts +0 -112
  46. package/src/adapters/hooks/stop.ts +0 -768
  47. package/src/adapters/mcp/tools/index.ts +0 -149
  48. package/src/adapters/mcp/tools/mem.ts +0 -258
  49. package/src/adapters/mcp/transport.ts +0 -147
  50. package/src/analyze/cache.ts +0 -162
  51. package/src/core/mem-log.ts +0 -394
  52. package/src/init-mem.ts +0 -143
  53. package/src/install/cursor.ts +0 -167
  54. package/src/install/utils.ts +0 -29
  55. package/src/mem/commands/read.ts +0 -792
  56. package/src/mem/commands/rotate.ts +0 -59
  57. package/src/mem/commands/where.ts +0 -56
  58. package/src/mem/commands/write.ts +0 -284
  59. package/src/mem/engine.ts +0 -329
  60. package/src/mem/index.ts +0 -201
  61. package/src/mem/key-registry.ts +0 -116
  62. package/src/mem/render.ts +0 -66
  63. package/src/mem/selectors.ts +0 -227
  64. package/src/mem/store.ts +0 -356
@@ -1,160 +1,89 @@
1
1
  // src/install/claude.ts — Claude Code install provider
2
2
  //
3
- // Shells out to `claude mcp add` (never parses/writes ~/.claude.json directly).
3
+ // Skills symlink + the PreToolUse hooks fapony still owns. Memory (MCP, Stop,
4
+ // SessionStart, per-file read context) moved to fael — `fael install` wires it.
4
5
 
5
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
7
  import { homedir } from "node:os";
7
8
  import { join } from "node:path";
8
- import { assertSafe } from "../safety.js";
9
9
  import { claudeSkillsDir, linkSkills, reportSkills } from "./skills.js";
10
- import {
11
- type ClaudeRunResult,
12
- defaultExit,
13
- INSTALL_ROOT,
14
- type InstallDeps,
15
- } from "./types.js";
10
+ import { INSTALL_ROOT, type InstallDeps } from "./types.js";
16
11
 
17
- function defaultRun(argv: string[]): ClaudeRunResult {
18
- assertSafe(argv);
19
- try {
20
- const proc = Bun.spawnSync(argv, {
21
- stdout: "pipe",
22
- stderr: "pipe",
23
- });
24
- return {
25
- exitCode: proc.exitCode,
26
- stdout: proc.stdout.toString(),
27
- stderr: proc.stderr.toString(),
28
- };
29
- } catch (e) {
30
- // `claude` binary missing (ENOENT) or spawn failed outright.
31
- return { exitCode: 127, stdout: "", stderr: (e as Error).message };
32
- }
33
- }
34
-
35
- /** `claude mcp get fapony` — read-only probe. exit 0 = an entry named fapony exists. */
36
- export function claudeGetArgs(): string[] {
37
- return ["claude", "mcp", "get", "fapony"];
38
- }
39
-
40
- /** Absolute-path add command — works even before `bun link` puts `fapony` on PATH. */
41
- export function claudeAddArgs(): string[] {
42
- return [
43
- "claude",
44
- "mcp",
45
- "add",
46
- "fapony",
47
- "-s",
48
- "user",
49
- "--",
50
- "bun",
51
- join(INSTALL_ROOT, "fapony.ts"),
52
- "mcp",
53
- ];
12
+ export function cmdInstallClaude(
13
+ dryRun: boolean,
14
+ deps: InstallDeps = {},
15
+ ): void {
16
+ const dir = claudeSkillsDir(deps.homedir ?? homedir);
17
+ reportSkills(linkSkills(dir, dryRun), dir, dryRun);
18
+ removeRetiredClaudeHooks(dryRun, deps);
19
+ installClaudeHooks(dryRun, deps);
54
20
  }
55
21
 
56
- function isClaudeMissing(res: ClaudeRunResult): boolean {
22
+ /** Hook subcommands fapony used to register — memory hooks moved to fael,
23
+ * the edit hint was cut 2026-09-26 (never moved an agent to migrate). The
24
+ * commands are gone, so a leftover entry would error on every tool call. */
25
+ const RETIRED_SUBCOMMANDS = [
26
+ "hook-read-hint",
27
+ "hook-stop",
28
+ "hook-session-start",
29
+ "hook-edit-hint",
30
+ ];
31
+
32
+ function isRetired(command: unknown): boolean {
57
33
  return (
58
- res.exitCode === 127 ||
59
- /ENOENT|command not found|not found/i.test(res.stderr) ||
60
- /ENOENT|command not found|not found/i.test(res.stdout)
34
+ typeof command === "string" &&
35
+ command.includes("fapony") &&
36
+ RETIRED_SUBCOMMANDS.some((sub) => command.endsWith(` ${sub}`))
61
37
  );
62
38
  }
63
39
 
64
- /**
65
- * An existing `fapony` entry counts as ours when its Command:/Args: lines
66
- * mention fapony (covers both `bun <abs>/fapony.ts mcp` and `fapony mcp`
67
- * launchers). Only those lines are inspected — the `fapony:` header matches
68
- * trivially and proves nothing. Anything else under our name is someone
69
- * else's entry — never overwrite it silently.
70
- */
71
- export function claudeGetPointsToFapony(getOutput: string): boolean {
72
- const cmdLines = getOutput
73
- .split("\n")
74
- .map((l) => l.trim())
75
- .filter((l) => l.startsWith("Command:") || l.startsWith("Args:"));
76
- return cmdLines.join("\n").includes("fapony");
77
- }
78
-
79
- export function cmdInstallClaude(
80
- dryRun: boolean,
81
- deps: InstallDeps = {},
82
- ): void {
83
- const run = deps.run ?? defaultRun;
84
- const exitFn = deps.exit ?? defaultExit;
85
-
86
- const getArgs = claudeGetArgs();
87
- assertSafe(getArgs);
88
- const get = run(getArgs);
89
- if (isClaudeMissing(get)) {
90
- console.error(`claude CLI not found — install Claude Code first`);
91
- exitFn(1);
40
+ /** Drop fapony's own retired hook entries from settings.json; foreign entries
41
+ * are never touched. An unreadable settings.json is left alone. */
42
+ function removeRetiredClaudeHooks(dryRun: boolean, deps: InstallDeps): void {
43
+ const home = deps.homedir ? deps.homedir() : homedir();
44
+ const settingsPath = join(home, ".claude", "settings.json");
45
+ let settings: { hooks?: Record<string, unknown> };
46
+ try {
47
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
48
+ } catch {
92
49
  return;
93
50
  }
94
-
95
- if (get.exitCode === 0) {
96
- if (claudeGetPointsToFapony(`${get.stdout}\n${get.stderr}`)) {
97
- console.error(`✓ mcp.fapony already configured — no change needed`);
98
- console.error(` (Claude Code user scope)`);
99
- const dir = claudeSkillsDir(deps.homedir ?? homedir);
100
- reportSkills(linkSkills(dir, dryRun), dir, dryRun);
101
- // MCP is already wired, but a hook can be new since the last install
102
- // (e.g. the Edit hint) — always ensure the hook wiring, not only on a
103
- // fresh MCP add. This is the upgrade path for existing installs.
104
- installClaudeHooks(dryRun, deps);
51
+ const hooks = settings.hooks;
52
+ if (!hooks || typeof hooks !== "object") return;
53
+ let removed = 0;
54
+ for (const [event, list] of Object.entries(hooks)) {
55
+ if (!Array.isArray(list)) continue;
56
+ const kept = list.flatMap((group: { hooks?: { command?: unknown }[] }) => {
57
+ if (!Array.isArray(group?.hooks)) return [group];
58
+ const inner = group.hooks.filter((h) => !isRetired(h?.command));
59
+ removed += group.hooks.length - inner.length;
60
+ return inner.length === 0 ? [] : [{ ...group, hooks: inner }];
61
+ });
62
+ if (kept.length === 0) delete hooks[event];
63
+ else hooks[event] = kept;
64
+ }
65
+ if (removed === 0) return;
66
+ if (!dryRun) {
67
+ try {
68
+ writeFileSync(
69
+ settingsPath,
70
+ `${JSON.stringify(settings, null, 2)}\n`,
71
+ "utf-8",
72
+ );
73
+ } catch (e) {
74
+ console.error(
75
+ ` retired hooks: failed to write — ${(e as Error).message}`,
76
+ );
105
77
  return;
106
78
  }
107
- console.error(
108
- `an MCP server named "fapony" exists but points elsewhere — not overwriting.`,
109
- );
110
- console.error(` inspect with: claude mcp get fapony`);
111
- console.error(` then remove it first: claude mcp remove fapony -s user`);
112
- exitFn(1);
113
- return;
114
- }
115
-
116
- const addArgs = claudeAddArgs();
117
- if (dryRun) {
118
- console.error(`── dry-run: would run ──`);
119
- console.error(` ${addArgs.join(" ")}`);
120
- // Dry-run shows the hook wiring too — the installers are dry-run-safe
121
- // ("would write", no writes), so the preview stays truthful.
122
- installClaudeHooks(dryRun, deps);
123
- return;
124
- }
125
-
126
- assertSafe(addArgs);
127
- const add = run(addArgs);
128
- if (isClaudeMissing(add)) {
129
- console.error(`claude CLI not found — install Claude Code first`);
130
- exitFn(1);
131
- return;
132
79
  }
133
- if (add.exitCode !== 0) {
134
- console.error(
135
- `failed to add mcp.fapony to Claude Code (exit ${add.exitCode})`,
136
- );
137
- const detail = `${add.stdout}\n${add.stderr}`.trim();
138
- if (detail) console.error(detail);
139
- console.error(`verify with: claude mcp add --help`);
140
- exitFn(1);
141
- return;
142
- }
143
- console.error(`✓ mcp.fapony configured for Claude Code (user scope)`);
144
- const skillsDir = claudeSkillsDir(deps.homedir ?? homedir);
145
- reportSkills(linkSkills(skillsDir, dryRun), skillsDir, dryRun);
146
-
147
- // Wire the Stop hook that refuses to end a turn with ungraded commits,
148
- // and the Read/Edit hints that annotate reads of large files and edits to
149
- // files with importers (both annotate-only).
150
- installClaudeHooks(dryRun, deps);
80
+ console.error(
81
+ ` retired hooks: ${dryRun ? "would remove" : "removed"} ${removed} fapony entr${removed === 1 ? "y" : "ies"}`,
82
+ );
151
83
  }
152
84
 
153
85
  /**
154
- * Shared append-to-settings.json hook installer. Both fapony hooks live
155
- * here now (Stop since PLAN-mem-mcp, PreToolUse read hint since the
156
- * large-file annotate feature) — the read/write/idempotence/append shape
157
- * is one implementation with two callers, not a scaffold.
86
+ * Append-to-settings.json hook installer (caller: plan-mv guard).
158
87
  * Never touch a hook someone else registered, never fail the install over it.
159
88
  */
160
89
  function ensureClaudeHook(
@@ -222,64 +151,14 @@ function ensureClaudeHook(
222
151
  );
223
152
  }
224
153
 
225
- /**
226
- * Wire every hook fapony owns: the Stop hook that refuses to end a turn with
227
- * ungraded commits, and the Read/Edit PreToolUse hints (annotate-only).
228
- * Idempotent and dry-run-safe. Called on every install, not just a fresh MCP
229
- * add — an existing install must still pick up a hook added later.
230
- */
154
+ /** Wire every hook fapony owns — the plan-mv guard. Idempotent and dry-run-safe. */
231
155
  function installClaudeHooks(dryRun: boolean, deps: InstallDeps): void {
232
- installStopHook(dryRun, deps);
233
- installReadHintHook(dryRun, deps);
234
- installEditHintHook(dryRun, deps);
235
156
  installMvGuardHook(dryRun, deps);
236
- installSessionStartHook(dryRun, deps);
237
- }
238
-
239
- function installStopHook(dryRun: boolean, deps: InstallDeps): void {
240
- ensureClaudeHook(dryRun, deps, {
241
- event: "Stop",
242
- subcommand: "hook-stop",
243
- label: "stop hook",
244
- });
245
- }
246
-
247
- /**
248
- * PreToolUse hook on Read: annotates a full-file read with one factual line —
249
- * the size + the review-seed command when the file is large, and the re-read
250
- * line when the same path was already read this session and its mtime has not
251
- * moved (an unchanged file, so the second read buys nothing; a changed one
252
- * stays silent). Annotate only — no permissionDecision is ever returned, the
253
- * read always proceeds. The matcher "Read" keeps the spawn off every other
254
- * tool call.
255
- */
256
- function installReadHintHook(dryRun: boolean, deps: InstallDeps): void {
257
- ensureClaudeHook(dryRun, deps, {
258
- event: "PreToolUse",
259
- matcher: "Read",
260
- subcommand: "hook-read-hint",
261
- label: "read hint",
262
- });
263
- }
264
-
265
- /**
266
- * PreToolUse hook on Edit: annotates an edit with the file's importer count
267
- * plus the review-seed command that lists them, once per (session, file).
268
- * Annotate only — no permissionDecision is ever returned, the edit always
269
- * proceeds. The matcher "Edit" keeps the spawn off every other tool call.
270
- */
271
- function installEditHintHook(dryRun: boolean, deps: InstallDeps): void {
272
- ensureClaudeHook(dryRun, deps, {
273
- event: "PreToolUse",
274
- matcher: "Edit",
275
- subcommand: "hook-edit-hint",
276
- label: "edit hint",
277
- });
278
157
  }
279
158
 
280
159
  /**
281
160
  * PreToolUse hook on Bash: denies a raw `git mv` of a plan file into a done/
282
- * directory, pointing at `fapony mem plan-sweep --apply` instead — that
161
+ * directory, pointing at `fapony plan sweep --apply` instead — that
283
162
  * command does the link rewrite a plain `git mv` skips. The one fapony hook
284
163
  * that blocks besides Stop; matcher "Bash" keeps the spawn off every other
285
164
  * tool call.
@@ -292,16 +171,3 @@ function installMvGuardHook(dryRun: boolean, deps: InstallDeps): void {
292
171
  label: "plan-mv guard",
293
172
  });
294
173
  }
295
-
296
- /**
297
- * SessionStart hook: injects `fapony mem kickoff` as context when the repo has
298
- * a mem log, and stays silent when it does not. Context only — SessionStart
299
- * cannot block, and a repo without mem never sees a line.
300
- */
301
- function installSessionStartHook(dryRun: boolean, deps: InstallDeps): void {
302
- ensureClaudeHook(dryRun, deps, {
303
- event: "SessionStart",
304
- subcommand: "hook-session-start",
305
- label: "session start",
306
- });
307
- }
@@ -1,192 +1,28 @@
1
1
  // src/install/codex.ts — Codex install provider
2
2
  //
3
- // Reads/writes ~/.codex/config.toml directly for MCP config.
4
- // Reads/writes ~/.codex/hooks.json for lifecycle hooks (Stop, SessionStart).
5
- // Symlinks skills into ~/.agents/skills/ (same dir as ZCode).
3
+ // Symlinks skills into ~/.agents/skills/ (same dir as ZCode). Memory (MCP,
4
+ // Stop, SessionStart) moved to fael — `fael install` wires it.
6
5
 
7
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { existsSync } from "node:fs";
8
7
  import { homedir } from "node:os";
9
8
  import { join } from "node:path";
10
9
  import { agentsSkillsDir, linkSkills, reportSkills } from "./skills.js";
11
- import {
12
- CODEX_MCP_ENTRY,
13
- defaultExit,
14
- INSTALL_ROOT,
15
- type InstallDeps,
16
- } from "./types.js";
10
+ import { defaultExit, type InstallDeps } from "./types.js";
17
11
 
18
12
  export function findCodexConfig(getHome: () => string): string | null {
19
13
  const p = join(getHome(), ".codex", "config.toml");
20
14
  return existsSync(p) ? p : null;
21
15
  }
22
16
 
23
- export function findCodexHooksJson(getHome: () => string): string | null {
24
- const p = join(getHome(), ".codex", "hooks.json");
25
- return existsSync(p) ? p : null;
26
- }
27
-
28
- function isCodexConfigured(content: string): boolean {
29
- // Check if [mcp_servers.fapony] section exists with our command
30
- const sectionRegex = /\[mcp_servers\.fapony\]/;
31
- if (!sectionRegex.test(content)) return false;
32
- // Verify it points to fapony
33
- return content.includes("fapony.ts") && content.includes("mcp");
34
- }
35
-
36
- function readJsonObject(path: string): Record<string, unknown> | null {
37
- try {
38
- const parsed: unknown = JSON.parse(readFileSync(path, "utf-8"));
39
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
40
- return null;
41
- }
42
- return parsed as Record<string, unknown>;
43
- } catch {
44
- return null;
45
- }
46
- }
47
-
48
- /**
49
- * Append a fapony command hook to one event group in ~/.codex/hooks.json.
50
- * Never replaces other hook groups or foreign entries within the group.
51
- * A fapony-owned entry is identified by the subcommand string
52
- * ("hook-stop" / "hook-session-start") inside its command — per-group
53
- * idempotence, so an existing install with only Stop still picks up
54
- * SessionStart on the next run.
55
- */
56
- function installCodexHookGroup(
57
- dryRun: boolean,
58
- getHome: () => string,
59
- opts: { event: "Stop" | "SessionStart"; subcommand: string; label: string },
60
- ): void {
61
- const hooksPath = join(getHome(), ".codex", "hooks.json");
62
- let config: Record<string, unknown> = {};
63
- if (existsSync(hooksPath)) {
64
- const parsed = readJsonObject(hooksPath);
65
- if (!parsed) {
66
- console.error(
67
- ` ${opts.label}: ${hooksPath} is unreadable or malformed — skipping`,
68
- );
69
- return;
70
- }
71
- config = parsed;
72
- }
73
-
74
- const hookMap = config.hooks;
75
- if (
76
- hookMap !== undefined &&
77
- (typeof hookMap !== "object" || Array.isArray(hookMap))
78
- ) {
79
- console.error(
80
- ` ${opts.label}: ${hooksPath} has an unexpected "hooks" shape — skipping`,
81
- );
82
- return;
83
- }
84
-
85
- const map = (hookMap ?? {}) as Record<string, unknown>;
86
- // Codex event arrays: each element is { matcher?, hooks: [...] }
87
- const group = Array.isArray(map[opts.event])
88
- ? (map[opts.event] as unknown[])
89
- : [];
90
-
91
- // Check if the fapony hook is already present in this group (by subcommand)
92
- const serialized = JSON.stringify(group);
93
- if (serialized.includes(opts.subcommand)) {
94
- console.error(
95
- ` ${opts.label}: already configured in hooks.json — no change`,
96
- );
97
- return;
98
- }
99
-
100
- const command = `bun ${join(INSTALL_ROOT, "fapony.ts")} ${opts.subcommand}`;
101
- const faponyEntry = { hooks: [{ type: "command", command }] };
102
- const after = {
103
- ...config,
104
- hooks: { ...map, [opts.event]: [...group, faponyEntry] },
105
- };
106
-
107
- if (!dryRun) {
108
- try {
109
- writeFileSync(hooksPath, `${JSON.stringify(after, null, 2)}\n`, "utf-8");
110
- } catch (e) {
111
- console.error(
112
- ` ${opts.label}: failed to write — ${(e as Error).message}`,
113
- );
114
- return;
115
- }
116
- }
117
- console.error(
118
- ` ${opts.label}: ${dryRun ? "would write" : "wrote"} hooks.${opts.event} → ${hooksPath}`,
119
- );
120
- console.error(
121
- ` review and trust via Codex /hooks before the hook will run`,
122
- );
123
- }
124
-
125
- function installStopHook(dryRun: boolean, getHome: () => string): void {
126
- installCodexHookGroup(dryRun, getHome, {
127
- event: "Stop",
128
- subcommand: "hook-stop",
129
- label: "stop hook",
130
- });
131
- }
132
-
133
- /**
134
- * SessionStart hook: injects `fapony mem kickoff` as context when the repo
135
- * has a mem log, silent when it does not (cmdHookSessionStart exits quiet).
136
- * Same `hookSpecificOutput.additionalContext` contract Claude uses — Codex
137
- * SessionStart reads that channel too. No matcher: match-all fires on every
138
- * source including `clear` and on versions that send no source at all, where
139
- * a `startup|resume` matcher would silently never fire; kickoff is capped at
140
- * 4k and costs one cheap spawn, so the /clear path stays fast.
141
- */
142
- function installSessionStartHook(dryRun: boolean, getHome: () => string): void {
143
- installCodexHookGroup(dryRun, getHome, {
144
- event: "SessionStart",
145
- subcommand: "hook-session-start",
146
- label: "session start",
147
- });
148
- }
149
-
150
17
  export function cmdInstallCodex(dryRun: boolean, deps: InstallDeps = {}): void {
151
- const exitFn = deps.exit ?? defaultExit;
152
18
  const getHome = deps.homedir ?? homedir;
153
- const configPath = findCodexConfig(getHome);
154
-
155
- if (!configPath) {
19
+ if (!findCodexConfig(getHome)) {
156
20
  console.error(
157
21
  `Codex config not found — open Codex at least once to create ~/.codex/config.toml`,
158
22
  );
159
- exitFn(1);
160
- return;
161
- }
162
-
163
- let content: string;
164
- try {
165
- content = readFileSync(configPath, "utf-8");
166
- } catch (e) {
167
- console.error(`failed to read ${configPath}: ${(e as Error).message}`);
168
- exitFn(1);
23
+ (deps.exit ?? defaultExit)(1);
169
24
  return;
170
25
  }
171
-
172
- if (isCodexConfigured(content)) {
173
- console.error(`✓ mcp_servers.fapony already configured — no change needed`);
174
- console.error(` (${configPath})`);
175
- } else if (dryRun) {
176
- console.error(`── dry-run: would append to ${configPath} ──`);
177
- console.log(CODEX_MCP_ENTRY);
178
- } else {
179
- const newContent = `${content.trimEnd()}\n\n${CODEX_MCP_ENTRY}`;
180
- writeFileSync(configPath, newContent);
181
- console.error(`✓ added mcp_servers.fapony to ${configPath}`);
182
- console.error(` restart Codex to load the MCP server`);
183
- }
184
-
185
- // --- Stop + SessionStart hooks (~/.codex/hooks.json) ---
186
- installStopHook(dryRun, getHome);
187
- installSessionStartHook(dryRun, getHome);
188
-
189
- // --- Skills (~/.agents/skills/) ---
190
26
  const skillsDir = agentsSkillsDir(getHome);
191
27
  reportSkills(linkSkills(skillsDir, dryRun), skillsDir, dryRun);
192
28
  }
@@ -1,9 +1,8 @@
1
- // src/install/detect.ts — detect which MCP clients are installed on this machine.
1
+ // src/install/detect.ts — detect which agent clients are installed on this machine.
2
2
  //
3
3
  // Signal per client:
4
4
  // antigravity = ~/.gemini exists (the app creates it on first run) OR `agy` on PATH
5
5
  // claude = `command -v claude` (CLI on PATH)
6
- // cursor = ~/.cursor exists (the app creates it on first run)
7
6
  // opencode = ~/.config/opencode/{opencode.json,opencode.jsonc} exists
8
7
  // zcode = ~/.zcode/cli/config.json or ~/.agents/mcp.json exists
9
8
  // codex = ~/.codex/config.toml exists
@@ -12,7 +11,6 @@ import { homedir } from "node:os";
12
11
  import { defaultCheckCmd } from "../setup.js";
13
12
  import { findGeminiDir } from "./antigravity.js";
14
13
  import { findCodexConfig } from "./codex.js";
15
- import { findCursorDir } from "./cursor.js";
16
14
  import { findOpencodeConfig } from "./opencode.js";
17
15
  import type { InstallDeps } from "./types.js";
18
16
  import { findZcodeConfig } from "./zcode.js";
@@ -24,11 +22,11 @@ export interface DetectedClient {
24
22
  }
25
23
 
26
24
  /**
27
- * Detect which MCP clients are available on this machine.
28
- * Returns one entry per platform, ordered: antigravity, claude, cursor, opencode, zcode, codex.
25
+ * Detect which agent clients are available on this machine.
26
+ * Returns one entry per platform, ordered: antigravity, claude, opencode, zcode, codex.
29
27
  *
30
28
  * Uses resolvers from each provider (file/dir-exists check) for
31
- * antigravity/cursor/opencode/zcode/codex, and `command -v claude`/`command -v agy`
29
+ * antigravity/opencode/zcode/codex, and `command -v claude`/`command -v agy`
32
30
  * for claude/antigravity — all through injected deps for testability.
33
31
  */
34
32
  export function detectClients(deps: InstallDeps = {}): DetectedClient[] {
@@ -39,7 +37,6 @@ export function detectClients(deps: InstallDeps = {}): DetectedClient[] {
39
37
  const agy = checkCmd("agy");
40
38
 
41
39
  const geminiDir = findGeminiDir(getHome);
42
- const cursorDir = findCursorDir(getHome);
43
40
  const opencodePath = findOpencodeConfig(getHome);
44
41
  const zcodeResult = findZcodeConfig(getHome);
45
42
  const codexPath = findCodexConfig(getHome);
@@ -59,13 +56,6 @@ export function detectClients(deps: InstallDeps = {}): DetectedClient[] {
59
56
  installed: claude,
60
57
  why: claude ? "claude CLI on PATH" : "claude CLI not found on PATH",
61
58
  },
62
- {
63
- platform: "cursor",
64
- installed: cursorDir !== null,
65
- why: cursorDir
66
- ? `dir at ${cursorDir}`
67
- : "no ~/.cursor directory (open Cursor once)",
68
- },
69
59
  {
70
60
  platform: "opencode",
71
61
  installed: opencodePath !== null,