portable-agent-layer 0.1.0

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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/assets/agents/claude-researcher.md +43 -0
  4. package/assets/agents/investigative-researcher.md +44 -0
  5. package/assets/agents/multi-perspective-researcher.md +43 -0
  6. package/assets/skills/analyze-pdf.md +40 -0
  7. package/assets/skills/analyze-youtube.md +35 -0
  8. package/assets/skills/council.md +43 -0
  9. package/assets/skills/create-skill.md +31 -0
  10. package/assets/skills/extract-entities.md +63 -0
  11. package/assets/skills/extract-wisdom.md +18 -0
  12. package/assets/skills/first-principles.md +17 -0
  13. package/assets/skills/fyzz-chat-api.md +43 -0
  14. package/assets/skills/reflect.md +87 -0
  15. package/assets/skills/research.md +68 -0
  16. package/assets/skills/review.md +19 -0
  17. package/assets/skills/summarize.md +15 -0
  18. package/assets/templates/AGENTS.md.template +45 -0
  19. package/assets/templates/telos/BELIEFS.md +4 -0
  20. package/assets/templates/telos/CHALLENGES.md +4 -0
  21. package/assets/templates/telos/GOALS.md +12 -0
  22. package/assets/templates/telos/IDEAS.md +4 -0
  23. package/assets/templates/telos/IDENTITY.md +4 -0
  24. package/assets/templates/telos/LEARNED.md +4 -0
  25. package/assets/templates/telos/MISSION.md +4 -0
  26. package/assets/templates/telos/MODELS.md +4 -0
  27. package/assets/templates/telos/NARRATIVES.md +4 -0
  28. package/assets/templates/telos/PROJECTS.md +7 -0
  29. package/assets/templates/telos/STRATEGIES.md +4 -0
  30. package/bin/pal +24 -0
  31. package/bin/pal.bat +8 -0
  32. package/bin/pal.ps1 +30 -0
  33. package/package.json +82 -0
  34. package/src/cli/index.ts +344 -0
  35. package/src/cli/install.ts +86 -0
  36. package/src/cli/uninstall.ts +45 -0
  37. package/src/hooks/LoadContext.ts +41 -0
  38. package/src/hooks/SecurityValidator.ts +52 -0
  39. package/src/hooks/SkillGuard.ts +41 -0
  40. package/src/hooks/StopOrchestrator.ts +35 -0
  41. package/src/hooks/UserPromptOrchestrator.ts +35 -0
  42. package/src/hooks/handlers/backup.ts +41 -0
  43. package/src/hooks/handlers/failure.ts +136 -0
  44. package/src/hooks/handlers/rating.ts +409 -0
  45. package/src/hooks/handlers/relationship.ts +113 -0
  46. package/src/hooks/handlers/session-name.ts +121 -0
  47. package/src/hooks/handlers/synthesis.ts +109 -0
  48. package/src/hooks/handlers/tab.ts +8 -0
  49. package/src/hooks/handlers/update-counts.ts +151 -0
  50. package/src/hooks/handlers/work-learning.ts +183 -0
  51. package/src/hooks/handlers/work-session.ts +58 -0
  52. package/src/hooks/lib/claude-md.ts +121 -0
  53. package/src/hooks/lib/context.ts +433 -0
  54. package/src/hooks/lib/entities.ts +304 -0
  55. package/src/hooks/lib/export.ts +76 -0
  56. package/src/hooks/lib/inference.ts +91 -0
  57. package/src/hooks/lib/learning-category.ts +14 -0
  58. package/src/hooks/lib/log.ts +53 -0
  59. package/src/hooks/lib/models.ts +16 -0
  60. package/src/hooks/lib/paths.ts +80 -0
  61. package/src/hooks/lib/relationship.ts +135 -0
  62. package/src/hooks/lib/security.ts +122 -0
  63. package/src/hooks/lib/session-names.ts +247 -0
  64. package/src/hooks/lib/setup.ts +189 -0
  65. package/src/hooks/lib/signal-trends.ts +117 -0
  66. package/src/hooks/lib/signals.ts +37 -0
  67. package/src/hooks/lib/stdin.ts +18 -0
  68. package/src/hooks/lib/stop.ts +155 -0
  69. package/src/hooks/lib/time.ts +19 -0
  70. package/src/hooks/lib/token-usage.ts +42 -0
  71. package/src/hooks/lib/transcript.ts +76 -0
  72. package/src/hooks/lib/wisdom.ts +48 -0
  73. package/src/hooks/lib/work-tracking.ts +193 -0
  74. package/src/hooks/setup-check.ts +42 -0
  75. package/src/targets/claude/install.ts +145 -0
  76. package/src/targets/claude/uninstall.ts +101 -0
  77. package/src/targets/lib.ts +337 -0
  78. package/src/targets/opencode/install.ts +59 -0
  79. package/src/targets/opencode/plugin.ts +328 -0
  80. package/src/targets/opencode/uninstall.ts +57 -0
  81. package/src/tools/entity-save.ts +110 -0
  82. package/src/tools/export.ts +34 -0
  83. package/src/tools/fyzz-api.ts +104 -0
  84. package/src/tools/import.ts +123 -0
  85. package/src/tools/pattern-synthesis.ts +435 -0
  86. package/src/tools/pdf-download.ts +102 -0
  87. package/src/tools/relationship-reflect.ts +362 -0
  88. package/src/tools/session-summary.ts +206 -0
  89. package/src/tools/token-cost.ts +301 -0
  90. package/src/tools/youtube-analyze.ts +105 -0
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Structured work tracking: session history + persistent projects.
3
+ * Replaces the single-file current-work.json approach.
4
+ */
5
+
6
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { resolve } from "node:path";
8
+ import { ensureDir, paths } from "./paths";
9
+ import { now } from "./time";
10
+
11
+ // ── Session Records ──────────────────────────────────────────────
12
+
13
+ export interface SessionRecord {
14
+ sessionId: string;
15
+ name: string;
16
+ ts: string;
17
+ cwd: string;
18
+ status: "completed" | "in-progress";
19
+ summary: string;
20
+ artifacts: string[];
21
+ handoff: string;
22
+ messageCount: number;
23
+ projectId?: string;
24
+ }
25
+
26
+ const MAX_SESSIONS = 50;
27
+ const MAX_ARTIFACTS = 20;
28
+
29
+ function sessionsPath(): string {
30
+ return resolve(ensureDir(paths.state()), "sessions.json");
31
+ }
32
+
33
+ export function readSessions(): SessionRecord[] {
34
+ const p = sessionsPath();
35
+ if (!existsSync(p)) return [];
36
+ try {
37
+ const data = JSON.parse(readFileSync(p, "utf-8"));
38
+ return Array.isArray(data) ? data : [];
39
+ } catch {
40
+ return [];
41
+ }
42
+ }
43
+
44
+ export function writeSession(record: SessionRecord): void {
45
+ const sessions = readSessions();
46
+ // Replace existing record with same sessionId, or append
47
+ const idx = sessions.findIndex((s) => s.sessionId === record.sessionId);
48
+ if (idx >= 0) {
49
+ sessions[idx] = record;
50
+ } else {
51
+ sessions.push(record);
52
+ }
53
+ // Prune to last N
54
+ const pruned = sessions.slice(-MAX_SESSIONS);
55
+ writeFileSync(sessionsPath(), JSON.stringify(pruned, null, 2), "utf-8");
56
+ }
57
+
58
+ /** Filter sessions within the last N hours */
59
+ export function recentSessions(hours: number): SessionRecord[] {
60
+ const cutoff = Date.now() - hours * 60 * 60 * 1000;
61
+ return readSessions().filter((s) => new Date(s.ts).getTime() > cutoff);
62
+ }
63
+
64
+ /** Detect session completion status from last assistant message */
65
+ export function detectStatus(lastAssistant: string): SessionRecord["status"] {
66
+ const completionSignals =
67
+ /\b(done|all set|let me know|ready to|complete|finished|that's it|looks good|should be good|merged|shipped|deployed)\b/i;
68
+ return completionSignals.test(lastAssistant) ? "completed" : "in-progress";
69
+ }
70
+
71
+ /** Extract file paths mentioned in assistant messages */
72
+ export function extractArtifacts(
73
+ messages: { role: string; content: string | unknown }[]
74
+ ): string[] {
75
+ const seen = new Set<string>();
76
+ const artifacts: string[] = [];
77
+
78
+ for (const msg of messages) {
79
+ if (msg.role !== "assistant") continue;
80
+ const text = typeof msg.content === "string" ? msg.content : "";
81
+ // Match file paths: /absolute/paths and relative/paths with extensions
82
+ const pathMatches = text.match(/(?:\/[\w./-]+\.[\w]+|[\w./-]+\/[\w.-]+\.[\w]+)/g);
83
+ if (!pathMatches) continue;
84
+ for (const p of pathMatches) {
85
+ // Skip URLs, common noise
86
+ if (p.includes("://") || p.includes("node_modules")) continue;
87
+ if (!seen.has(p)) {
88
+ seen.add(p);
89
+ artifacts.push(p);
90
+ }
91
+ if (artifacts.length >= MAX_ARTIFACTS) return artifacts;
92
+ }
93
+ }
94
+
95
+ return artifacts;
96
+ }
97
+
98
+ /** Strip code blocks, paths, and technical noise from text */
99
+ function cleanForHandoff(text: string): string {
100
+ return (
101
+ text
102
+ // Remove fenced code blocks
103
+ .replace(/```[\s\S]*?```/g, "")
104
+ // Remove inline code
105
+ .replace(/`[^`]+`/g, "")
106
+ // Remove file paths
107
+ .replace(/(?:\/[\w./-]+\.[\w]+)/g, "")
108
+ // Remove markdown formatting
109
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
110
+ .replace(/^[#]+\s*/gm, "")
111
+ // Remove tool call artifacts
112
+ .replace(/^\s*[-*]\s*`[^`]+`.*$/gm, "")
113
+ // Collapse whitespace
114
+ .replace(/\n{3,}/g, "\n\n")
115
+ .trim()
116
+ );
117
+ }
118
+
119
+ /** Extract handoff notes from last assistant message */
120
+ export function extractHandoff(lastAssistant: string): string {
121
+ // Look for explicit next-steps / TODO / remaining sections
122
+ const sectionMatch = lastAssistant.match(
123
+ /(?:next steps?|todo|remaining|what's left|still need|want me to)[:\s]*\n([\s\S]{10,300}?)(?:\n\n|\n(?=[A-Z#]))/i
124
+ );
125
+ if (sectionMatch) return cleanForHandoff(sectionMatch[1]);
126
+
127
+ // Look for closing question/offer (common assistant pattern)
128
+ const closingMatch = lastAssistant.match(
129
+ /(?:want (?:me to|to)|shall I|should I|ready to|anything else|let me know)[^\n]*$/im
130
+ );
131
+
132
+ const cleaned = cleanForHandoff(lastAssistant);
133
+
134
+ // Use last meaningful paragraph
135
+ const paragraphs = cleaned
136
+ .split(/\n\n+/)
137
+ .map((p) => p.trim())
138
+ .filter((p) => p.length >= 15 && p.length <= 300);
139
+
140
+ if (closingMatch) return closingMatch[0].trim();
141
+ if (paragraphs.length > 0) return paragraphs[paragraphs.length - 1];
142
+ if (cleaned.length > 200) return cleaned.slice(-200).trim();
143
+ return cleaned;
144
+ }
145
+
146
+ // ── Persistent Projects ──────────────────────────────────────────
147
+
148
+ export interface Project {
149
+ id: string;
150
+ name: string;
151
+ created: string;
152
+ updated: string;
153
+ status: "active" | "paused" | "completed";
154
+ objectives: string[];
155
+ decisions: string[];
156
+ completed: string[];
157
+ blockers: string[];
158
+ nextSteps: string[];
159
+ handoff: string;
160
+ sessions: string[];
161
+ }
162
+
163
+ function projectsPath(): string {
164
+ return resolve(ensureDir(paths.state()), "projects.json");
165
+ }
166
+
167
+ export function readProjects(): Record<string, Project> {
168
+ const p = projectsPath();
169
+ if (!existsSync(p)) return {};
170
+ try {
171
+ return JSON.parse(readFileSync(p, "utf-8"));
172
+ } catch {
173
+ return {};
174
+ }
175
+ }
176
+
177
+ export function writeProject(project: Project): void {
178
+ const projects = readProjects();
179
+ project.updated = now();
180
+ projects[project.id] = project;
181
+ writeFileSync(projectsPath(), JSON.stringify(projects, null, 2), "utf-8");
182
+ }
183
+
184
+ export function activeProjects(): Project[] {
185
+ return Object.values(readProjects()).filter((p) => p.status === "active");
186
+ }
187
+
188
+ export function staleProjects(days = 7): Project[] {
189
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
190
+ return Object.values(readProjects()).filter(
191
+ (p) => p.status === "active" && new Date(p.updated).getTime() < cutoff
192
+ );
193
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * CLI helper: check setup state and output results.
3
+ *
4
+ * Usage:
5
+ * bun run hooks/setup-check.ts status → "complete" or "incomplete"
6
+ * bun run hooks/setup-check.ts init → create setup.json if missing
7
+ * bun run hooks/setup-check.ts prompt → output setup prompt (if incomplete)
8
+ *
9
+ * Used by shell scripts to avoid duplicating setup logic.
10
+ */
11
+
12
+ import {
13
+ buildSetupPrompt,
14
+ ensureSetupState,
15
+ isSetupComplete,
16
+ readSetupState,
17
+ } from "./lib/setup";
18
+
19
+ const command = process.argv[2] ?? "status";
20
+
21
+ switch (command) {
22
+ case "status": {
23
+ const state = readSetupState();
24
+ console.log(state && isSetupComplete(state) ? "complete" : "incomplete");
25
+ break;
26
+ }
27
+ case "init": {
28
+ ensureSetupState();
29
+ break;
30
+ }
31
+ case "prompt": {
32
+ const state = readSetupState();
33
+ if (state) {
34
+ const prompt = buildSetupPrompt(state);
35
+ if (prompt) console.log(prompt);
36
+ }
37
+ break;
38
+ }
39
+ default:
40
+ console.error(`Unknown command: ${command}`);
41
+ process.exit(1);
42
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * PAL — Claude Code target installer (TypeScript)
3
+ * Merges hooks into existing settings.json (never overwrites).
4
+ * Copies skills additively. Generates CLAUDE.md from TELOS.
5
+ */
6
+
7
+ import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
10
+ import { palHome, palPkg, platform } from "../../hooks/lib/paths";
11
+ import {
12
+ copyAgents,
13
+ copySkills,
14
+ countAgents,
15
+ countMd,
16
+ countSkills,
17
+ log,
18
+ readJson,
19
+ writeJson,
20
+ } from "../lib";
21
+
22
+ const PKG_ROOT = palPkg().replaceAll("\\", "/");
23
+ const CLAUDE_DIR = platform.claudeDir();
24
+ const SETTINGS = resolve(CLAUDE_DIR, "settings.json");
25
+
26
+ // --- Ensure settings.json exists ---
27
+ mkdirSync(CLAUDE_DIR, { recursive: true });
28
+ if (!existsSync(SETTINGS)) {
29
+ writeFileSync(SETTINGS, "{}\n", "utf-8");
30
+ log.info("Created new settings.json");
31
+ }
32
+
33
+ // --- Backup ---
34
+ const backup = `${SETTINGS}.bak.${Date.now()}`;
35
+ copyFileSync(SETTINGS, backup);
36
+ log.info("Backed up settings.json");
37
+
38
+ // --- Build hooks payload ---
39
+ const hooksPayload = {
40
+ hooks: {
41
+ SessionStart: [
42
+ {
43
+ matcher: "",
44
+ hooks: [
45
+ { type: "command", command: `bun run ${PKG_ROOT}/src/hooks/LoadContext.ts` },
46
+ ],
47
+ },
48
+ ],
49
+ UserPromptSubmit: [
50
+ {
51
+ matcher: "",
52
+ hooks: [
53
+ {
54
+ type: "command",
55
+ command: `bun run ${PKG_ROOT}/src/hooks/UserPromptOrchestrator.ts`,
56
+ },
57
+ ],
58
+ },
59
+ ],
60
+ PreToolUse: [
61
+ {
62
+ matcher: "Bash|Write|Edit",
63
+ hooks: [
64
+ {
65
+ type: "command",
66
+ command: `bun run ${PKG_ROOT}/src/hooks/SecurityValidator.ts`,
67
+ },
68
+ ],
69
+ },
70
+ {
71
+ matcher: "Skill",
72
+ hooks: [
73
+ { type: "command", command: `bun run ${PKG_ROOT}/src/hooks/SkillGuard.ts` },
74
+ ],
75
+ },
76
+ ],
77
+ Stop: [
78
+ {
79
+ matcher: "",
80
+ hooks: [
81
+ {
82
+ type: "command",
83
+ command: `bun run ${PKG_ROOT}/src/hooks/StopOrchestrator.ts`,
84
+ },
85
+ ],
86
+ },
87
+ ],
88
+ },
89
+ };
90
+
91
+ // --- Merge hooks additively (deduplicate by command) ---
92
+ type HookEntry = { matcher: string; hooks: Array<{ type: string; command: string }> };
93
+ type Settings = { hooks?: Record<string, HookEntry[]>; env?: Record<string, string> };
94
+
95
+ const settings = readJson<Settings>(SETTINGS, {});
96
+ if (!settings.hooks) settings.hooks = {};
97
+
98
+ for (const [event, entries] of Object.entries(hooksPayload.hooks)) {
99
+ const existing = settings.hooks[event] ?? [];
100
+ for (const entry of entries) {
101
+ const cmd = entry.hooks[0]?.command;
102
+ const alreadyPresent = existing.some((e) => e.hooks?.[0]?.command === cmd);
103
+ if (!alreadyPresent) existing.push(entry);
104
+ }
105
+ settings.hooks[event] = existing;
106
+ }
107
+
108
+ // --- Add PAL tool permissions (auto-allow ai: scripts) ---
109
+ type SettingsWithPermissions = Settings & { permissions?: { allow?: string[] } };
110
+ const s = settings as SettingsWithPermissions;
111
+ if (!s.permissions) s.permissions = {};
112
+ if (!s.permissions.allow) s.permissions.allow = [];
113
+ const aiTools = [
114
+ "ai:entity-save",
115
+ "ai:fyzz-api",
116
+ "ai:pdf-download",
117
+ "ai:youtube-analyze",
118
+ ];
119
+ for (const tool of aiTools) {
120
+ const perm = `Bash(bun run ${tool} *)`;
121
+ if (!s.permissions.allow.includes(perm)) {
122
+ s.permissions.allow.push(perm);
123
+ }
124
+ }
125
+
126
+ writeJson(SETTINGS, settings);
127
+ log.success("Merged hooks into settings.json");
128
+
129
+ // --- Copy skills ---
130
+ const skillsDir = resolve(CLAUDE_DIR, "skills");
131
+ copySkills(skillsDir);
132
+
133
+ // --- Copy agents ---
134
+ copyAgents();
135
+
136
+ // --- Generate ~/.claude/AGENTS.md and symlink ~/.claude/CLAUDE.md → AGENTS.md ---
137
+ regenerateIfNeeded();
138
+ log.success("Generated ~/.config/opencode/AGENTS.md (→ ~/.claude/CLAUDE.md symlink)");
139
+
140
+ log.success("Claude Code installation complete");
141
+ console.log("");
142
+ log.info(`Hooks: 5 (SessionStart, UserPromptSubmit, PreToolUse×2, Stop)`);
143
+ log.info(`Skills: ${countSkills()}`);
144
+ log.info(`Agents: ${countAgents()}`);
145
+ log.info(`TELOS: ${countMd(resolve(palHome(), "telos"))} files`);
@@ -0,0 +1,101 @@
1
+ /**
2
+ * PAL — Claude Code uninstaller (TypeScript)
3
+ * Removes PAL hooks, skills, and env from settings.json.
4
+ */
5
+
6
+ import { copyFileSync, existsSync, unlinkSync } from "node:fs";
7
+ import { resolve } from "node:path";
8
+ import { palPkg, platform } from "../../hooks/lib/paths";
9
+ import { log, readJson, removeAgents, removeSkills, writeJson } from "../lib";
10
+
11
+ const PKG_ROOT = palPkg();
12
+ const CLAUDE_DIR = platform.claudeDir();
13
+ const SETTINGS = resolve(CLAUDE_DIR, "settings.json");
14
+
15
+ if (!existsSync(SETTINGS)) {
16
+ log.info("No settings.json found, nothing to do.");
17
+ process.exit(0);
18
+ }
19
+
20
+ // --- Backup ---
21
+ copyFileSync(SETTINGS, `${SETTINGS}.bak.${Date.now()}`);
22
+ log.info("Backed up settings.json");
23
+
24
+ // --- Remove PAL hooks ---
25
+ type HookEntry = {
26
+ matcher?: string;
27
+ hooks?: Array<{ command?: string }>;
28
+ command?: string;
29
+ };
30
+ type Settings = { hooks?: Record<string, HookEntry[]>; env?: Record<string, string> };
31
+
32
+ const settings = readJson<Settings>(SETTINGS, {});
33
+
34
+ if (settings.hooks) {
35
+ for (const [event, entries] of Object.entries(settings.hooks)) {
36
+ settings.hooks[event] = entries.filter((entry) => {
37
+ // New format: { matcher, hooks: [{ command }] }
38
+ if (entry.hooks) return !entry.hooks.some((h) => h.command?.includes(PKG_ROOT));
39
+ // Old flat format: { type, command }
40
+ if (entry.command) return !entry.command.includes(PKG_ROOT);
41
+ return true;
42
+ });
43
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
44
+ }
45
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
46
+ }
47
+
48
+ // --- Remove env ---
49
+ if (settings.env) {
50
+ // Clean up env vars
51
+ delete settings.env.PAL_DIR;
52
+ if (Object.keys(settings.env).length === 0) delete settings.env;
53
+ }
54
+
55
+ // --- Remove PAL tool permissions ---
56
+ type SettingsWithPermissions = Settings & { permissions?: { allow?: string[] } };
57
+ const s = settings as SettingsWithPermissions;
58
+ if (s.permissions?.allow) {
59
+ s.permissions.allow = s.permissions.allow.filter(
60
+ (p) => !p.includes(PKG_ROOT) && !p.startsWith("Bash(bun run ai:")
61
+ );
62
+ if (s.permissions.allow.length === 0) delete s.permissions.allow;
63
+ if (Object.keys(s.permissions).length === 0) delete s.permissions;
64
+ }
65
+
66
+ writeJson(SETTINGS, settings);
67
+ log.success("Removed PAL hooks and env from settings.json");
68
+
69
+ // --- Remove PAL skills ---
70
+ const removed = removeSkills(resolve(CLAUDE_DIR, "skills"));
71
+ if (removed.length > 0) {
72
+ log.success(`Removed ${removed.length} skill(s): ${removed.join(", ")}`);
73
+ } else {
74
+ log.info("No PAL skills found");
75
+ }
76
+
77
+ // --- Remove PAL agents ---
78
+ const removedAgents = removeAgents();
79
+ if (removedAgents.length > 0) {
80
+ log.success(`Removed ${removedAgents.length} agent(s): ${removedAgents.join(", ")}`);
81
+ } else {
82
+ log.info("No PAL agents found");
83
+ }
84
+
85
+ // --- Remove AGENTS.md and CLAUDE.md symlink ---
86
+ const agentsMd = resolve(platform.opencodeDir(), "AGENTS.md");
87
+ const claudeMd = resolve(CLAUDE_DIR, "CLAUDE.md");
88
+ try {
89
+ unlinkSync(claudeMd);
90
+ log.success("Removed ~/.claude/CLAUDE.md");
91
+ } catch {
92
+ /* gone */
93
+ }
94
+ try {
95
+ unlinkSync(agentsMd);
96
+ log.success("Removed ~/.config/opencode/AGENTS.md");
97
+ } catch {
98
+ /* gone */
99
+ }
100
+
101
+ log.success("Claude Code uninstall complete");