portable-agent-layer 0.9.0 → 0.11.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/assets/skills/{analyze-pdf.md → analyze-pdf/SKILL.md} +4 -4
- package/{src → assets/skills/analyze-pdf}/tools/pdf-download.ts +3 -3
- package/assets/skills/{analyze-youtube.md → analyze-youtube/SKILL.md} +4 -4
- package/{src → assets/skills/analyze-youtube}/tools/youtube-analyze.ts +2 -2
- package/assets/skills/{council.md → council/SKILL.md} +3 -2
- package/assets/skills/{create-skill.md → create-skill/SKILL.md} +2 -1
- package/assets/skills/{extract-entities.md → extract-entities/SKILL.md} +4 -5
- package/{src → assets/skills/extract-entities}/tools/entity-save.ts +3 -3
- package/assets/skills/{extract-wisdom.md → extract-wisdom/SKILL.md} +3 -2
- package/assets/skills/{first-principles.md → first-principles/SKILL.md} +3 -2
- package/assets/skills/{fyzz-chat-api.md → fyzz-chat-api/SKILL.md} +6 -6
- package/{src → assets/skills/fyzz-chat-api}/tools/fyzz-api.ts +6 -6
- package/assets/skills/{reflect.md → reflect/SKILL.md} +2 -1
- package/assets/skills/{research.md → research/SKILL.md} +2 -1
- package/assets/skills/{review.md → review/SKILL.md} +2 -1
- package/assets/skills/{summarize.md → summarize/SKILL.md} +3 -2
- package/assets/skills/telos/SKILL.md +60 -0
- package/assets/skills/telos/tools/update-telos.ts +101 -0
- package/assets/skills/think/SKILL.md +47 -0
- package/assets/templates/AGENTS.md.template +8 -37
- package/assets/templates/PAL/CONTEXT_ROUTING.md +12 -0
- package/assets/templates/PAL/MEMORY_SYSTEM.md +26 -0
- package/assets/templates/PAL/OPINION_TRACKING.md +3 -0
- package/assets/templates/PAL/STEERING_RULES.md +23 -0
- package/assets/templates/PAL/WORK_TRACKING.md +14 -0
- package/assets/templates/settings.claude.json +80 -0
- package/package.json +2 -5
- package/src/hooks/handlers/rating.ts +4 -47
- package/src/hooks/handlers/reflect-trigger.ts +83 -0
- package/src/hooks/handlers/relationship.ts +8 -5
- package/src/hooks/handlers/session-name.ts +8 -6
- package/src/hooks/handlers/work-learning.ts +1 -0
- package/src/hooks/handlers/work-session.ts +16 -3
- package/src/hooks/lib/claude-md.ts +9 -24
- package/src/hooks/lib/context.ts +31 -48
- package/src/hooks/lib/graduation.ts +6 -4
- package/src/hooks/lib/learning-store.ts +7 -117
- package/src/hooks/lib/opinions.ts +191 -0
- package/src/hooks/lib/paths.ts +2 -0
- package/src/hooks/lib/relationship.ts +5 -4
- package/src/hooks/lib/security.ts +2 -0
- package/src/hooks/lib/stop.ts +3 -0
- package/src/hooks/lib/text-similarity.ts +125 -0
- package/src/targets/claude/install.ts +16 -93
- package/src/targets/claude/uninstall.ts +22 -47
- package/src/targets/lib.ts +190 -48
- package/src/targets/opencode/install.ts +13 -2
- package/src/targets/opencode/uninstall.ts +4 -1
- package/src/tools/analyze.ts +49 -15
- package/src/tools/opinion.ts +250 -0
- package/src/tools/relationship-reflect.ts +215 -105
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PAL — Claude Code target installer (TypeScript)
|
|
3
|
-
* Merges
|
|
4
|
-
* Copies skills
|
|
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,
|
|
19
22
|
writeJson,
|
|
20
23
|
} from "../lib";
|
|
@@ -35,96 +38,13 @@ const backup = `${SETTINGS}.bak.${Date.now()}`;
|
|
|
35
38
|
copyFileSync(SETTINGS, backup);
|
|
36
39
|
log.info("Backed up settings.json");
|
|
37
40
|
|
|
38
|
-
// ---
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
};
|
|
41
|
+
// --- Load template and merge into existing settings ---
|
|
42
|
+
const template = loadSettingsTemplate(assets.claudeSettingsTemplate(), PKG_ROOT);
|
|
43
|
+
const existing = readJson<Record<string, unknown>>(SETTINGS, {});
|
|
44
|
+
const merged = mergeSettings(existing, template);
|
|
90
45
|
|
|
91
|
-
|
|
92
|
-
|
|
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");
|
|
46
|
+
writeJson(SETTINGS, merged);
|
|
47
|
+
log.success("Merged PAL settings into settings.json");
|
|
128
48
|
|
|
129
49
|
// --- Copy skills ---
|
|
130
50
|
const skillsDir = resolve(CLAUDE_DIR, "skills");
|
|
@@ -133,13 +53,16 @@ copySkills(skillsDir);
|
|
|
133
53
|
// --- Copy agents ---
|
|
134
54
|
copyAgents();
|
|
135
55
|
|
|
56
|
+
// --- Copy PAL system docs ---
|
|
57
|
+
const palDocsCount = copyPalDocs();
|
|
58
|
+
log.success(`Installed ${palDocsCount} PAL docs to ~/.agents/PAL/`);
|
|
59
|
+
|
|
136
60
|
// --- Generate ~/.claude/AGENTS.md and symlink ~/.claude/CLAUDE.md → AGENTS.md ---
|
|
137
61
|
regenerateIfNeeded();
|
|
138
62
|
log.success("Generated ~/.config/opencode/AGENTS.md (→ ~/.claude/CLAUDE.md symlink)");
|
|
139
63
|
|
|
140
64
|
log.success("Claude Code installation complete");
|
|
141
65
|
console.log("");
|
|
142
|
-
log.info(`Hooks: 5 (SessionStart, UserPromptSubmit, PreToolUse×2, Stop)`);
|
|
143
66
|
log.info(`Skills: ${countSkills()}`);
|
|
144
67
|
log.info(`Agents: ${countAgents()}`);
|
|
145
68
|
log.info(`TELOS: ${countMd(resolve(palHome(), "telos"))} files`);
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PAL — Claude Code uninstaller (TypeScript)
|
|
3
|
-
* Removes
|
|
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 {
|
|
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
|
-
// ---
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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");
|
package/src/targets/lib.ts
CHANGED
|
@@ -41,6 +41,101 @@ export function writeJson(path: string, data: unknown): void {
|
|
|
41
41
|
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// --- Settings template merge/unmerge ---
|
|
45
|
+
|
|
46
|
+
type HookEntry = { matcher?: string; hooks?: Array<{ type: string; command: string }> };
|
|
47
|
+
type Settings = Record<string, unknown> & {
|
|
48
|
+
hooks?: Record<string, HookEntry[]>;
|
|
49
|
+
permissions?: { allow?: string[]; deny?: string[]; ask?: string[] };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load a settings template, replacing {{PKG_ROOT}} with the actual path.
|
|
54
|
+
*/
|
|
55
|
+
export function loadSettingsTemplate(templatePath: string, pkgRoot: string): Settings {
|
|
56
|
+
const raw = readFileSync(templatePath, "utf-8");
|
|
57
|
+
const resolved = raw.replaceAll("{{PKG_ROOT}}", pkgRoot);
|
|
58
|
+
return JSON.parse(resolved) as Settings;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Merge a PAL settings template into existing settings.
|
|
63
|
+
* - hooks: deduplicate by command string
|
|
64
|
+
* - permissions.allow: deduplicate by value
|
|
65
|
+
* - other keys: template values are added if not already present
|
|
66
|
+
*/
|
|
67
|
+
export function mergeSettings(existing: Settings, template: Settings): Settings {
|
|
68
|
+
const result = { ...existing };
|
|
69
|
+
|
|
70
|
+
// Merge hooks (deduplicate by command)
|
|
71
|
+
if (template.hooks) {
|
|
72
|
+
if (!result.hooks) result.hooks = {};
|
|
73
|
+
for (const [event, entries] of Object.entries(template.hooks)) {
|
|
74
|
+
const current = result.hooks[event] ?? [];
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const cmd = entry.hooks?.[0]?.command;
|
|
77
|
+
if (cmd && !current.some((e) => e.hooks?.[0]?.command === cmd)) {
|
|
78
|
+
current.push(entry);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
result.hooks[event] = current;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Merge permissions.allow (deduplicate)
|
|
86
|
+
if (template.permissions?.allow) {
|
|
87
|
+
if (!result.permissions) result.permissions = {};
|
|
88
|
+
if (!result.permissions.allow) result.permissions.allow = [];
|
|
89
|
+
for (const perm of template.permissions.allow) {
|
|
90
|
+
if (!result.permissions.allow.includes(perm)) {
|
|
91
|
+
result.permissions.allow.push(perm);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Remove everything a PAL settings template added from existing settings.
|
|
101
|
+
* - hooks: remove entries whose command matches any template command
|
|
102
|
+
* - permissions.allow: remove entries that appear in the template
|
|
103
|
+
* - cleans up empty arrays/objects
|
|
104
|
+
*/
|
|
105
|
+
export function unmergeSettings(existing: Settings, template: Settings): Settings {
|
|
106
|
+
const result = { ...existing };
|
|
107
|
+
|
|
108
|
+
// Collect all PAL hook commands from template
|
|
109
|
+
if (template.hooks && result.hooks) {
|
|
110
|
+
const palCommands = new Set<string>();
|
|
111
|
+
for (const entries of Object.values(template.hooks)) {
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
const cmd = entry.hooks?.[0]?.command;
|
|
114
|
+
if (cmd) palCommands.add(cmd);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const [event, entries] of Object.entries(result.hooks)) {
|
|
119
|
+
result.hooks[event] = entries.filter((e) => {
|
|
120
|
+
const cmd = e.hooks?.[0]?.command;
|
|
121
|
+
return !cmd || !palCommands.has(cmd);
|
|
122
|
+
});
|
|
123
|
+
if (result.hooks[event].length === 0) delete result.hooks[event];
|
|
124
|
+
}
|
|
125
|
+
if (Object.keys(result.hooks).length === 0) delete result.hooks;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Remove PAL permissions
|
|
129
|
+
if (template.permissions?.allow && result.permissions?.allow) {
|
|
130
|
+
const palPerms = new Set(template.permissions.allow);
|
|
131
|
+
result.permissions.allow = result.permissions.allow.filter((p) => !palPerms.has(p));
|
|
132
|
+
if (result.permissions.allow.length === 0) delete result.permissions.allow;
|
|
133
|
+
if (Object.keys(result.permissions).length === 0) delete result.permissions;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
44
139
|
// --- TELOS scaffolding ---
|
|
45
140
|
|
|
46
141
|
/** Copy template files into telos/ without overwriting existing ones */
|
|
@@ -60,14 +155,65 @@ export function scaffoldTelos(): void {
|
|
|
60
155
|
}
|
|
61
156
|
}
|
|
62
157
|
|
|
158
|
+
// --- PAL docs (modular context routing files) ---
|
|
159
|
+
|
|
160
|
+
const PAL_DOCS_DIR = resolve(platform.agentsDir(), "PAL");
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Install PAL system docs into ~/.agents/PAL/.
|
|
164
|
+
* Always overwrites — these are engine-managed, not user-editable.
|
|
165
|
+
*/
|
|
166
|
+
export function copyPalDocs(): number {
|
|
167
|
+
const srcDir = assets.palDocs();
|
|
168
|
+
if (!existsSync(srcDir)) return 0;
|
|
169
|
+
|
|
170
|
+
mkdirSync(PAL_DOCS_DIR, { recursive: true });
|
|
171
|
+
let count = 0;
|
|
172
|
+
|
|
173
|
+
for (const file of readdirSync(srcDir).filter((f) => f.endsWith(".md"))) {
|
|
174
|
+
const src = resolve(srcDir, file);
|
|
175
|
+
const dst = resolve(PAL_DOCS_DIR, file);
|
|
176
|
+
copyFileSync(src, dst);
|
|
177
|
+
count++;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Symlink ~/.agents/PAL/telos and ~/.agents/PAL/memory → <palHome>/...
|
|
181
|
+
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
182
|
+
ensureSymlink(resolve(PAL_DOCS_DIR, "telos"), resolve(palHome(), "telos"), linkType);
|
|
183
|
+
ensureSymlink(resolve(PAL_DOCS_DIR, "memory"), resolve(palHome(), "memory"), linkType);
|
|
184
|
+
|
|
185
|
+
return count;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Remove PAL system docs from ~/.agents/PAL/ */
|
|
189
|
+
export function removePalDocs(): void {
|
|
190
|
+
if (!existsSync(PAL_DOCS_DIR)) return;
|
|
191
|
+
for (const file of readdirSync(PAL_DOCS_DIR).filter((f) => f.endsWith(".md"))) {
|
|
192
|
+
try {
|
|
193
|
+
unlinkSync(resolve(PAL_DOCS_DIR, file));
|
|
194
|
+
} catch {
|
|
195
|
+
/* gone */
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
rmSync(PAL_DOCS_DIR, { recursive: true });
|
|
200
|
+
log.info("Removed ~/.agents/PAL/");
|
|
201
|
+
} catch {
|
|
202
|
+
/* gone */
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
63
206
|
// --- Skills ---
|
|
64
207
|
|
|
65
208
|
const AGENTS_SKILLS_DIR = resolve(platform.agentsDir(), "skills");
|
|
66
209
|
|
|
67
210
|
/**
|
|
68
|
-
* Install PAL skills
|
|
69
|
-
*
|
|
70
|
-
*
|
|
211
|
+
* Install PAL skills by symlinking:
|
|
212
|
+
* ~/.agents/skills/<name> → <repo>/assets/skills/<name> (source of truth)
|
|
213
|
+
* ~/.claude/skills/<name> → ~/.agents/skills/<name> (Claude Code discovery)
|
|
214
|
+
*
|
|
215
|
+
* Symlinks mean tools inside skills can import from the repo (src/hooks/lib/*)
|
|
216
|
+
* and everything resolves naturally. Additive — skips skills already installed.
|
|
71
217
|
*/
|
|
72
218
|
export function copySkills(claudeSkillsDir: string): number {
|
|
73
219
|
const skillsDir = assets.skills();
|
|
@@ -75,69 +221,65 @@ export function copySkills(claudeSkillsDir: string): number {
|
|
|
75
221
|
|
|
76
222
|
mkdirSync(AGENTS_SKILLS_DIR, { recursive: true });
|
|
77
223
|
mkdirSync(claudeSkillsDir, { recursive: true });
|
|
224
|
+
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
78
225
|
let count = 0;
|
|
79
226
|
|
|
80
|
-
for (const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
227
|
+
for (const name of readdirSync(skillsDir)) {
|
|
228
|
+
const srcDir = resolve(skillsDir, name);
|
|
229
|
+
if (!existsSync(resolve(srcDir, "SKILL.md"))) continue;
|
|
230
|
+
|
|
231
|
+
// ~/.agents/skills/<name> → <repo>/assets/skills/<name>
|
|
232
|
+
const agentLink = resolve(AGENTS_SKILLS_DIR, name);
|
|
233
|
+
ensureSymlink(agentLink, srcDir, linkType);
|
|
234
|
+
|
|
235
|
+
// ~/.claude/skills/<name> → ~/.agents/skills/<name>
|
|
85
236
|
const claudeLink = resolve(claudeSkillsDir, name);
|
|
237
|
+
ensureSymlink(claudeLink, agentLink, linkType);
|
|
86
238
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
count++;
|
|
93
|
-
} else {
|
|
94
|
-
log.warn(`Skill exists, skipping: ${name}`);
|
|
95
|
-
}
|
|
239
|
+
log.info(`Linked skill: ${name}`);
|
|
240
|
+
count++;
|
|
241
|
+
}
|
|
242
|
+
return count;
|
|
243
|
+
}
|
|
96
244
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
245
|
+
/** Create or update a symlink/junction, replacing any non-symlink entry. */
|
|
246
|
+
function ensureSymlink(link: string, target: string, type: "dir" | "junction"): void {
|
|
247
|
+
try {
|
|
248
|
+
const st = lstatSync(link);
|
|
249
|
+
if (st.isSymbolicLink()) return; // already a symlink, leave it
|
|
250
|
+
rmSync(link, { recursive: true, force: true });
|
|
251
|
+
} catch {
|
|
252
|
+
// doesn't exist or broken — clean up just in case
|
|
100
253
|
try {
|
|
101
|
-
|
|
102
|
-
if (!st.isSymbolicLink()) {
|
|
103
|
-
rmSync(claudeLink, { recursive: true, force: true });
|
|
104
|
-
symlinkSync(`../../.agents/skills/${name}`, claudeLink, linkType);
|
|
105
|
-
}
|
|
254
|
+
rmSync(link, { recursive: true, force: true });
|
|
106
255
|
} catch {
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
rmSync(claudeLink, { recursive: true, force: true });
|
|
110
|
-
} catch {
|
|
111
|
-
/* gone */
|
|
112
|
-
}
|
|
113
|
-
symlinkSync(`../../.agents/skills/${name}`, claudeLink, linkType);
|
|
256
|
+
/* gone */
|
|
114
257
|
}
|
|
115
258
|
}
|
|
116
|
-
|
|
259
|
+
symlinkSync(target, link, type);
|
|
117
260
|
}
|
|
118
261
|
|
|
119
|
-
/** Remove PAL
|
|
262
|
+
/** Remove PAL skill symlinks from ~/.agents/skills/ and ~/.claude/skills/ */
|
|
120
263
|
export function removeSkills(claudeSkillsDir: string): string[] {
|
|
121
264
|
const skillsDir = assets.skills();
|
|
122
265
|
if (!existsSync(skillsDir)) return [];
|
|
123
266
|
|
|
124
267
|
const removed: string[] = [];
|
|
125
|
-
for (const
|
|
126
|
-
|
|
268
|
+
for (const name of readdirSync(skillsDir)) {
|
|
269
|
+
if (!existsSync(resolve(skillsDir, name, "SKILL.md"))) continue;
|
|
127
270
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
unlinkSync(claudeLink);
|
|
138
|
-
} catch {
|
|
139
|
-
/* already gone */
|
|
271
|
+
for (const link of [
|
|
272
|
+
resolve(AGENTS_SKILLS_DIR, name),
|
|
273
|
+
resolve(claudeSkillsDir, name),
|
|
274
|
+
]) {
|
|
275
|
+
try {
|
|
276
|
+
unlinkSync(link);
|
|
277
|
+
} catch {
|
|
278
|
+
/* already gone */
|
|
279
|
+
}
|
|
140
280
|
}
|
|
281
|
+
removed.push(name);
|
|
282
|
+
log.info(`Removed skill: ${name}`);
|
|
141
283
|
}
|
|
142
284
|
return removed;
|
|
143
285
|
}
|
|
@@ -7,7 +7,14 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "
|
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
8
|
import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
|
|
9
9
|
import { palPkg, platform } from "../../hooks/lib/paths";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
copyAgentsForOpencode,
|
|
12
|
+
copyPalDocs,
|
|
13
|
+
copySkills,
|
|
14
|
+
countSkills,
|
|
15
|
+
log,
|
|
16
|
+
writeJson,
|
|
17
|
+
} from "../lib";
|
|
11
18
|
|
|
12
19
|
const PKG_ROOT = palPkg();
|
|
13
20
|
const OC_GLOBAL_DIR = platform.opencodeDir();
|
|
@@ -53,7 +60,11 @@ log.success("Installed skills to ~/.agents/skills/");
|
|
|
53
60
|
const ocAgentsDir = resolve(OC_GLOBAL_DIR, "agents");
|
|
54
61
|
copyAgentsForOpencode(ocAgentsDir);
|
|
55
62
|
|
|
56
|
-
// --- 5.
|
|
63
|
+
// --- 5. Copy PAL system docs ---
|
|
64
|
+
const palDocsCount = copyPalDocs();
|
|
65
|
+
log.success(`Installed ${palDocsCount} PAL docs to ~/.agents/PAL/`);
|
|
66
|
+
|
|
67
|
+
// --- 6. Generate ~/.config/opencode/AGENTS.md ---
|
|
57
68
|
regenerateIfNeeded();
|
|
58
69
|
log.success("Generated ~/.config/opencode/AGENTS.md");
|
|
59
70
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { unlinkSync } from "node:fs";
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
8
|
import { platform } from "../../hooks/lib/paths";
|
|
9
|
-
import { log, removeAgentsFromOpencode, removeSkills } from "../lib";
|
|
9
|
+
import { log, removeAgentsFromOpencode, removePalDocs, removeSkills } from "../lib";
|
|
10
10
|
|
|
11
11
|
const OC_GLOBAL_DIR = platform.opencodeDir() || "";
|
|
12
12
|
|
|
@@ -38,6 +38,9 @@ if (removedAgents.length > 0)
|
|
|
38
38
|
`Removed ${removedAgents.length} opencode agent(s): ${removedAgents.join(", ")}`
|
|
39
39
|
);
|
|
40
40
|
|
|
41
|
+
// --- Remove PAL system docs ---
|
|
42
|
+
removePalDocs();
|
|
43
|
+
|
|
41
44
|
// --- Remove AGENTS.md and CLAUDE.md symlink ---
|
|
42
45
|
const agentsMd = resolve(OC_GLOBAL_DIR, "AGENTS.md");
|
|
43
46
|
const claudeMd = resolve(PAL_CLAUDE_DIR, "CLAUDE.md");
|