portable-agent-layer 0.10.0 → 0.12.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 (45) hide show
  1. package/assets/skills/{analyze-pdf.md → analyze-pdf/SKILL.md} +4 -4
  2. package/{src → assets/skills/analyze-pdf}/tools/pdf-download.ts +3 -3
  3. package/assets/skills/{analyze-youtube.md → analyze-youtube/SKILL.md} +4 -4
  4. package/{src → assets/skills/analyze-youtube}/tools/youtube-analyze.ts +2 -2
  5. package/assets/skills/{council.md → council/SKILL.md} +3 -2
  6. package/assets/skills/{create-skill.md → create-skill/SKILL.md} +2 -1
  7. package/assets/skills/{extract-entities.md → extract-entities/SKILL.md} +4 -5
  8. package/{src → assets/skills/extract-entities}/tools/entity-save.ts +3 -3
  9. package/assets/skills/{extract-wisdom.md → extract-wisdom/SKILL.md} +3 -2
  10. package/assets/skills/{first-principles.md → first-principles/SKILL.md} +3 -2
  11. package/assets/skills/{fyzz-chat-api.md → fyzz-chat-api/SKILL.md} +6 -6
  12. package/{src → assets/skills/fyzz-chat-api}/tools/fyzz-api.ts +6 -6
  13. package/assets/skills/{reflect.md → reflect/SKILL.md} +2 -1
  14. package/assets/skills/{research.md → research/SKILL.md} +2 -1
  15. package/assets/skills/{review.md → review/SKILL.md} +2 -1
  16. package/assets/skills/{summarize.md → summarize/SKILL.md} +3 -2
  17. package/assets/skills/telos/SKILL.md +94 -0
  18. package/assets/skills/telos/tools/update-telos.ts +100 -0
  19. package/assets/skills/think/SKILL.md +47 -0
  20. package/assets/templates/AGENTS.md.template +51 -32
  21. package/assets/templates/PAL/ALGORITHM.md +120 -0
  22. package/assets/templates/PAL/CONTEXT_ROUTING.md +28 -0
  23. package/assets/templates/PAL/MEMORY_SYSTEM.md +26 -0
  24. package/assets/templates/PAL/OPINION_TRACKING.md +3 -0
  25. package/assets/templates/PAL/STEERING_RULES.md +43 -0
  26. package/assets/templates/PAL/WORK_TRACKING.md +14 -0
  27. package/assets/templates/pal-settings.json +32 -0
  28. package/assets/templates/settings.claude.json +80 -0
  29. package/package.json +4 -7
  30. package/src/cli/index.ts +7 -0
  31. package/src/cli/setup-identity.ts +119 -0
  32. package/src/hooks/lib/claude-md.ts +52 -26
  33. package/src/hooks/lib/context.ts +49 -25
  34. package/src/hooks/lib/paths.ts +2 -0
  35. package/src/hooks/lib/security.ts +2 -0
  36. package/src/hooks/lib/setup.ts +4 -16
  37. package/src/targets/claude/install.ts +20 -93
  38. package/src/targets/claude/uninstall.ts +22 -47
  39. package/src/targets/lib.ts +207 -48
  40. package/src/targets/opencode/install.ts +13 -2
  41. package/src/targets/opencode/uninstall.ts +4 -1
  42. package/assets/templates/STEERING-RULES.md +0 -23
  43. package/assets/templates/telos/IDENTITY.md +0 -4
  44. package/src/cli/install.ts +0 -86
  45. package/src/cli/uninstall.ts +0 -45
@@ -17,7 +17,6 @@ import {
17
17
  writeFileSync,
18
18
  } from "node:fs";
19
19
  import { dirname, relative, resolve } from "node:path";
20
- import { loadTelos } from "./context";
21
20
  import { assets, ensureDir, palHome, paths, platform } from "./paths";
22
21
  import { buildSetupPrompt, readSetupState } from "./setup";
23
22
 
