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
@@ -0,0 +1,14 @@
1
+ # Work Tracking
2
+
3
+ PAL tracks your work across sessions in `memory/state/sessions.json` (auto-captured) and `memory/state/projects.json` (AI-managed).
4
+
5
+ ## Projects
6
+
7
+ Update `projects.json` via the work-tracking library when:
8
+ - **Starting sustained multi-session work** → create a project with objectives and an id (slugified, e.g. "pdf-template-engine")
9
+ - **Making a key decision** → add to the project's `decisions` array
10
+ - **Completing a milestone** → add to `completed`, remove from `nextSteps`
11
+ - **Session ends with open work** → update `nextSteps` and `handoff`
12
+ - **Work is done** → set status to "completed"
13
+
14
+ Do not create projects for one-off questions or quick fixes.
@@ -0,0 +1,80 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Read",
5
+ "Grep",
6
+ "Glob",
7
+ "WebFetch",
8
+ "WebSearch",
9
+ "Bash(cat *)",
10
+ "Bash(head *)",
11
+ "Bash(tail *)",
12
+ "Bash(ls *)",
13
+ "Bash(find *)",
14
+ "Bash(grep *)",
15
+ "Bash(rg *)",
16
+ "Bash(wc *)",
17
+ "Bash(diff *)",
18
+ "Bash(which *)",
19
+ "Bash(file *)",
20
+ "Bash(stat *)",
21
+ "Bash(readlink *)",
22
+ "Bash(bun ~/.agents/skills/*/tools/*.ts *)"
23
+ ]
24
+ },
25
+ "hooks": {
26
+ "SessionStart": [
27
+ {
28
+ "matcher": "",
29
+ "hooks": [
30
+ {
31
+ "type": "command",
32
+ "command": "bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts"
33
+ }
34
+ ]
35
+ }
36
+ ],
37
+ "UserPromptSubmit": [
38
+ {
39
+ "matcher": "",
40
+ "hooks": [
41
+ {
42
+ "type": "command",
43
+ "command": "bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts"
44
+ }
45
+ ]
46
+ }
47
+ ],
48
+ "PreToolUse": [
49
+ {
50
+ "matcher": "Bash|Write|Edit",
51
+ "hooks": [
52
+ {
53
+ "type": "command",
54
+ "command": "bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts"
55
+ }
56
+ ]
57
+ },
58
+ {
59
+ "matcher": "Skill",
60
+ "hooks": [
61
+ {
62
+ "type": "command",
63
+ "command": "bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts"
64
+ }
65
+ ]
66
+ }
67
+ ],
68
+ "Stop": [
69
+ {
70
+ "matcher": "",
71
+ "hooks": [
72
+ {
73
+ "type": "command",
74
+ "command": "bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts"
75
+ }
76
+ ]
77
+ }
78
+ ]
79
+ }
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,10 +44,6 @@
44
44
  "prepare": "husky",
45
45
  "install:all": "bun run src/cli/install.ts",
46
46
  "uninstall": "bun run src/cli/uninstall.ts",
47
- "ai:entity-save": "bun run src/tools/entity-save.ts",
48
- "ai:fyzz-api": "bun run src/tools/fyzz-api.ts",
49
- "ai:pdf-download": "bun run src/tools/pdf-download.ts",
50
- "ai:youtube-analyze": "bun run src/tools/youtube-analyze.ts",
51
47
  "tool:analyze": "bun run src/tools/analyze.ts",
52
48
  "tool:opinion": "bun run src/tools/opinion.ts",
53
49
  "tool:reflect": "bun run src/tools/relationship-reflect.ts",
@@ -17,8 +17,7 @@ import {
17
17
  writeFileSync,
18
18
  } from "node:fs";
19
19
  import { dirname, relative, resolve } from "node:path";
20
- import { loadTelos } from "./context";
21
- import { assets, ensureDir, palHome, paths, platform } from "./paths";
20
+ import { assets, ensureDir, paths, platform } from "./paths";
22
21
  import { buildSetupPrompt, readSetupState } from "./setup";
23
22
 
24
23
  const TEMPLATE_PATH = assets.agentsMdTemplate();
