portable-agent-layer 0.36.0 → 0.37.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 (89) hide show
  1. package/README.md +1 -0
  2. package/assets/skills/analyze-pdf/tools/pdf-download.ts +1 -1
  3. package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +1 -1
  4. package/assets/skills/consulting-report/tools/dev.ts +2 -2
  5. package/assets/skills/consulting-report/tools/generate-pdf.ts +9 -9
  6. package/assets/skills/consulting-report/tools/scaffold.ts +2 -2
  7. package/assets/skills/create-pdf/tools/md-to-html-pdf.ts +2 -2
  8. package/assets/skills/opinion/tools/opinion.ts +3 -2
  9. package/assets/skills/presentation/SKILL.md +1 -1
  10. package/assets/skills/presentation/tools/doctor.ts +2 -5
  11. package/assets/skills/presentation/tools/lib/inline.ts +6 -11
  12. package/assets/skills/presentation/tools/lib/lint-helpers.ts +2 -2
  13. package/assets/skills/presentation/tools/lib/lint-rules.ts +5 -2
  14. package/assets/skills/presentation/tools/setup-template.ts +10 -7
  15. package/assets/skills/projects/SKILL.md +44 -20
  16. package/assets/skills/research/tools/gemini-search.ts +2 -2
  17. package/assets/skills/research/tools/grok-search.ts +2 -2
  18. package/assets/skills/research/tools/perplexity-search.ts +2 -2
  19. package/assets/skills/telos/tools/update-telos.ts +0 -1
  20. package/assets/templates/PAL/ALGORITHM.md +27 -3
  21. package/assets/templates/hooks.codex.json +44 -0
  22. package/assets/templates/hooks.cursor.json +11 -5
  23. package/package.json +2 -1
  24. package/src/cli/index.ts +112 -14
  25. package/src/cli/migrate.ts +299 -0
  26. package/src/cli/setup-identity.ts +3 -3
  27. package/src/cli/setup-telos.ts +0 -1
  28. package/src/hooks/CompactRecover.ts +11 -5
  29. package/src/hooks/LoadContext.ts +14 -2
  30. package/src/hooks/PreCompactPersist.ts +26 -34
  31. package/src/hooks/SecurityValidator.ts +43 -21
  32. package/src/hooks/StopOrchestrator.ts +4 -1
  33. package/src/hooks/UserPromptOrchestrator.ts +4 -2
  34. package/src/hooks/handlers/auto-graduate.ts +2 -2
  35. package/src/hooks/handlers/backup.ts +3 -3
  36. package/src/hooks/handlers/failure.ts +5 -3
  37. package/src/hooks/handlers/inject-retrieval.ts +29 -6
  38. package/src/hooks/handlers/persist-last-exchange.ts +76 -0
  39. package/src/hooks/handlers/rating.ts +2 -1
  40. package/src/hooks/handlers/readme-sync.ts +3 -2
  41. package/src/hooks/handlers/session-intelligence.ts +9 -8
  42. package/src/hooks/handlers/session-name.ts +2 -2
  43. package/src/hooks/handlers/synthesis.ts +5 -2
  44. package/src/hooks/handlers/update-counts.ts +3 -2
  45. package/src/hooks/lib/agent.ts +20 -18
  46. package/src/hooks/lib/context.ts +45 -117
  47. package/src/hooks/lib/entities.ts +7 -7
  48. package/src/hooks/lib/frontmatter.ts +4 -4
  49. package/src/hooks/lib/graduation.ts +7 -6
  50. package/src/hooks/lib/inference.ts +6 -2
  51. package/src/hooks/lib/learning-category.ts +1 -1
  52. package/src/hooks/lib/learning-store.ts +6 -1
  53. package/src/hooks/lib/notify.ts +2 -2
  54. package/src/hooks/lib/opinions.ts +3 -3
  55. package/src/hooks/lib/paths.ts +2 -0
  56. package/src/hooks/lib/projects.ts +142 -74
  57. package/src/hooks/lib/readme-sync.ts +1 -1
  58. package/src/hooks/lib/relationship.ts +3 -15
  59. package/src/hooks/lib/retrieval-index.ts +5 -3
  60. package/src/hooks/lib/retrieval.ts +11 -12
  61. package/src/hooks/lib/security.ts +22 -18
  62. package/src/hooks/lib/semi-static.ts +4 -2
  63. package/src/hooks/lib/session-names.ts +1 -1
  64. package/src/hooks/lib/settings.ts +1 -1
  65. package/src/hooks/lib/setup.ts +2 -60
  66. package/src/hooks/lib/signals.ts +2 -2
  67. package/src/hooks/lib/stdin.ts +1 -1
  68. package/src/hooks/lib/stop.ts +13 -6
  69. package/src/hooks/lib/token-usage.ts +1 -2
  70. package/src/hooks/lib/transcript.ts +1 -1
  71. package/src/hooks/lib/wisdom.ts +5 -5
  72. package/src/hooks/lib/work-tracking.ts +8 -14
  73. package/src/targets/codex/install.ts +95 -0
  74. package/src/targets/codex/uninstall.ts +70 -0
  75. package/src/targets/lib.ts +140 -14
  76. package/src/targets/opencode/plugin.ts +22 -11
  77. package/src/tools/agent/algorithm-reflect.ts +1 -1
  78. package/src/tools/agent/analyze.ts +18 -18
  79. package/src/tools/agent/handoff-note.ts +1 -1
  80. package/src/tools/agent/project.ts +375 -75
  81. package/src/tools/agent/synthesize.ts +6 -42
  82. package/src/tools/agent/thread.ts +15 -14
  83. package/src/tools/agent/wisdom-frame.ts +9 -3
  84. package/src/tools/import.ts +1 -1
  85. package/src/tools/relationship-reflect.ts +13 -11
  86. package/src/tools/self-model.ts +20 -16
  87. package/src/tools/session-summary.ts +3 -3
  88. package/src/tools/token-cost.ts +15 -16
  89. package/assets/skills/telos/tools/update-projects.ts +0 -106