@@ -71,54 +70,81 @@ export function needsRebuild(): boolean {
71
70
 
72
71
  const outputMtime = statSync(outputPath).mtimeMs;
73
72
 
74
- // Collect source files: template + setup.json + all telos/*.md
73
+ // Collect source files: template + setup.json + identity + PAL docs
75
74
  const sources: string[] = [
76
75
  TEMPLATE_PATH,
77
- resolve(dirname(TEMPLATE_PATH), "STEERING-RULES.md"),
78
76
  resolve(paths.state(), "setup.json"),
77
+ palSettingsPath(),
79
78
  ];
80
79
 
81
- const telosDir = paths.telos();
82
- if (existsSync(telosDir)) {
83
- for (const f of readdirSync(telosDir).filter((f) => f.endsWith(".md"))) {
84
- sources.push(resolve(telosDir, f));
80
+ // Track PAL doc sources for rebuild detection
81
+ const palDocsDir = assets.palDocs();
82
+ if (existsSync(palDocsDir)) {
83
+ for (const f of readdirSync(palDocsDir).filter((f) => f.endsWith(".md"))) {
84
+ sources.push(resolve(palDocsDir, f));
85
85
  }
86
86
  }
87
87
 
88
88
  return latestMtime(...sources) > outputMtime;
89
89
  }
90
90
 
91
- function memoryPaths(): string {
92
- const mem = resolve(palHome(), "memory");
93
- return [
94
- `- **Wisdom frames**: \`${resolve(mem, "wisdom", "frames")}/\` — crystallized principles per domain (loaded every session)`,
95
- `- **Relationship notes**: \`${resolve(mem, "relationship")}/YYYY-MM/YYYY-MM-DD.md\` — daily interaction observations (loaded every session)`,
96
- `- **Session learnings**: \`${resolve(mem, "learning", "session")}/YYYY-MM/*.md\` — reusable insights from sessions (loaded every session)`,
97
- `- **Failure captures**: \`${resolve(mem, "learning", "failures")}/YYYY-MM/{timestamp}_{slug}/capture.md\` — what went wrong and why`,
98
- `- **Signals**: \`${resolve(mem, "signals")}/ratings.jsonl\` — append-only rating signal log (do not edit directly)`,
99
- ].join("\n");
91
+ interface Identity {
92
+ ai: { name: string; displayName: string; catchphrase: string };
93
+ principal: { name: string };
94
+ }
95
+
96
+ const IDENTITY_DEFAULTS: Identity = {
97
+ ai: { name: "Assistant", displayName: "ASSISTANT", catchphrase: "" },
98
+ principal: { name: "" },
99
+ };
100
+
101
+ function palSettingsPath(): string {
102
+ return resolve(palHome(), "memory", "pal-settings.json");
103
+ }
104
+
105
+ /** Load identity from pal-settings.json */
106
+ export function loadIdentity(): Identity {
107
+ const p = palSettingsPath();
108
+ if (!existsSync(p)) return IDENTITY_DEFAULTS;
109
+
110
+ try {
111
+ const data = JSON.parse(readFileSync(p, "utf-8"));
112
+ const ai = data.identity?.ai ?? {};
113
+ const principal = data.identity?.principal ?? {};
114
+ const name = ai.name || IDENTITY_DEFAULTS.ai.name;
115
+ const catchphrase = (ai.catchphrase || "").replace("{name}", name);
116
+
117
+ return {
118
+ ai: {
119
+ name,
120
+ displayName: ai.displayName || IDENTITY_DEFAULTS.ai.displayName,
121
+ catchphrase,
122
+ },
123
+ principal: {
124
+ name: principal.name || IDENTITY_DEFAULTS.principal.name,
125
+ },
126
+ };
127
+ } catch {
128
+ return IDENTITY_DEFAULTS;
129
+ }
100
130
  }
101
131
 
102
132
  /** Render AGENTS.md from the template using current state */
103
133
  export function buildClaudeMd(): string {
104
134
  const template = existsSync(TEMPLATE_PATH)
105
135
  ? readFileSync(TEMPLATE_PATH, "utf-8")
106
- : "# PAL Context\n\n{{SETUP_PROMPT}}\n{{TELOS}}\n## Memory\n\n{{MEMORY_PATHS}}\n";
136
+ : "# PAL Context\n\n{{SETUP_PROMPT}}\n";
107
137
 
108
138
  const state = readSetupState();
109
139
  const setupPrompt = state ? buildSetupPrompt(state) : null;
110
- const telos = loadTelos();
111
-
112
- const steeringPath = resolve(dirname(TEMPLATE_PATH), "STEERING-RULES.md");
113
- const steeringRules = existsSync(steeringPath)
114
- ? readFileSync(steeringPath, "utf-8").trim()
115
- : "";
140
+ const identity = loadIdentity();
116
141
 
117
142
  return template
118
143
  .replace("{{SETUP_PROMPT}}", setupPrompt ? `${setupPrompt}\n` : "")
119
- .replace("{{TELOS}}", telos ? `${telos}\n` : "")
120
- .replace("{{MEMORY_PATHS}}", memoryPaths())
121
- .replace("{{STEERING_RULES}}", steeringRules);
144
+ .replaceAll("{{IDENTITY_NAME}}", identity.ai.name)
145
+ .replaceAll("{{IDENTITY_DISPLAY}}", identity.ai.displayName)
146
+ .replaceAll("{{IDENTITY_CATCHPHRASE}}", identity.ai.catchphrase)
147
+ .replaceAll("{{PRINCIPAL_NAME}}", identity.principal.name);
122
148
  }
123
149
 
124
150
  /** Regenerate AGENTS.md if any source file is newer, and ensure CLAUDE.md symlink exists. Returns true if rebuilt. */
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { existsSync, readdirSync, readFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
7
8
  import { resolve } from "node:path";
8
9
  import { parse } from "./frontmatter";
9
10
  import { readFailures, readLearnings } from "./learning-store";
@@ -21,28 +22,44 @@ import {
21
22
  staleProjects,
22
23
  } from "./work-tracking";
23
24
 
24
- /** Load all populated TELOS files as a single markdown string */
25
- export function loadTelos(): string {
26
- const telosDir = paths.telos();
27
- if (!existsSync(telosDir)) return "";
25
+ interface PalSettings {
26
+ loadAtStartup?: { files?: string[] };
27
+ dynamicContext?: Record<string, boolean>;
28
+ }
29
+
30
+ /** Load pal-settings.json from memory/ */
31
+ function loadPalSettings(): PalSettings {
32
+ const p = resolve(paths.memory(), "pal-settings.json");
33
+ if (!existsSync(p)) return {};
34
+ try {
35
+ return JSON.parse(readFileSync(p, "utf-8"));
36
+ } catch {
37
+ return {};
38
+ }
39
+ }
40
+
41
+ /** Check if a dynamic context section is enabled (defaults to true) */
42
+ function isEnabled(settings: PalSettings, key: string): boolean {
43
+ return settings.dynamicContext?.[key] !== false;
44
+ }
28
45
 
29
- const files = readdirSync(telosDir)
30
- .filter((f) => f.endsWith(".md"))
31
- .sort();
46
+ /** Load and concatenate loadAtStartup files */
47
+ function loadStartupFiles(settings: PalSettings): string {
48
+ const files = settings.loadAtStartup?.files;
49
+ if (!files || files.length === 0) return "";
32
50
 
51
+ const home = homedir();
33
52
  const sections: string[] = [];
34
53
 
35
54
  for (const file of files) {
36
- const content = readFileSync(resolve(telosDir, file), "utf-8").trim();
37
- // Skip empty templates (only have a heading and comment)
38
- const realLines = content
39
- .split("\n")
40
- .filter(
41
- (l) =>
42
- !l.startsWith("#") && !l.startsWith("<!--") && !l.startsWith("-->") && l.trim()
43
- );
44
- if (realLines.length === 0) continue;
45
- sections.push(content);
55
+ const resolved = file.replace("~", home);
56
+ if (!existsSync(resolved)) continue;
57
+ try {
58
+ const content = readFileSync(resolved, "utf-8").trim();
59
+ if (content) sections.push(content);
60
+ } catch {
61
+ /* skip unreadable files */
62
+ }
46
63
  }
47
64
 
48
65
  return sections.join("\n\n---\n\n");
@@ -348,15 +365,22 @@ export function loadRelationshipContext(): string {
348
365
  * things that change per-session and can't live in a static file.
349
366
  */
350
367
  export function buildSystemReminder(): string {
351
- const work = loadActiveWork();
352
- const wisdom = loadWisdomContext();
353
- const relationship = loadRelationshipContext();
354
- const digest = loadLearningDigest();
355
- const trends = loadSignalTrends();
356
- const failures = loadFailurePatterns();
357
- const synthesis = loadSynthesisRecommendations();
358
- const opinions = loadOpinionContext();
368
+ const settings = loadPalSettings();
369
+ const startup = loadStartupFiles(settings);
370
+ const work = isEnabled(settings, "activeWork") ? loadActiveWork() : null;
371
+ const wisdom = isEnabled(settings, "wisdom") ? loadWisdomContext() : "";
372
+ const relationship = isEnabled(settings, "relationship")
373
+ ? loadRelationshipContext()
374
+ : "";
375
+ const digest = isEnabled(settings, "learningDigest") ? loadLearningDigest() : "";
376
+ const trends = isEnabled(settings, "signalTrends") ? loadSignalTrends() : "";
377
+ const failures = isEnabled(settings, "failurePatterns") ? loadFailurePatterns() : "";
378
+ const synthesis = isEnabled(settings, "synthesis")
379
+ ? loadSynthesisRecommendations()
380
+ : "";
381
+ const opinions = isEnabled(settings, "opinions") ? loadOpinionContext() : "";
359
382
  const parts: string[] = [];
383
+ if (startup) parts.push(startup);
360
384
  if (wisdom) parts.push(wisdom);
361
385
  if (opinions) parts.push(opinions);
362
386
  if (relationship) parts.push(relationship);
@@ -77,4 +77,6 @@ export const assets = {
77
77
  hooks: () => pkg("src", "hooks"),
78
78
  telosTemplates: () => pkg("assets", "templates", "telos"),
79
79
  agentsMdTemplate: () => pkg("assets", "templates", "AGENTS.md.template"),
80
+ claudeSettingsTemplate: () => pkg("assets", "templates", "settings.claude.json"),
81
+ palDocs: () => pkg("assets", "templates", "PAL"),
80
82
  } as const;
@@ -33,6 +33,7 @@ export const HOOK_MANAGED_FILES = [
33
33
  "update-available.json",
34
34
  "debug.log.prev",
35
35
  "opinions.json",
36
+ "pal-settings.json",
36
37
  ];
37
38
 
38
39
  /** Hook-managed directories — AI must not write to or delete from these */
@@ -43,6 +44,7 @@ export const HOOK_MANAGED_DIRS = [
43
44
  "memory/learning/synthesis",
44
45
  "memory/relationship",
45
46
  "memory/wisdom/state",
47
+ ".agents/PAL",
46
48
  ];
47
49
 
48
50
  /** Escape a string for use in a RegExp */
@@ -26,20 +26,8 @@ export interface SetupState {
26
26
  const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
27
27
  mission: {
28
28
  file: "telos/MISSION.md",
29
- question: "What's your name and what do you do?",
30
- hint: "Write their name, role, and core purpose to telos/MISSION.md",
31
- },
32
- ai_name: {
33
- file: "telos/IDENTITY.md",
34
- question:
35
- "What would you like to call your AI? (Pick a name — this is how I'll identify myself.)",
36
- hint: "Write the chosen AI name and identity to telos/IDENTITY.md with fields: name, fullName (name — Personal AI), displayName (UPPERCASED)",
37
- },
38
- catchphrase: {
39
- file: "telos/IDENTITY.md",
40
- question:
41
- 'What should your AI\'s startup catchphrase be? (e.g. "{name} here, ready to go" — {name} gets replaced with the AI name.)',
42
- hint: "Append the catchphrase to telos/IDENTITY.md under a ## Catchphrase heading. Support {name} as a placeholder.",
29
+ question: "What do you do? What's your role and core purpose?",
30
+ hint: "Write their role and core purpose to telos/MISSION.md",
43
31
  },
44
32
  goals: {
45
33
  file: "telos/GOALS.md",
@@ -152,8 +140,8 @@ export function buildSetupPrompt(state: SetupState): string | null {
152
140
  const lines: string[] = [
153
141
  "## IMPORTANT: PAL First-Run Setup Required",
154
142
  "",
155
- "TELOS files are empty — this user has not been set up yet.",
156
- "You MUST start the setup process immediately, regardless of what the user says.",
143
+ "TELOS files are empty — the user's identity is already configured (via the installer),",
144
+ "but personal context is still needed. You MUST start the setup process immediately.",
157
145
  "Greet them, explain that PAL needs to learn about them to personalize future sessions,",
158
146
  "and ask the first remaining question below. Do NOT wait for the user to ask about setup.",
159
147
  "",
@@ -1,21 +1,25 @@
1
1
  /**
2
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.
3
+ * Merges settings template into existing settings.json (never overwrites).
4
+ * Copies skills, agents, and PAL docs. Generates CLAUDE.md from TELOS.
5
5
  */
6
6
 
7
7
  import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
8
8
  import { resolve } from "node:path";
9
9
  import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
10
- import { palHome, palPkg, platform } from "../../hooks/lib/paths";
10
+ import { assets, palHome, palPkg, platform } from "../../hooks/lib/paths";
11
11
  import {
12
12
  copyAgents,
13
+ copyPalDocs,
13
14
  copySkills,
14
15
  countAgents,
15
16
  countMd,
16
17
  countSkills,
18
+ loadSettingsTemplate,
17
19
  log,
20
+ mergeSettings,
18
21
  readJson,
22
+ scaffoldPalSettings,
19
23
  writeJson,
20
24
  } from "../lib";
21
25
 
@@ -35,96 +39,13 @@ const backup = `${SETTINGS}.bak.${Date.now()}`;
35
39
  copyFileSync(SETTINGS, backup);
36
40
  log.info("Backed up settings.json");
37
41
 
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
- };
42
+ // --- Load template and merge into existing settings ---
43
+ const template = loadSettingsTemplate(assets.claudeSettingsTemplate(), PKG_ROOT);
44
+ const existing = readJson<Record<string, unknown>>(SETTINGS, {});
45
+ const merged = mergeSettings(existing, template);
90
46
 
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");
47
+ writeJson(SETTINGS, merged);
48
+ log.success("Merged PAL settings into settings.json");
128
49
 
129
50
  // --- Copy skills ---
130
51
  const skillsDir = resolve(CLAUDE_DIR, "skills");
@@ -133,13 +54,19 @@ copySkills(skillsDir);
133
54
  // --- Copy agents ---
134
55
  copyAgents();
135
56
 
57
+ // --- Copy PAL system docs ---
58
+ const palDocsCount = copyPalDocs();
59
+ log.success(`Installed ${palDocsCount} PAL docs to ~/.agents/PAL/`);
60
+
61
+ // --- Scaffold PAL settings ---
62
+ scaffoldPalSettings();
63
+
136
64
  // --- Generate ~/.claude/AGENTS.md and symlink ~/.claude/CLAUDE.md → AGENTS.md ---
137
65
  regenerateIfNeeded();
138
66
  log.success("Generated ~/.config/opencode/AGENTS.md (→ ~/.claude/CLAUDE.md symlink)");
139
67
 
140
68
  log.success("Claude Code installation complete");
141
69
  console.log("");
142
- log.info(`Hooks: 5 (SessionStart, UserPromptSubmit, PreToolUse×2, Stop)`);
143
70
  log.info(`Skills: ${countSkills()}`);
144
71
  log.info(`Agents: ${countAgents()}`);
145
72
  log.info(`TELOS: ${countMd(resolve(palHome(), "telos"))} files`);
@@ -1,14 +1,23 @@
1
1
  /**
2
2
  * PAL — Claude Code uninstaller (TypeScript)
3
- * Removes PAL hooks, skills, and env from settings.json.
3
+ * Removes exactly what the settings template added, plus skills, agents, and PAL docs.
4
4
  */
5
5
 
6
6
  import { copyFileSync, existsSync, unlinkSync } from "node:fs";
7
7
  import { resolve } from "node:path";
8
- import { palPkg, platform } from "../../hooks/lib/paths";
9
- import { log, readJson, removeAgents, removeSkills, writeJson } from "../lib";
8
+ import { assets, palPkg, platform } from "../../hooks/lib/paths";
9
+ import {
10
+ loadSettingsTemplate,
11
+ log,
12
+ readJson,
13
+ removeAgents,
14
+ removePalDocs,
15
+ removeSkills,
16
+ unmergeSettings,
17
+ writeJson,
18
+ } from "../lib";
10
19
 
11
- const PKG_ROOT = palPkg();
20
+ const PKG_ROOT = palPkg().replaceAll("\\", "/");
12
21
  const CLAUDE_DIR = platform.claudeDir();
13
22
  const SETTINGS = resolve(CLAUDE_DIR, "settings.json");
14
23
 
@@ -21,50 +30,13 @@ if (!existsSync(SETTINGS)) {
21
30
  copyFileSync(SETTINGS, `${SETTINGS}.bak.${Date.now()}`);
22
31
  log.info("Backed up settings.json");
23
32
 
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> };
33
+ // --- Load template and unmerge from existing settings ---
34
+ const template = loadSettingsTemplate(assets.claudeSettingsTemplate(), PKG_ROOT);
35
+ const existing = readJson<Record<string, unknown>>(SETTINGS, {});
36
+ const cleaned = unmergeSettings(existing, template);
31
37
 
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");
38
+ writeJson(SETTINGS, cleaned);
39
+ log.success("Removed PAL settings from settings.json");
68
40
 
69
41
  // --- Remove PAL skills ---
70
42
  const removed = removeSkills(resolve(CLAUDE_DIR, "skills"));
@@ -82,6 +54,9 @@ if (removedAgents.length > 0) {
82
54
  log.info("No PAL agents found");
83
55
  }
84
56
 
57
+ // --- Remove PAL system docs ---
58
+ removePalDocs();
59
+
85
60
  // --- Remove AGENTS.md and CLAUDE.md symlink ---
86
61
  const agentsMd = resolve(platform.opencodeDir(), "AGENTS.md");
87
62
  const claudeMd = resolve(CLAUDE_DIR, "CLAUDE.md");