@@ -71,54 +70,30 @@ export function needsRebuild(): boolean {
71
70
 
72
71
  const outputMtime = statSync(outputPath).mtimeMs;
73
72
 
74
- // Collect source files: template + setup.json + all telos/*.md
75
- const sources: string[] = [
76
- TEMPLATE_PATH,
77
- resolve(dirname(TEMPLATE_PATH), "STEERING-RULES.md"),
78
- resolve(paths.state(), "setup.json"),
79
- ];
73
+ // Collect source files: template + setup.json + PAL docs
74
+ const sources: string[] = [TEMPLATE_PATH, resolve(paths.state(), "setup.json")];
80
75
 
81
- const telosDir = paths.telos();
82
- if (existsSync(telosDir)) {
83
- for (const f of readdirSync(telosDir).filter((f) => f.endsWith(".md"))) {
84
- sources.push(resolve(telosDir, f));
76
+ // Track PAL doc sources for rebuild detection
77
+ const palDocsDir = assets.palDocs();
78
+ if (existsSync(palDocsDir)) {
79
+ for (const f of readdirSync(palDocsDir).filter((f) => f.endsWith(".md"))) {
80
+ sources.push(resolve(palDocsDir, f));
85
81
  }
86
82
  }
87
83
 
88
84
  return latestMtime(...sources) > outputMtime;
89
85
  }
90
86
 
91
- function memoryPaths(): string {
92
- const mem = resolve(palHome(), "memory");
93
- return [
94
- `- **Wisdom frames**: \`${resolve(mem, "wisdom", "frames")}/\` — crystallized principles per domain (loaded every session)`,
95
- `- **Relationship notes**: \`${resolve(mem, "relationship")}/YYYY-MM/YYYY-MM-DD.md\` — daily interaction observations (loaded every session)`,
96
- `- **Session learnings**: \`${resolve(mem, "learning", "session")}/YYYY-MM/*.md\` — reusable insights from sessions (loaded every session)`,
97
- `- **Failure captures**: \`${resolve(mem, "learning", "failures")}/YYYY-MM/{timestamp}_{slug}/capture.md\` — what went wrong and why`,
98
- `- **Signals**: \`${resolve(mem, "signals")}/ratings.jsonl\` — append-only rating signal log (do not edit directly)`,
99
- ].join("\n");
100
- }
101
-
102
87
  /** Render AGENTS.md from the template using current state */
103
88
  export function buildClaudeMd(): string {
104
89
  const template = existsSync(TEMPLATE_PATH)
105
90
  ? readFileSync(TEMPLATE_PATH, "utf-8")
106
- : "# PAL Context\n\n{{SETUP_PROMPT}}\n{{TELOS}}\n## Memory\n\n{{MEMORY_PATHS}}\n";
91
+ : "# PAL Context\n\n{{SETUP_PROMPT}}\n";
107
92
 
108
93
  const state = readSetupState();
109
94
  const setupPrompt = state ? buildSetupPrompt(state) : null;
110
- const telos = loadTelos();
111
-
112
- const steeringPath = resolve(dirname(TEMPLATE_PATH), "STEERING-RULES.md");
113
- const steeringRules = existsSync(steeringPath)
114
- ? readFileSync(steeringPath, "utf-8").trim()
115
- : "";
116
95
 
117
- return template
118
- .replace("{{SETUP_PROMPT}}", setupPrompt ? `${setupPrompt}\n` : "")
119
- .replace("{{TELOS}}", telos ? `${telos}\n` : "")
120
- .replace("{{MEMORY_PATHS}}", memoryPaths())
121
- .replace("{{STEERING_RULES}}", steeringRules);
96
+ return template.replace("{{SETUP_PROMPT}}", setupPrompt ? `${setupPrompt}\n` : "");
122
97
  }
123
98
 
124
99
  /** Regenerate AGENTS.md if any source file is newer, and ensure CLAUDE.md symlink exists. Returns true if rebuilt. */
@@ -21,33 +21,6 @@ import {
21
21
  staleProjects,
22
22
  } from "./work-tracking";
23
23
 
24
- /** Load all populated TELOS files as a single markdown string */
25
- export function loadTelos(): string {
26
- const telosDir = paths.telos();
27
- if (!existsSync(telosDir)) return "";
28
-
29
- const files = readdirSync(telosDir)
30
- .filter((f) => f.endsWith(".md"))
31
- .sort();
32
-
33
- const sections: string[] = [];
34
-
35
- for (const file of files) {
36
- const content = readFileSync(resolve(telosDir, file), "utf-8").trim();
37
- // Skip empty templates (only have a heading and comment)
38
- const realLines = content
39
- .split("\n")
40
- .filter(
41
- (l) =>
42
- !l.startsWith("#") && !l.startsWith("<!--") && !l.startsWith("-->") && l.trim()
43
- );
44
- if (realLines.length === 0) continue;
45
- sections.push(content);
46
- }
47
-
48
- return sections.join("\n\n---\n\n");
49
- }
50
-
51
24
  /** Count lines in a signals JSONL file */
52
25
  export function countSignals(filename: string): number {
53
26
  const filepath = resolve(paths.signals(), filename);
@@ -77,4 +77,6 @@ export const assets = {
77
77
  hooks: () => pkg("src", "hooks"),
78
78
  telosTemplates: () => pkg("assets", "templates", "telos"),
79
79
  agentsMdTemplate: () => pkg("assets", "templates", "AGENTS.md.template"),
80
+ claudeSettingsTemplate: () => pkg("assets", "templates", "settings.claude.json"),
81
+ palDocs: () => pkg("assets", "templates", "PAL"),
80
82
  } as const;
@@ -43,6 +43,7 @@ export const HOOK_MANAGED_DIRS = [
43
43
  "memory/learning/synthesis",
44
44
  "memory/relationship",
45
45
  "memory/wisdom/state",
46
+ ".agents/PAL",
46
47
  ];
47
48
 
48
49
  /** Escape a string for use in a RegExp */
@@ -1,20 +1,23 @@
1
1
  /**
2
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.
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
- // --- 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
- };
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
- // --- 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");
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 PAL hooks, skills, and env from settings.json.
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 { log, readJson, removeAgents, removeSkills, writeJson } from "../lib";
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
- // --- 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> };
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
- 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");
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");