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.
- package/README.md +1 -0
- package/assets/skills/analyze-pdf/tools/pdf-download.ts +1 -1
- package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +1 -1
- package/assets/skills/consulting-report/tools/dev.ts +2 -2
- package/assets/skills/consulting-report/tools/generate-pdf.ts +9 -9
- package/assets/skills/consulting-report/tools/scaffold.ts +2 -2
- package/assets/skills/create-pdf/tools/md-to-html-pdf.ts +2 -2
- package/assets/skills/opinion/tools/opinion.ts +3 -2
- package/assets/skills/presentation/SKILL.md +1 -1
- package/assets/skills/presentation/tools/doctor.ts +2 -5
- package/assets/skills/presentation/tools/lib/inline.ts +6 -11
- package/assets/skills/presentation/tools/lib/lint-helpers.ts +2 -2
- package/assets/skills/presentation/tools/lib/lint-rules.ts +5 -2
- package/assets/skills/presentation/tools/setup-template.ts +10 -7
- package/assets/skills/projects/SKILL.md +44 -20
- package/assets/skills/research/tools/gemini-search.ts +2 -2
- package/assets/skills/research/tools/grok-search.ts +2 -2
- package/assets/skills/research/tools/perplexity-search.ts +2 -2
- package/assets/skills/telos/tools/update-telos.ts +0 -1
- package/assets/templates/PAL/ALGORITHM.md +27 -3
- package/assets/templates/hooks.codex.json +44 -0
- package/assets/templates/hooks.cursor.json +11 -5
- package/package.json +2 -1
- package/src/cli/index.ts +112 -14
- package/src/cli/migrate.ts +299 -0
- package/src/cli/setup-identity.ts +3 -3
- package/src/cli/setup-telos.ts +0 -1
- package/src/hooks/CompactRecover.ts +11 -5
- package/src/hooks/LoadContext.ts +14 -2
- package/src/hooks/PreCompactPersist.ts +26 -34
- package/src/hooks/SecurityValidator.ts +43 -21
- package/src/hooks/StopOrchestrator.ts +4 -1
- package/src/hooks/UserPromptOrchestrator.ts +4 -2
- package/src/hooks/handlers/auto-graduate.ts +2 -2
- package/src/hooks/handlers/backup.ts +3 -3
- package/src/hooks/handlers/failure.ts +5 -3
- package/src/hooks/handlers/inject-retrieval.ts +29 -6
- package/src/hooks/handlers/persist-last-exchange.ts +76 -0
- package/src/hooks/handlers/rating.ts +2 -1
- package/src/hooks/handlers/readme-sync.ts +3 -2
- package/src/hooks/handlers/session-intelligence.ts +9 -8
- package/src/hooks/handlers/session-name.ts +2 -2
- package/src/hooks/handlers/synthesis.ts +5 -2
- package/src/hooks/handlers/update-counts.ts +3 -2
- package/src/hooks/lib/agent.ts +20 -18
- package/src/hooks/lib/context.ts +45 -117
- package/src/hooks/lib/entities.ts +7 -7
- package/src/hooks/lib/frontmatter.ts +4 -4
- package/src/hooks/lib/graduation.ts +7 -6
- package/src/hooks/lib/inference.ts +6 -2
- package/src/hooks/lib/learning-category.ts +1 -1
- package/src/hooks/lib/learning-store.ts +6 -1
- package/src/hooks/lib/notify.ts +2 -2
- package/src/hooks/lib/opinions.ts +3 -3
- package/src/hooks/lib/paths.ts +2 -0
- package/src/hooks/lib/projects.ts +142 -74
- package/src/hooks/lib/readme-sync.ts +1 -1
- package/src/hooks/lib/relationship.ts +3 -15
- package/src/hooks/lib/retrieval-index.ts +5 -3
- package/src/hooks/lib/retrieval.ts +11 -12
- package/src/hooks/lib/security.ts +22 -18
- package/src/hooks/lib/semi-static.ts +4 -2
- package/src/hooks/lib/session-names.ts +1 -1
- package/src/hooks/lib/settings.ts +1 -1
- package/src/hooks/lib/setup.ts +2 -60
- package/src/hooks/lib/signals.ts +2 -2
- package/src/hooks/lib/stdin.ts +1 -1
- package/src/hooks/lib/stop.ts +13 -6
- package/src/hooks/lib/token-usage.ts +1 -2
- package/src/hooks/lib/transcript.ts +1 -1
- package/src/hooks/lib/wisdom.ts +5 -5
- package/src/hooks/lib/work-tracking.ts +8 -14
- package/src/targets/codex/install.ts +95 -0
- package/src/targets/codex/uninstall.ts +70 -0
- package/src/targets/lib.ts +140 -14
- package/src/targets/opencode/plugin.ts +22 -11
- package/src/tools/agent/algorithm-reflect.ts +1 -1
- package/src/tools/agent/analyze.ts +18 -18
- package/src/tools/agent/handoff-note.ts +1 -1
- package/src/tools/agent/project.ts +375 -75
- package/src/tools/agent/synthesize.ts +6 -42
- package/src/tools/agent/thread.ts +15 -14
- package/src/tools/agent/wisdom-frame.ts +9 -3
- package/src/tools/import.ts +1 -1
- package/src/tools/relationship-reflect.ts +13 -11
- package/src/tools/self-model.ts +20 -16
- package/src/tools/session-summary.ts +3 -3
- package/src/tools/token-cost.ts +15 -16
- package/assets/skills/telos/tools/update-projects.ts +0 -106
package/src/hooks/lib/setup.ts
CHANGED
|
@@ -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
|
|
9
|
-
import { resolve } from "node:path";
|
|
10
|
-
import { ensureDir, palHome, paths } from "./paths";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
9
|
|
|
12
|
-
|
|
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
|
-
}
|
package/src/hooks/lib/signals.ts
CHANGED
|
@@ -3,14 +3,14 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { paths } from "./paths";
|
|
4
4
|
import { now } from "./time";
|
|
5
5
|
|
|
6
|
-
|
|
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
|
-
|
|
13
|
+
function emitSignal(
|
|
14
14
|
filename: string,
|
|
15
15
|
data: { type: string; [key: string]: unknown }
|
|
16
16
|
): void {
|
package/src/hooks/lib/stdin.ts
CHANGED
package/src/hooks/lib/stop.ts
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
* Used by StopOrchestrator.ts (Claude Code) and opencode plugin.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, readFileSync,
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
+
type TokenCaller =
|
|
12
12
|
| "rating"
|
|
13
13
|
| "failure"
|
|
14
|
-
| "work-learning"
|
|
15
14
|
| "session-name"
|
|
16
15
|
| "session-intelligence"
|
|
17
16
|
| "relationship"
|
package/src/hooks/lib/wisdom.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
const normalized = cwd.
|
|
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");
|