portable-agent-layer 0.63.0 → 0.63.1

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,33 +4,39 @@
4
4
  "sessionStart": [
5
5
  {
6
6
  "type": "command",
7
- "bash": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts"
7
+ "bash": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts",
8
+ "powershell": "$env:PAL_AGENT='copilot'; bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts"
8
9
  }
9
10
  ],
10
11
  "userPromptSubmitted": [
11
12
  {
12
13
  "type": "command",
13
- "bash": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts"
14
+ "bash": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts",
15
+ "powershell": "$env:PAL_AGENT='copilot'; bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts"
14
16
  }
15
17
  ],
16
18
  "preToolUse": [
17
19
  {
18
20
  "type": "command",
19
- "bash": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts"
21
+ "bash": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts",
22
+ "powershell": "$env:PAL_AGENT='copilot'; bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts"
20
23
  },
21
24
  {
22
25
  "type": "command",
23
- "bash": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts"
26
+ "bash": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts",
27
+ "powershell": "$env:PAL_AGENT='copilot'; bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts"
24
28
  },
25
29
  {
26
30
  "type": "command",
27
- "bash": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/RtkWrap.ts"
31
+ "bash": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/RtkWrap.ts",
32
+ "powershell": "$env:PAL_AGENT='copilot'; bun run {{PKG_ROOT}}/src/hooks/RtkWrap.ts"
28
33
  }
29
34
  ],
30
35
  "agentStop": [
31
36
  {
32
37
  "type": "command",
33
- "bash": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts"
38
+ "bash": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts",
39
+ "powershell": "$env:PAL_AGENT='copilot'; bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts"
34
40
  }
35
41
  ]
36
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.63.0",
3
+ "version": "0.63.1",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/index.ts CHANGED
@@ -80,6 +80,20 @@ function checkTool(cmd: string, versionArgs: string[] = ["--version"]): ToolChec
80
80
  return { name: cmd, available: false };
81
81
  }
82
82
 