@@ -5,23 +5,15 @@
5
5
  * The AI is instructed to mark steps done after writing each file.
6
6
  */
7
7
 
8
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
9
- import { resolve } from "node:path";
10
- import { ensureDir, palHome, paths } from "./paths";
8
+ import { existsSync, readFileSync } from "node:fs";
11
9
 
12
- export interface SetupStep {
10
+ interface SetupStep {
13
11
  done: boolean;
14
12
  file: string;
15
13
  question: string;
16
14
  hint: string;
17
15
  }
18
16
 
19
- export interface SetupState {
20
- version: number;
21
- completed: boolean;
22
- steps: Record<string, SetupStep>;
23
- }
24
-
25
17
  /** Ordered setup steps — defines the wizard flow */
26
18
  export const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
27
19
  mission: {
@@ -50,10 +42,6 @@ export const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
50
42
 
51
43
  export const STEP_ORDER = Object.keys(SETUP_STEPS);
52
44
 
53
- function setupPath(): string {
54
- return resolve(ensureDir(paths.state()), "setup.json");
55
- }
56
-
57
45
  /** Check if a TELOS file has real content (not just template scaffolding) */
58
46
  export function hasRealContent(filePath: string): boolean {
59
47
  if (!existsSync(filePath)) return false;
@@ -70,49 +58,3 @@ export function hasRealContent(filePath: string): boolean {
70
58
  return false;
71
59
  }
72
60
  }
73
-
74
- /** Create initial setup state, auto-detecting already-populated TELOS files */
75
- export function createInitialState(): SetupState {
76
- const steps: Record<string, SetupStep> = {};
77
- for (const [key, def] of Object.entries(SETUP_STEPS)) {
78
- const populated = hasRealContent(resolve(palHome(), def.file));
79
- steps[key] = { done: populated, ...def };
80
- }
81
- const allDone = Object.values(steps).every((s) => s.done);
82
- return { version: 1, completed: allDone, steps };
83
- }
84
-
85
- /** Read setup state, or return null if no setup.json exists */
86
- export function readSetupState(): SetupState | null {
87
- const p = setupPath();
88
- if (!existsSync(p)) return null;
89
- try {
90
- return JSON.parse(readFileSync(p, "utf-8"));
91
- } catch {
92
- return null;
93
- }
94
- }
95
-
96
- /** Write setup state to disk */
97
- export function writeSetupState(state: SetupState): void {
98
- writeFileSync(setupPath(), `${JSON.stringify(state, null, 2)}\n`);
99
- }
100
-
101
- /** Seed setup.json if it doesn't exist yet. Returns the state. */
102
- export function ensureSetupState(): SetupState {
103
- const existing = readSetupState();
104
- if (existing) return existing;
105
- const fresh = createInitialState();
106
- writeSetupState(fresh);
107
- return fresh;
108
- }
109
-
110
- /** Get the list of remaining (not done) step keys, in order */
111
- export function remainingSteps(state: SetupState): string[] {
112
- return STEP_ORDER.filter((k) => !state.steps[k]?.done);
113
- }
114
-
115
- /** Check if setup is fully completed */
116
- export function isSetupComplete(state: SetupState): boolean {
117
- return state.completed;
118
- }
@@ -3,14 +3,14 @@ import { resolve } from "node:path";
3
3
  import { paths } from "./paths";
4
4
  import { now } from "./time";
5
5
 
6
- export interface Signal {
6
+ interface Signal {
7
7
  ts: string;
8
8
  type: string;
9
9
  [key: string]: unknown;
10
10
  }
11
11
 
12
12
  /** Append a signal to a JSONL file in the signals directory */
13
- export function emitSignal(
13
+ function emitSignal(
14
14
  filename: string,
15
15
  data: { type: string; [key: string]: unknown }
16
16
  ): void {
@@ -1,5 +1,5 @@
1
1
  /** Read all of stdin as a string */
2
- export async function readStdin(): Promise<string> {
2
+ async function readStdin(): Promise<string> {
3
3
  const chunks: Buffer[] = [];
4
4
  for await (const chunk of Bun.stdin.stream()) {
5
5
  chunks.push(Buffer.from(chunk));
@@ -3,13 +3,15 @@
3
3
  * Used by StopOrchestrator.ts (Claude Code) and opencode plugin.
4
4
  */
5
5
 
6
- import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
6
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { readFile, unlink } from "node:fs/promises";
7
8
  import { resolve } from "node:path";
8
9
  import { autoGraduate } from "../handlers/auto-graduate";
9
10
  import { autoBackup } from "../handlers/backup";
10
11
  import { writeContextDigests } from "../handlers/context-digests";
11
12
  import { notifyDesktop } from "../handlers/desktop-notify";
12
13
  import { captureFailure } from "../handlers/failure";
14
+ import { persistLastExchange } from "../handlers/persist-last-exchange";
13
15
  import { projectTouch } from "../handlers/project-touch";
14
16
  import { checkReflectTrigger } from "../handlers/reflect-trigger";
15
17
  import { checkSelfModelTrigger } from "../handlers/self-model-trigger";
@@ -23,7 +25,7 @@ import { logDebug, logError } from "./log";
23
25
  import { ensureDir, paths } from "./paths";
24
26
  import { extractContent, extractLastAssistant, parseMessages } from "./transcript";
25
27
 
26
- export interface RunStopHandlersOptions {
28
+ interface RunStopHandlersOptions {
27
29
  lastAssistantMessage?: string;
28
30
  sessionId?: string;
29
31
  }
@@ -41,6 +43,9 @@ export async function runStopHandlers(
41
43
  // Cache last assistant response (session-scoped)
42
44
  cacheLastResponse(messages, options.lastAssistantMessage, options.sessionId);
43
45
 
46
+ // Always persist last exchange — drives CompactRecover + "Pick Up Where You Left Off"
47
+ if (options.sessionId) persistLastExchange(messages, options.sessionId);
48
+
44
49
  // Run all handlers concurrently. Auto-graduate is idempotent (24h TTL +
45
50
  // state-dedup + content-dedup) so it's safe to fire on every Stop.
46
51
  // project-touch only fires when cwd resolves to an active registered project.
@@ -151,15 +156,16 @@ async function checkPendingFailure(transcript: string): Promise<void> {
151
156
  if (!existsSync(pendingPath)) return;
152
157
 
153
158
  try {
154
- const pending = JSON.parse(readFileSync(pendingPath, "utf-8")) as {
159
+ const pending = JSON.parse(await readFile(pendingPath, "utf-8")) as {
155
160
  rating: number;
156
161
  context: string;
157
162
  detailedContext?: string;
158
163
  principle?: string;
159
164
  responsePreview?: string;
160
165
  userPreview?: string;
166
+ cwd?: string;
161
167
  };
162
- unlinkSync(pendingPath);
168
+ await unlink(pendingPath);
163
169
 
164
170
  // Extract principle from full transcript if not already present
165
171
  let { principle, detailedContext } = pending;
@@ -199,7 +205,7 @@ Return JSON:
199
205
  detailed_context?: string;
200
206
  };
201
207
  principle = parsed.principle || undefined;
202
- if (!detailedContext) detailedContext = parsed.detailed_context || undefined;
208
+ detailedContext ??= parsed.detailed_context || undefined;
203
209
  }
204
210
  } catch {
205
211
  /* graceful fallback — capture without principle */
@@ -211,7 +217,8 @@ Return JSON:
211
217
  pending.context,
212
218
  transcript,
213
219
  detailedContext,
214
- principle
220
+ principle,
221
+ pending.cwd
215
222
  );
216
223
  } catch {
217
224
  // Non-critical
@@ -8,10 +8,9 @@ import { resolve } from "node:path";
8
8
  import { HAIKU_MODEL } from "./models";
9
9
  import { ensureDir, paths } from "./paths";
10
10
 
11
- export type TokenCaller =
11
+ type TokenCaller =
12
12
  | "rating"
13
13
  | "failure"
14
- | "work-learning"
15
14
  | "session-name"
16
15
  | "session-intelligence"
17
16
  | "relationship"
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { readFileSync } from "node:fs";
7
7
 
8
- export interface Message {
8
+ interface Message {
9
9
  role: string;
10
10
  content: string | unknown;
11
11
  }
@@ -63,13 +63,13 @@ function existingCrystalPrinciples(content: string): string[] {
63
63
  if (m[1]) out.push(m[1].trim());
64
64
  }
65
65
  for (const line of content.split("\n")) {
66
- const m = line.match(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*\d+%\]\s*$/);
66
+ const m = new RegExp(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*\d+%\]\s*$/).exec(line);
67
67
  if (m?.[1]) out.push(m[1].trim());
68
68
  }
69
69
  return out;
70
70
  }
71
71
 
72
- export interface PromoteCrystalResult {
72
+ interface PromoteCrystalResult {
73
73
  domain: string;
74
74
  principle: string;
75
75
  confidence: number;
@@ -123,7 +123,7 @@ export function promoteCrystal(
123
123
  return { domain, principle, confidence, framePath, skipped: null };
124
124
  }
125
125
 
126
- export interface FrameDoc {
126
+ interface FrameDoc {
127
127
  domain: string;
128
128
  principle: string;
129
129
  body: string;
@@ -153,7 +153,7 @@ export function readFramesForRetrieval(): FrameDoc[] {
153
153
  }
154
154
 
155
155
  for (const line of content.split("\n")) {
156
- const m = line.match(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*(\d+)%\]\s*$/);
156
+ const m = new RegExp(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*(\d+)%\]\s*$/).exec(line);
157
157
  if (!m) continue;
158
158
  const name = m[1]?.trim();
159
159
  const pct = parseInt(m[2] ?? "", 10);
@@ -188,7 +188,7 @@ export function readFramePrinciples(): string[] {
188
188
 
189
189
  // legacy fallback: bullet lines "- X [CRYSTAL: N%]"
190
190
  for (const line of content.split("\n")) {
191
- const match = line.match(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*(\d+)%\]\s*$/);
191
+ const match = new RegExp(/^\s*-\s*(.+?)\s*\[CRYSTAL:\s*(\d+)%\]\s*$/).exec(line);
192
192
  if (!match) continue;
193
193
  const name = match[1]?.trim();
194
194
  const pct = parseInt(match[2] ?? "", 10);
@@ -29,7 +29,7 @@ function sessionsPath(): string {
29
29
  return resolve(ensureDir(paths.state()), "sessions.json");
30
30
  }
31
31
 
32
- export function readSessions(): SessionRecord[] {
32
+ function readSessions(): SessionRecord[] {
33
33
  const p = sessionsPath();
34
34
  if (!existsSync(p)) return [];
35
35
  try {
@@ -54,12 +54,6 @@ export function writeSession(record: SessionRecord): void {
54
54
  writeFileSync(sessionsPath(), JSON.stringify(pruned, null, 2), "utf-8");
55
55
  }
56
56
 
57
- /** Filter sessions within the last N hours */
58
- export function recentSessions(hours: number): SessionRecord[] {
59
- const cutoff = Date.now() - hours * 60 * 60 * 1000;
60
- return readSessions().filter((s) => new Date(s.ts).getTime() > cutoff);
61
- }
62
-
63
57
  /** Detect session completion status from last assistant message */
64
58
  export function detectStatus(lastAssistant: string): SessionRecord["status"] {
65
59
  const completionSignals =
@@ -118,15 +112,15 @@ function cleanForHandoff(text: string): string {
118
112
  /** Extract handoff notes from last assistant message */
119
113
  export function extractHandoff(lastAssistant: string): string {
120
114
  // Look for explicit next-steps / TODO / remaining sections
121
- const sectionMatch = lastAssistant.match(
115
+ const sectionMatch = new RegExp(
122
116
  /(?:next steps?|todo|remaining|what's left|still need|want me to)[:\s]*\n([\s\S]{10,300}?)(?:\n\n|\n(?=[A-Z#]))/i
123
- );
117
+ ).exec(lastAssistant);
124
118
  if (sectionMatch) return cleanForHandoff(sectionMatch[1]);
125
119
 
126
120
  // Look for closing question/offer (common assistant pattern)
127
- const closingMatch = lastAssistant.match(
121
+ const closingMatch = new RegExp(
128
122
  /(?:want (?:me to|to)|shall I|should I|ready to|anything else|let me know)[^\n]*$/im
129
- );
123
+ ).exec(lastAssistant);
130
124
 
131
125
  const cleaned = cleanForHandoff(lastAssistant);
132
126
 
@@ -144,7 +138,7 @@ export function extractHandoff(lastAssistant: string): string {
144
138
 
145
139
  // ── Per-Project History ──────────────────────────────────────────
146
140
 
147
- export interface ProjectHistoryEntry {
141
+ interface ProjectHistoryEntry {
148
142
  date: string;
149
143
  title: string;
150
144
  summary: string;
@@ -152,8 +146,8 @@ export interface ProjectHistoryEntry {
152
146
  }
153
147
 
154
148
  /** Convert a cwd path to a filesystem-safe slug (last directory segment) */
155
- export function cwdToSlug(cwd: string): string {
156
- const normalized = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
149
+ function cwdToSlug(cwd: string): string {
150
+ const normalized = cwd.replaceAll("\\", "/").replace(/\/+$/, "");
157
151
  return normalized.split("/").pop() || "unknown";
158
152
  }
159
153
 
@@ -0,0 +1,95 @@
1
+ /**
2
+ * PAL — Codex target installer
3
+ * Merges PAL hooks into ~/.codex/hooks.json (never overwrites user hooks).
4
+ * Symlinks skills. Ensures AGENTS.md symlink via regenerateIfNeeded().
5
+ */
6
+
7
+ import {
8
+ copyFileSync,
9
+ existsSync,
10
+ mkdirSync,
11
+ readFileSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { resolve } from "node:path";
15
+ import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
16
+ import { assets, palPkg, platform } from "../../hooks/lib/paths";
17
+ import {
18
+ copySkills,
19
+ countSkills,
20
+ generateSkillIndex,
21
+ loadCodexHooksTemplate,
22
+ log,
23
+ mergeCodexHooks,
24
+ readJson,
25
+ scaffoldPalSettings,
26
+ writeJson,
27
+ } from "../lib";
28
+
29
+ /**
30
+ * Ensure `features.hooks = true` in config.toml without touching other content.
31
+ * Appends the setting if missing; skips if already present.
32
+ */
33
+ function enableCodexHooks(configPath: string): void {
34
+ let content = "";
35
+ if (existsSync(configPath)) {
36
+ content = readFileSync(configPath, "utf-8");
37
+ // Already enabled — nothing to do
38
+ if (/^\s*hooks\s*=\s*true/m.test(content)) {
39
+ log.info("Codex hooks already enabled in config.toml");
40
+ return;
41
+ }
42
+ // [features] section exists but no hooks line — insert after the header
43
+ if (/^\[features\]/m.test(content)) {
44
+ content = content.replace(/(\[features\][^\n]*\n)/, "$1hooks = true\n");
45
+ writeFileSync(configPath, content, "utf-8");
46
+ log.success("Added hooks = true to existing [features] section in config.toml");
47
+ return;
48
+ }
49
+ }
50
+ // No config.toml, or no [features] section — append the block
51
+ const block = `${content.endsWith("\n") || content === "" ? "" : "\n"}\n[features]\nhooks = true\n`;
52
+ writeFileSync(configPath, content + block, "utf-8");
53
+ log.success("Enabled hooks = true in ~/.codex/config.toml");
54
+ }
55
+
56
+ const PKG_ROOT = palPkg().replaceAll("\\", "/");
57
+ const CODEX_DIR = platform.codexDir();
58
+ const HOOKS_FILE = resolve(CODEX_DIR, "hooks.json");
59
+
60
+ // --- Ensure ~/.codex/ exists ---
61
+ mkdirSync(CODEX_DIR, { recursive: true });
62
+
63
+ // --- Merge hooks ---
64
+ if (existsSync(HOOKS_FILE)) {
65
+ copyFileSync(HOOKS_FILE, `${HOOKS_FILE}.bak.${Date.now()}`);
66
+ log.info("Backed up hooks.json");
67
+ }
68
+
69
+ const template = loadCodexHooksTemplate(assets.codexHooksTemplate(), PKG_ROOT);
70
+ const existing = readJson<Record<string, unknown>>(HOOKS_FILE, {});
71
+ const merged = mergeCodexHooks(existing, template);
72
+
73
+ writeJson(HOOKS_FILE, merged);
74
+ log.success("Merged PAL hooks into ~/.codex/hooks.json");
75
+
76
+ // --- Symlink skills to ~/.codex/skills/ ---
77
+ const codexSkillsDir = resolve(CODEX_DIR, "skills");
78
+ copySkills(codexSkillsDir);
79
+ generateSkillIndex();
80
+
81
+ // --- Scaffold PAL settings ---
82
+ scaffoldPalSettings();
83
+
84
+ // --- Generate / verify AGENTS.md symlink ---
85
+ regenerateIfNeeded();
86
+ log.success("Ensured AGENTS.md symlink at ~/.codex/AGENTS.md");
87
+
88
+ // --- Enable hooks in config.toml ---
89
+ const CONFIG_FILE = resolve(CODEX_DIR, "config.toml");
90
+ enableCodexHooks(CONFIG_FILE);
91
+
92
+ log.success("Codex installation complete");
93
+ console.log("");
94
+ log.info(`Skills: ${countSkills()}`);
95
+ log.info(`Hooks: ${HOOKS_FILE}`);
@@ -0,0 +1,70 @@
1
+ /**
2
+ * PAL — Codex uninstaller
3
+ * Removes only PAL-owned hooks from ~/.codex/hooks.json. Preserves user hooks.
4
+ * Removes PAL skill symlinks.
5
+ */
6
+
7
+ import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ import { assets, palPkg, platform } from "../../hooks/lib/paths";
10
+ import {
11
+ loadCodexHooksTemplate,
12
+ log,
13
+ readJson,
14
+ removeSkills,
15
+ unmergeCodexHooks,
16
+ writeJson,
17
+ } from "../lib";
18
+
19
+ /**
20
+ * Remove `hooks = true` from config.toml.
21
+ * Also removes a now-empty [features] section header.
22
+ */
23
+ function disableCodexHooks(configPath: string): void {
24
+ if (!existsSync(configPath)) return;
25
+ let content = readFileSync(configPath, "utf-8");
26
+ if (!/^\s*hooks\s*=\s*true/m.test(content)) return;
27
+
28
+ // Remove the hooks line
29
+ content = content.replace(/^[ \t]*hooks\s*=\s*true[ \t]*\n?/m, "");
30
+
31
+ // Remove [features] header if it's now empty (nothing between it and next section / EOF)
32
+ content = content.replace(/\[features\]\n(?=\[|$)/m, "");
33
+
34
+ writeFileSync(configPath, content, "utf-8");
35
+ log.success("Removed hooks from ~/.codex/config.toml");
36
+ }
37
+
38
+ const PKG_ROOT = palPkg().replaceAll("\\", "/");
39
+ const CODEX_DIR = platform.codexDir();
40
+ const HOOKS_FILE = resolve(CODEX_DIR, "hooks.json");
41
+
42
+ // --- Remove PAL hooks from hooks.json ---
43
+ if (existsSync(HOOKS_FILE)) {
44
+ copyFileSync(HOOKS_FILE, `${HOOKS_FILE}.bak.${Date.now()}`);
45
+ log.info("Backed up hooks.json");
46
+
47
+ const template = loadCodexHooksTemplate(assets.codexHooksTemplate(), PKG_ROOT);
48
+ const existing = readJson<Record<string, unknown>>(HOOKS_FILE, {});
49
+ const cleaned = unmergeCodexHooks(existing, template);
50
+
51
+ writeJson(HOOKS_FILE, cleaned);
52
+ log.success("Removed PAL hooks from ~/.codex/hooks.json");
53
+ } else {
54
+ log.info("No hooks.json found, nothing to do");
55
+ }
56
+
57
+ // --- Remove PAL skill symlinks ---
58
+ const codexSkillsDir = resolve(CODEX_DIR, "skills");
59
+ const removed = removeSkills(codexSkillsDir);
60
+ if (removed.length > 0) {
61
+ log.success(`Removed ${removed.length} skill(s): ${removed.join(", ")}`);
62
+ } else {
63
+ log.info("No PAL skills found");
64
+ }
65
+
66
+ // --- Disable hooks in config.toml ---
67
+ const CONFIG_FILE = resolve(CODEX_DIR, "config.toml");
68
+ disableCodexHooks(CONFIG_FILE);
69
+
70
+ log.success("Codex uninstall complete");