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.
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/assets/agents/claude-researcher.md +43 -0
- package/assets/agents/investigative-researcher.md +44 -0
- package/assets/agents/multi-perspective-researcher.md +43 -0
- package/assets/skills/analyze-pdf.md +40 -0
- package/assets/skills/analyze-youtube.md +35 -0
- package/assets/skills/council.md +43 -0
- package/assets/skills/create-skill.md +31 -0
- package/assets/skills/extract-entities.md +63 -0
- package/assets/skills/extract-wisdom.md +18 -0
- package/assets/skills/first-principles.md +17 -0
- package/assets/skills/fyzz-chat-api.md +43 -0
- package/assets/skills/reflect.md +87 -0
- package/assets/skills/research.md +68 -0
- package/assets/skills/review.md +19 -0
- package/assets/skills/summarize.md +15 -0
- package/assets/templates/AGENTS.md.template +45 -0
- package/assets/templates/telos/BELIEFS.md +4 -0
- package/assets/templates/telos/CHALLENGES.md +4 -0
- package/assets/templates/telos/GOALS.md +12 -0
- package/assets/templates/telos/IDEAS.md +4 -0
- package/assets/templates/telos/IDENTITY.md +4 -0
- package/assets/templates/telos/LEARNED.md +4 -0
- package/assets/templates/telos/MISSION.md +4 -0
- package/assets/templates/telos/MODELS.md +4 -0
- package/assets/templates/telos/NARRATIVES.md +4 -0
- package/assets/templates/telos/PROJECTS.md +7 -0
- package/assets/templates/telos/STRATEGIES.md +4 -0
- package/bin/pal +24 -0
- package/bin/pal.bat +8 -0
- package/bin/pal.ps1 +30 -0
- package/package.json +82 -0
- package/src/cli/index.ts +344 -0
- package/src/cli/install.ts +86 -0
- package/src/cli/uninstall.ts +45 -0
- package/src/hooks/LoadContext.ts +41 -0
- package/src/hooks/SecurityValidator.ts +52 -0
- package/src/hooks/SkillGuard.ts +41 -0
- package/src/hooks/StopOrchestrator.ts +35 -0
- package/src/hooks/UserPromptOrchestrator.ts +35 -0
- package/src/hooks/handlers/backup.ts +41 -0
- package/src/hooks/handlers/failure.ts +136 -0
- package/src/hooks/handlers/rating.ts +409 -0
- package/src/hooks/handlers/relationship.ts +113 -0
- package/src/hooks/handlers/session-name.ts +121 -0
- package/src/hooks/handlers/synthesis.ts +109 -0
- package/src/hooks/handlers/tab.ts +8 -0
- package/src/hooks/handlers/update-counts.ts +151 -0
- package/src/hooks/handlers/work-learning.ts +183 -0
- package/src/hooks/handlers/work-session.ts +58 -0
- package/src/hooks/lib/claude-md.ts +121 -0
- package/src/hooks/lib/context.ts +433 -0
- package/src/hooks/lib/entities.ts +304 -0
- package/src/hooks/lib/export.ts +76 -0
- package/src/hooks/lib/inference.ts +91 -0
- package/src/hooks/lib/learning-category.ts +14 -0
- package/src/hooks/lib/log.ts +53 -0
- package/src/hooks/lib/models.ts +16 -0
- package/src/hooks/lib/paths.ts +80 -0
- package/src/hooks/lib/relationship.ts +135 -0
- package/src/hooks/lib/security.ts +122 -0
- package/src/hooks/lib/session-names.ts +247 -0
- package/src/hooks/lib/setup.ts +189 -0
- package/src/hooks/lib/signal-trends.ts +117 -0
- package/src/hooks/lib/signals.ts +37 -0
- package/src/hooks/lib/stdin.ts +18 -0
- package/src/hooks/lib/stop.ts +155 -0
- package/src/hooks/lib/time.ts +19 -0
- package/src/hooks/lib/token-usage.ts +42 -0
- package/src/hooks/lib/transcript.ts +76 -0
- package/src/hooks/lib/wisdom.ts +48 -0
- package/src/hooks/lib/work-tracking.ts +193 -0
- package/src/hooks/setup-check.ts +42 -0
- package/src/targets/claude/install.ts +145 -0
- package/src/targets/claude/uninstall.ts +101 -0
- package/src/targets/lib.ts +337 -0
- package/src/targets/opencode/install.ts +59 -0
- package/src/targets/opencode/plugin.ts +328 -0
- package/src/targets/opencode/uninstall.ts +57 -0
- package/src/tools/entity-save.ts +110 -0
- package/src/tools/export.ts +34 -0
- package/src/tools/fyzz-api.ts +104 -0
- package/src/tools/import.ts +123 -0
- package/src/tools/pattern-synthesis.ts +435 -0
- package/src/tools/pdf-download.ts +102 -0
- package/src/tools/relationship-reflect.ts +362 -0
- package/src/tools/session-summary.ts +206 -0
- package/src/tools/token-cost.ts +301 -0
- 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");
|