83
+ /**
84
+ * Copilot ships as a VS Code extension as well as a CLI, and the extension puts
85
+ * no `copilot` binary on PATH — but both read hooks, skills and agents out of
86
+ * ~/.copilot. Fall back to that directory so PAL's copilot checks still run.
87
+ */
88
+ function checkCopilot(): ToolCheck {
89
+ const cli = checkTool("copilot", ["version"]);
90
+ if (cli.available) return cli;
91
+ if (existsSync(platform.copilotDir())) {
92
+ return { name: "copilot", available: true, version: "~/.copilot (no CLI on PATH)" };
93
+ }
94
+ return cli;
95
+ }
96
+
83
97
  function detectAgent(): string | null {
84
98
  if (checkTool("claude").available) return "claude";
85
99
  if (checkTool("opencode").available) return "opencode";
@@ -443,13 +457,18 @@ function checkCopilotInstructionsPresent(): boolean {
443
457
 
444
458
  // ── Install integrity (Tier 2 doctor checks) ──
445
459
 
446
- /** Recursively collect every `command` or `bash` field value in a hook-config JSON. */
460
+ /** Hook-config keys that carry a shell command: cross-platform and per-shell variants. */
461
+ function isHookCommandField(key: string): boolean {
462
+ return key === "command" || key === "bash" || key === "powershell";
463
+ }
464
+
465
+ /** Recursively collect every command-carrying field value in a hook-config JSON. */
447
466
  function extractAllHookCommands(obj: unknown, out: string[] = []): string[] {
448
467
  if (Array.isArray(obj)) {
449
468
  for (const item of obj) extractAllHookCommands(item, out);
450
469
  } else if (obj && typeof obj === "object") {
451
470
  for (const [k, v] of Object.entries(obj)) {
452
- if ((k === "command" || k === "bash") && typeof v === "string") {
471
+ if (isHookCommandField(k) && typeof v === "string") {
453
472
  out.push(v);
454
473
  } else {
455
474
  extractAllHookCommands(v, out);
@@ -466,14 +485,21 @@ interface HookPrefixCheck {
466
485
  firstMissing?: string;
467
486
  }
468
487
 
469
- /** Verify every command in an installed hook file starts with `PAL_AGENT=<agent>`. */
488
+ /** True when a hook command sets PAL_AGENT up front, in POSIX or PowerShell syntax. */
489
+ function setsAgentEnvPrefix(cmd: string, agentName: string): boolean {
490
+ return (
491
+ cmd.startsWith(`PAL_AGENT=${agentName} `) ||
492
+ cmd.startsWith(`$env:PAL_AGENT='${agentName}'; `)
493
+ );
494
+ }
495
+
496
+ /** Verify every command in an installed hook file sets `PAL_AGENT=<agent>` first. */
470
497
  function checkAgentHookPrefix(filePath: string, agentName: string): HookPrefixCheck {
471
498
  if (!existsSync(filePath)) return { ok: false, total: 0, missing: 0 };
472
499
  try {
473
500
  const data = JSON.parse(readFileSync(filePath, "utf-8"));
474
501
  const commands = extractAllHookCommands(data.hooks ?? data);
475
- const prefix = `PAL_AGENT=${agentName} `;
476
- const missing = commands.filter((c) => !c.startsWith(prefix));
502
+ const missing = commands.filter((c) => !setsAgentEnvPrefix(c, agentName));
477
503
  return {
478
504
  ok: commands.length > 0 && missing.length === 0,
479
505
  total: commands.length,
@@ -701,7 +727,7 @@ function doctor(silent = false): DoctorResult {
701
727
  const claude = checkTool("claude");
702
728
  const opencode = checkTool("opencode");
703
729
  const cursor = checkTool("cursor");
704
- const copilot = checkTool("copilot", ["version"]);
730
+ const copilot = checkCopilot();
705
731
  const codex = checkTool("codex");
706
732
  const rtk = checkTool("rtk");
707
733
  const hasAgent =
@@ -7,11 +7,11 @@
7
7
  import { copyFileSync, existsSync, lstatSync, readlinkSync, unlinkSync } from "node:fs";
8
8
  import { resolve } from "node:path";
9
9
  import { platform } from "../../hooks/lib/paths";
10
- import { copilotFilename, getSemiStaticSources } from "../../hooks/lib/semi-static";
11
10
  import {
12
11
  log,
13
12
  readJson,
14
13
  removeAgentsFromCopilot,
14
+ removePalContextFiles,
15
15
  removePalDocs,
16
16
  removeSkills,
17
17
  vscodeSettingsFile,
@@ -49,20 +49,13 @@ if (removedAgents.length > 0) {
49
49
  removePalDocs();
50
50
 
51
51
  // --- Remove ~/.copilot/instructions/pal-*.instructions.md ---
52
- for (const src of getSemiStaticSources()) {
53
- try {
54
- unlinkSync(resolve(COPILOT_DIR, "instructions", copilotFilename(src)));
55
- } catch {
56
- /* gone */
57
- }
58
- }
59
- // pal-session.instructions.md is written by LoadContext (not a semi-static source)
60
- try {
61
- unlinkSync(resolve(COPILOT_DIR, "instructions", "pal-session.instructions.md"));
62
- } catch {
63
- /* gone */
64
- }
65
- log.success("Removed ~/.copilot/instructions/pal-*.instructions.md");
52
+ const removedInstructions = removePalContextFiles(
53
+ resolve(COPILOT_DIR, "instructions"),
54
+ ".instructions.md"
55
+ );
56
+ log.success(
57
+ `Removed ${removedInstructions.length} ~/.copilot/instructions/pal-*.instructions.md`
58
+ );
66
59
 
67
60
  // --- Backward compat: remove old copilot-instructions.md symlink if present ---
68
61
  const legacyPath = resolve(COPILOT_DIR, "copilot-instructions.md");
@@ -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");
@@ -838,6 +838,29 @@ function ensureSymlink(link: string, target: string, type: "dir" | "junction"):
838
838
  symlinkSync(target, link, type);
839
839
  }
840
840
 
841
+ /**
842
+ * Remove every `pal-*` context file with the given suffix from a directory.
843
+ *
844
+ * Globs rather than iterating getSemiStaticSources(): a slug retired from that
845
+ * registry keeps its already-written file on disk, and a registry-driven delete
846
+ * can no longer name it — agents then keep loading retired context forever.
847
+ * Also catches the legacy pre-split filenames without needing a special case.
848
+ */
849
+ export function removePalContextFiles(dir: string, suffix: string): string[] {
850
+ if (!existsSync(dir)) return [];
851
+ const removed: string[] = [];
852
+ for (const file of readdirSync(dir)) {
853
+ if (!file.startsWith("pal-") || !file.endsWith(suffix)) continue;
854
+ try {
855
+ unlinkSync(resolve(dir, file));
856
+ removed.push(file);
857
+ } catch {
858
+ /* gone or not ours to remove */
859
+ }
860
+ }
861
+ return removed;
862
+ }
863
+
841
864
  /** Remove PAL skill symlinks from ~/.pal/skills/ and ~/.claude/skills/ */
842
865
  export function removeSkills(claudeSkillsDir: string): string[] {
843
866
  const skillsDir = assets.skills();