portable-agent-layer 0.63.0 → 0.63.2

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.
@@ -4,15 +4,15 @@
4
4
  * Removes PAL skill symlinks.
5
5
  */
6
6
 
7
- import { copyFileSync, existsSync, unlinkSync } from "node:fs";
7
+ import { copyFileSync, existsSync } from "node:fs";
8
8
  import { resolve } from "node:path";
9
9
  import { assets, palPkg, platform } from "../../hooks/lib/paths";
10
- import { cursorFilename, getSemiStaticSources } from "../../hooks/lib/semi-static";
11
10
  import {
12
11
  loadCursorHooksTemplate,
13
12
  log,
14
13
  readJson,
15
14
  removeAgentsFromCursor,
15
+ removePalContextFiles,
16
16
  removePalDocs,
17
17
  removeSkills,
18
18
  removeStatusline,
@@ -73,19 +73,7 @@ if (existsSync(CLI_CONFIG)) {
73
73
  removeStatusline("cursor");
74
74
 
75
75
  // --- Remove ~/.cursor/rules/pal-*.mdc ---
76
- for (const src of getSemiStaticSources()) {
77
- try {
78
- unlinkSync(resolve(CURSOR_DIR, "rules", cursorFilename(src)));
79
- } catch {
80
- /* gone */
81
- }
82
- }
83
- // Backward compat: remove legacy merged file if present
84
- try {
85
- unlinkSync(resolve(CURSOR_DIR, "rules", "pal-context.mdc"));
86
- } catch {
87
- /* gone */
88
- }
89
- log.success("Removed ~/.cursor/rules/pal-*.mdc");
76
+ const removedRules = removePalContextFiles(resolve(CURSOR_DIR, "rules"), ".mdc");
77
+ log.success(`Removed ${removedRules.length} ~/.cursor/rules/pal-*.mdc`);
90
78
 
91
79
  log.success("Cursor uninstall complete");
@@ -741,6 +741,23 @@ export function removePalDocs(): void {
741
741
 
742
742
  const PAL_SKILLS_DIR = resolve(palHome(), "skills");
743
743
 
744
+ /**
745
+ * Run one step of a bulk install, naming it only when it fails.
746
+ *
747
+ * Installs handle dozens of skills and agents; a line each buries the paths,
748
+ * backups and warnings that a reader actually has to act on. Returning false
749
+ * instead of throwing also keeps one unlinkable skill from aborting the rest.
750
+ */
751
+ function reportOnlyOnFailure(label: string, install: () => void): boolean {
752
+ try {
753
+ install();
754
+ return true;
755
+ } catch (e) {
756
+ log.warn(`Could not install ${label} — ${(e as Error).message}`);
757
+ return false;
758
+ }
759
+ }
760
+
744
761
  /**
745
762
  * Install PAL skills by symlinking:
746
763
  * ~/.pal/skills/<name> → <repo>/assets/skills/<name> (source of truth)
@@ -762,16 +779,15 @@ export function copySkills(claudeSkillsDir: string): number {
762
779
  const srcDir = resolve(skillsDir, name);
763
780
  if (!existsSync(resolve(srcDir, "SKILL.md"))) continue;
764
781
 
765
- // ~/.pal/skills/<name> → <repo>/assets/skills/<name>
766
782
  const palLink = resolve(PAL_SKILLS_DIR, name);
767
- ensureSymlink(palLink, srcDir, linkType);
768
-
769
- // ~/.claude/skills/<name> → ~/.pal/skills/<name>
770
783
  const claudeLink = resolve(claudeSkillsDir, name);
771
- ensureSymlink(claudeLink, palLink, linkType);
772
-
773
- log.info(`Linked skill: ${name}`);
774
- count++;
784
+ const linked = reportOnlyOnFailure(`skill ${name}`, () => {
785
+ // ~/.pal/skills/<name> → <repo>/assets/skills/<name>
786
+ ensureSymlink(palLink, srcDir, linkType);
787
+ // ~/.claude/skills/<name> → ~/.pal/skills/<name>
788
+ ensureSymlink(claudeLink, palLink, linkType);
789
+ });
790
+ if (linked) count++;
775
791
  }
776
792
 
777
793
  // ~/.agents/skills/ → ~/.pal/skills/
@@ -838,6 +854,29 @@ function ensureSymlink(link: string, target: string, type: "dir" | "junction"):
838
854
  symlinkSync(target, link, type);
839
855
  }
840
856
 
857
+ /**
858
+ * Remove every `pal-*` context file with the given suffix from a directory.
859
+ *
860
+ * Globs rather than iterating getSemiStaticSources(): a slug retired from that
861
+ * registry keeps its already-written file on disk, and a registry-driven delete
862
+ * can no longer name it — agents then keep loading retired context forever.
863
+ * Also catches the legacy pre-split filenames without needing a special case.
864
+ */
865
+ export function removePalContextFiles(dir: string, suffix: string): string[] {
866
+ if (!existsSync(dir)) return [];
867
+ const removed: string[] = [];
868
+ for (const file of readdirSync(dir)) {
869
+ if (!file.startsWith("pal-") || !file.endsWith(suffix)) continue;
870
+ try {
871
+ unlinkSync(resolve(dir, file));
872
+ removed.push(file);
873
+ } catch {
874
+ /* gone or not ours to remove */
875
+ }
876
+ }
877
+ return removed;
878
+ }
879
+
841
880
  /** Remove PAL skill symlinks from ~/.pal/skills/ and ~/.claude/skills/ */
842
881
  export function removeSkills(claudeSkillsDir: string): string[] {
843
882
  const skillsDir = assets.skills();
@@ -977,14 +1016,16 @@ function installAgents(targetDir: string, platform: AgentPlatform): number {
977
1016
  let count = 0;
978
1017
 
979
1018
  for (const file of readdirSync(agentsDir).filter((f) => f.endsWith(".md"))) {
980
- const content = readFileSync(resolve(agentsDir, file), "utf-8");
981
- writeFileSync(
982
- resolve(targetDir, file),
983
- extractAgentForPlatform(content, platform),
984
- "utf-8"
985
- );
986
- log.info(`Installed ${platform} agent: ${file.replace(/\.md$/, "")}`);
987
- count++;
1019
+ const name = file.replace(/\.md$/, "");
1020
+ const installed = reportOnlyOnFailure(`${platform} agent ${name}`, () => {
1021
+ const content = readFileSync(resolve(agentsDir, file), "utf-8");
1022
+ writeFileSync(
1023
+ resolve(targetDir, file),
1024
+ extractAgentForPlatform(content, platform),
1025
+ "utf-8"
1026
+ );
1027
+ });
1028
+ if (installed) count++;
988
1029
  }
989
1030
  return count;
990
1031
  }
@@ -1352,7 +1393,6 @@ export function generateSkillIndex(): number {
1352
1393
  const stateDir = resolve(palHome(), "memory", "state");
1353
1394
  mkdirSync(stateDir, { recursive: true });
1354
1395
  writeJson(resolve(stateDir, "skill-index.json"), index);
1355
- log.info(`Skill index: ${index.totalSkills} skills indexed`);
1356
1396
 
1357
1397
  return index.totalSkills;
1358
1398
  }
@@ -12,18 +12,9 @@ import {
12
12
  writeFileSync,
13
13
  } from "node:fs";
14
14
  import { resolve } from "node:path";
15
- import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
16
15
  import { palPkg, platform } from "../../hooks/lib/paths";
17
16
  import { getSemiStaticSources } from "../../hooks/lib/semi-static";
18
- import {
19
- copyAgentsForOpencode,
20
- copyPalDocs,
21
- copySkills,
22
- countSkills,
23
- generateSkillIndex,
24
- log,
25
- writeJson,
26
- } from "../lib";
17
+ import { copyAgentsForOpencode, copySkills, countSkills, log, writeJson } from "../lib";
27
18
 
28
19
  const PKG_ROOT = palPkg();
29
20
  const OC_GLOBAL_DIR = platform.opencodeDir();
@@ -55,7 +46,6 @@ if (!existsSync(pkgPath)) {
55
46
 
56
47
  try {
57
48
  Bun.spawnSync(["bun", "install", "--silent"], { cwd: OC_PLUGINS_DIR });
58
- log.success("Installed plugin dependencies");
59
49
  } catch {
60
50
  log.warn(`Could not install plugin deps — run 'bun install' in ${OC_PLUGINS_DIR}`);
61
51
  }
@@ -63,22 +53,13 @@ try {
63
53
  // --- 3. Install skills into ~/.pal/skills/ ---
64
54
  const claudeSkillsDir = resolve(platform.claudeDir(), "skills");
65
55
  copySkills(claudeSkillsDir);
66
- generateSkillIndex();
67
- log.success("Installed skills to ~/.pal/skills/");
68
56
 
69
57
  // --- 4. Install agents into ~/.config/opencode/agents/ ---
70
58
  const ocAgentsDir = resolve(OC_GLOBAL_DIR, "agents");
71
- copyAgentsForOpencode(ocAgentsDir);
72
-
73
- // --- 5. Copy PAL system docs ---
74
- const palDocsCount = copyPalDocs();
75
- log.success(`Installed ${palDocsCount} PAL docs to ~/.pal/docs/`);
76
-
77
- // --- 6. Generate ~/.config/opencode/AGENTS.md ---
78
- regenerateIfNeeded();
79
- log.success("Generated ~/.config/opencode/AGENTS.md");
59
+ const agentCount = copyAgentsForOpencode(ocAgentsDir);
60
+ log.success(`${countSkills()} skills · ${agentCount} agents → ~/.config/opencode/`);
80
61
 
81
- // --- 7. Add semi-static digest files to instructions[] in config.json ---
62
+ // --- 5. Add semi-static digest files to instructions[] in config.json ---
82
63
  const configPath = resolve(OC_GLOBAL_DIR, "config.json");
83
64
  const staticFiles = getSemiStaticSources().map((s) => s.path);
84
65
  let ocConfig: Record<string, unknown> = {};
@@ -94,11 +75,3 @@ const existingInstructions = Array.isArray(ocConfig.instructions)
94
75
  : [];
95
76
  ocConfig.instructions = [...new Set([...existingInstructions, ...staticFiles])];
96
77
  writeFileSync(configPath, `${JSON.stringify(ocConfig, null, 2)}\n`, "utf-8");
97
- log.success(
98
- `Updated config.json: ${(ocConfig.instructions as string[]).length} instructions`
99
- );
100
-
101
- log.success("opencode installation complete");
102
- console.log("");
103
- log.info(`Plugin: ${pluginDst}`);
104
- log.info(`Skills: ${countSkills()} (native via ~/.pal/skills/)`);