portable-agent-layer 0.10.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.
Files changed (36) hide show
  1. package/assets/skills/{analyze-pdf.md → analyze-pdf/SKILL.md} +4 -4
  2. package/{src → assets/skills/analyze-pdf}/tools/pdf-download.ts +3 -3
  3. package/assets/skills/{analyze-youtube.md → analyze-youtube/SKILL.md} +4 -4
  4. package/{src → assets/skills/analyze-youtube}/tools/youtube-analyze.ts +2 -2
  5. package/assets/skills/{council.md → council/SKILL.md} +3 -2
  6. package/assets/skills/{create-skill.md → create-skill/SKILL.md} +2 -1
  7. package/assets/skills/{extract-entities.md → extract-entities/SKILL.md} +4 -5
  8. package/{src → assets/skills/extract-entities}/tools/entity-save.ts +3 -3
  9. package/assets/skills/{extract-wisdom.md → extract-wisdom/SKILL.md} +3 -2
  10. package/assets/skills/{first-principles.md → first-principles/SKILL.md} +3 -2
  11. package/assets/skills/{fyzz-chat-api.md → fyzz-chat-api/SKILL.md} +6 -6
  12. package/{src → assets/skills/fyzz-chat-api}/tools/fyzz-api.ts +6 -6
  13. package/assets/skills/{reflect.md → reflect/SKILL.md} +2 -1
  14. package/assets/skills/{research.md → research/SKILL.md} +2 -1
  15. package/assets/skills/{review.md → review/SKILL.md} +2 -1
  16. package/assets/skills/{summarize.md → summarize/SKILL.md} +3 -2
  17. package/assets/skills/telos/SKILL.md +60 -0
  18. package/assets/skills/telos/tools/update-telos.ts +101 -0
  19. package/assets/skills/think/SKILL.md +47 -0
  20. package/assets/templates/AGENTS.md.template +8 -43
  21. package/assets/templates/PAL/CONTEXT_ROUTING.md +12 -0
  22. package/assets/templates/PAL/MEMORY_SYSTEM.md +26 -0
  23. package/assets/templates/PAL/OPINION_TRACKING.md +3 -0
  24. package/assets/templates/{STEERING-RULES.md → PAL/STEERING_RULES.md} +1 -1
  25. package/assets/templates/PAL/WORK_TRACKING.md +14 -0
  26. package/assets/templates/settings.claude.json +80 -0
  27. package/package.json +1 -5
  28. package/src/hooks/lib/claude-md.ts +10 -35
  29. package/src/hooks/lib/context.ts +0 -27
  30. package/src/hooks/lib/paths.ts +2 -0
  31. package/src/hooks/lib/security.ts +1 -0
  32. package/src/targets/claude/install.ts +16 -93
  33. package/src/targets/claude/uninstall.ts +22 -47
  34. package/src/targets/lib.ts +190 -48
  35. package/src/targets/opencode/install.ts +13 -2
  36. package/src/targets/opencode/uninstall.ts +4 -1
@@ -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 into the shared ~/.agents/skills/<name>/SKILL.md standard,
69
- * then symlink ~/.claude/skills/<name> → ../../.agents/skills/<name>.
70
- * Additive — skips skills already installed.
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 file of readdirSync(skillsDir).filter((f) => f.endsWith(".md"))) {
81
- const name = file.replace(/\.md$/, "");
82
- const src = resolve(skillsDir, file);
83
- const agentSkillDir = resolve(AGENTS_SKILLS_DIR, name);
84
- const agentSkillFile = resolve(agentSkillDir, "SKILL.md");
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
- // Install into ~/.agents/skills/<name>/SKILL.md
88
- if (!existsSync(agentSkillFile)) {
89
- mkdirSync(agentSkillDir, { recursive: true });
90
- copyFileSync(src, agentSkillFile);
91
- log.info(`Added skill: ${name}`);
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
- // Create ~/.claude/skills/<name> symlink if missing or not a symlink
98
- // Use 'junction' on Windows (no admin required), 'dir' symlink on Unix
99
- const linkType = process.platform === "win32" ? "junction" : "dir";
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
- const st = lstatSync(claudeLink);
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
- // Entry might exist but lstatSync failed (broken symlink/junction on Windows)
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
- return count;
259
+ symlinkSync(target, link, type);
117
260
  }
118
261
 
119
- /** Remove PAL skills from ~/.agents/skills/ and their symlinks from ~/.claude/skills/ */
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 file of readdirSync(skillsDir).filter((f) => f.endsWith(".md"))) {
126
- const name = file.replace(/\.md$/, "");
268
+ for (const name of readdirSync(skillsDir)) {
269
+ if (!existsSync(resolve(skillsDir, name, "SKILL.md"))) continue;
127
270
 
128
- const agentSkillDir = resolve(AGENTS_SKILLS_DIR, name);
129
- if (existsSync(agentSkillDir)) {
130
- rmSync(agentSkillDir, { recursive: true });
131
- removed.push(name);
132
- log.info(`Removed skill: ${name}`);
133
- }
134
-
135
- const claudeLink = resolve(claudeSkillsDir, name);
136
- try {
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 { copyAgentsForOpencode, copySkills, countSkills, log, writeJson } from "../lib";
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. Generate ~/.config/opencode/AGENTS.md ---
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");