portable-agent-layer 0.85.11 → 0.85.13

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/README.md CHANGED
@@ -51,10 +51,10 @@ bun install
51
51
  bun run install:all
52
52
  ```
53
53
 
54
- In repo mode, add an alias to your shell profile:
54
+ Then put `pal` on your PATH from the checkout. An alias is not enough: agents run skills in non-interactive shells, which never load it.
55
55
 
56
56
  ```bash
57
- alias pal="bun run ~/path/to/portable-agent-layer/src/cli/index.ts"
57
+ bun link
58
58
  ```
59
59
 
60
60
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.85.11",
3
+ "version": "0.85.13",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, resolve } from "node:path";
4
+ import { isRepoMode } from "../../hooks/handlers/update-check";
4
5
  import { palPkg } from "../../hooks/lib/paths";
5
6
  import { findBinaryOnPath } from "../../hooks/lib/which";
6
7
  import type { ToolCheck } from "./agents";
@@ -82,14 +83,29 @@ export function playwrightFinding(host: BrowserHost): Finding {
82
83
  );
83
84
  }
84
85
 
85
- function palOnPathFinding(): Finding {
86
- const pal = findBinaryOnPath("pal");
87
- return pal
88
- ? passed("pal.path", `pal on PATH — ${pal}`)
89
- : failing("pal.path", "pal is not on PATH — skills that run 'pal cli …' fail", {
90
- say: "Install PAL globally",
91
- command: "bun add -g portable-agent-layer",
92
- });
86
+ interface PalInstall {
87
+ pal: string | null;
88
+ pkg: string;
89
+ repoMode: boolean;
90
+ }
91
+
92
+ function putPalOnPath(install: PalInstall): Fix {
93
+ return install.repoMode
94
+ ? {
95
+ say: "Link the checkout — a shell alias is invisible to agents",
96
+ command: `cd ${install.pkg} && bun link`,
97
+ }
98
+ : { say: "Install PAL globally", command: "bun add -g portable-agent-layer" };
99
+ }
100
+
101
+ export function palOnPathFinding(install: PalInstall): Finding {
102
+ return install.pal
103
+ ? passed("pal.path", `pal on PATH — ${install.pal}`)
104
+ : failing(
105
+ "pal.path",
106
+ "pal is not on PATH — skills that run 'pal cli …' fail",
107
+ putPalOnPath(install)
108
+ );
93
109
  }
94
110
 
95
111
  function rtkInstall(): Fix {
@@ -115,7 +131,11 @@ function rtkFinding(rtk: ToolCheck): Finding {
115
131
  export function environmentFindings(rtk: ToolCheck): Finding[] {
116
132
  return [
117
133
  passed("bun", `Bun ${Bun.version}`),
118
- palOnPathFinding(),
134
+ palOnPathFinding({
135
+ pal: findBinaryOnPath("pal"),
136
+ pkg: palPkg(),
137
+ repoMode: isRepoMode(),
138
+ }),
119
139
  playwrightFinding({
120
140
  browsersPath: playwrightBrowsersPath(),
121
141
  playwright: installedPlaywright(),
@@ -3,7 +3,7 @@ import { cachedStatus, type UpdateCache } from "../../hooks/handlers/update-chec
3
3
  import { type HookErrorGroup, recentHookErrors } from "../../hooks/lib/log";
4
4
  import { palHome } from "../../hooks/lib/paths";
5
5
  import { checkPendingMigrations } from "../migrate";
6
- import { type Finding, passed, warning } from "./finding";
6
+ import { type Finding, failing, passed, warning } from "./finding";
7
7
 
8
8
  interface PendingMigration {
9
9
  id: string;
@@ -11,11 +11,25 @@ interface PendingMigration {
11
11
  detail?: string;
12
12
  }
13
13
 
14
+ const CLAUDE_LOGIN_EXPIRED = /Failed to authenticate/;
15
+
16
+ function claudeLoginExpiredFinding(group: HookErrorGroup): Finding {
17
+ return failing(
18
+ `hook-errors.${group.source}`,
19
+ `Background calls to Claude cannot log in — ${group.count} failed in the last 24h: ${group.last}`,
20
+ {
21
+ say: "Create a year-long token, then export it as CLAUDE_CODE_OAUTH_TOKEN in your shell profile",
22
+ command: "claude setup-token",
23
+ }
24
+ );
25
+ }
26
+
14
27
  export function hookErrorFindings(groups: HookErrorGroup[]): Finding[] {
15
28
  if (groups.length === 0)
16
29
  return [passed("hook-errors", "No hook errors in the last 24h")];
17
30
  const log = resolve(palHome(), "debug", "debug.log");
18
31
  return groups.map((group) => {
32
+ if (CLAUDE_LOGIN_EXPIRED.test(group.last)) return claudeLoginExpiredFinding(group);
19
33
  const times = group.count === 1 ? "once" : `${group.count} times`;
20
34
  return warning(
21
35
  `hook-errors.${group.source}`,
@@ -209,13 +209,43 @@ function bindingFindings(): Finding[] {
209
209
  : issues.map(bindingFinding);
210
210
  }
211
211
 
212
+ function resolves(name: string, from: string): boolean {
213
+ try {
214
+ Bun.resolveSync(`${name}/package.json`, from);
215
+ return true;
216
+ } catch {
217
+ try {
218
+ Bun.resolveSync(name, from);
219
+ return true;
220
+ } catch {
221
+ return false;
222
+ }
223
+ }
224
+ }
225
+
226
+ export function unresolvedDependencies(pkg: string): string[] {
227
+ try {
228
+ const { dependencies = {} } = JSON.parse(
229
+ readFileSync(resolve(pkg, "package.json"), "utf-8")
230
+ );
231
+ return Object.keys(dependencies).filter((name) => !resolves(name, pkg));
232
+ } catch {
233
+ return [];
234
+ }
235
+ }
236
+
212
237
  function dependencyFinding(): Finding {
213
- return existsSync(resolve(palPkg(), "node_modules"))
238
+ const missing = unresolvedDependencies(palPkg());
239
+ return missing.length === 0
214
240
  ? passed("dependencies", "Dependencies installed")
215
- : failing("dependencies", "PAL's dependencies are not installed", {
216
- say: "Install them",
217
- command: "pal cli install",
218
- });
241
+ : failing(
242
+ "dependencies",
243
+ `PAL's dependencies do not resolve: ${missing.join(", ")}`,
244
+ {
245
+ say: "Install them",
246
+ command: "pal cli install",
247
+ }
248
+ );
219
249
  }
220
250
 
221
251
  export function stateFindings(): Finding[] {
@@ -642,6 +642,77 @@ const v6HistorySlugs: Migration = {
642
642
  },
643
643
  };
644
644
 
645
+ // ── v7-retired-settings-keys: keys older settings templates wrote ──
646
+
647
+ const RETIRED_SETTINGS_KEYS: Record<string, string[]> = {
648
+ loadAtStartup: ["_docs"],
649
+ steering: ["_docs"],
650
+ dynamicContext: [
651
+ "_docs",
652
+ "learningDigest",
653
+ "projectHistory",
654
+ "sessionIntelligence",
655
+ "synthesis",
656
+ "signalTrends",
657
+ "activeWork",
658
+ ],
659
+ };
660
+
661
+ function settingsFile(): string {
662
+ return resolve(paths.memory(), "pal-settings.json");
663
+ }
664
+
665
+ function readSettingsObject(): Record<string, Record<string, unknown>> | null {
666
+ try {
667
+ const settings = JSON.parse(readFileSync(settingsFile(), "utf-8"));
668
+ return typeof settings === "object" && settings !== null ? settings : null;
669
+ } catch {
670
+ return null;
671
+ }
672
+ }
673
+
674
+ function retiredKeysIn(settings: Record<string, Record<string, unknown>>): string[] {
675
+ return Object.entries(RETIRED_SETTINGS_KEYS).flatMap(([section, keys]) => {
676
+ const value = settings[section];
677
+ if (typeof value !== "object" || value === null) return [];
678
+ return keys.filter((key) => key in value).map((key) => `${section}.${key}`);
679
+ });
680
+ }
681
+
682
+ const v7RetiredSettingsKeys: Migration = {
683
+ id: "v7-retired-settings-keys",
684
+ description: "Remove settings older templates wrote that PAL no longer reads",
685
+
686
+ check() {
687
+ const settings = readSettingsObject();
688
+ const retired = settings ? retiredKeysIn(settings) : [];
689
+ return {
690
+ pending: retired.length > 0,
691
+ detail: retired.length > 0 ? retired.join(", ") : undefined,
692
+ };
693
+ },
694
+
695
+ run(dryRun = false): MigrationResult {
696
+ const settings = readSettingsObject();
697
+ const retired = settings ? retiredKeysIn(settings) : [];
698
+ if (!settings || retired.length === 0)
699
+ return { migrated: 0, skipped: 0, results: [] };
700
+ if (!dryRun) {
701
+ for (const path of retired) {
702
+ const [section, key] = path.split(".");
703
+ delete settings[section][key];
704
+ }
705
+ writeFileSync(settingsFile(), `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
706
+ }
707
+ const verb = dryRun ? "would remove" : "removed";
708
+ return {
709
+ migrated: retired.length,
710
+ skipped: 0,
711
+ results: [`pal-settings.json: ${verb} ${retired.join(", ")}`],
712
+ };
713
+ },
714
+ };
715
+
645
716
  const MIGRATIONS: Migration[] = [
646
717
  v1Projects,
647
718
  v2ThreadsToIsc,
@@ -649,6 +720,7 @@ const MIGRATIONS: Migration[] = [
649
720
  v4PathsToBindings,
650
721
  v5AttributionKeys,
651
722
  v6HistorySlugs,
723
+ v7RetiredSettingsKeys,
652
724
  ];
653
725
 
654
726
  // ── Public API ────────────────────────────────────────────────────
@@ -108,16 +108,19 @@ function loggedAt(line: string): number {
108
108
  return match ? Date.parse(`${match[1]}T${match[2]}Z`) : 0;
109
109
  }
110
110
 
111
+ function whatTheAgentSaid(message: string): string {
112
+ const output = / stderr\(\d+\)=(.*?) stdout\(\d+\)=(.*)$/.exec(message);
113
+ if (!output) return message;
114
+ return output[1].trim() || output[2].trim() || message;
115
+ }
116
+
111
117
  function groupBySource(errors: string[]): HookErrorGroup[] {
112
118
  const groups = new Map<string, HookErrorGroup>();
113
119
  for (const error of errors) {
114
120
  const source = /^([^:]+):/.exec(error)?.[1] ?? "unknown";
115
121
  const group = groups.get(source) ?? { source, count: 0, last: "" };
116
122
  group.count++;
117
- group.last = error
118
- .slice(source.length + 1)
119
- .trim()
120
- .slice(0, 120);
123
+ group.last = whatTheAgentSaid(error.slice(source.length + 1).trim()).slice(0, 120);
121
124
  groups.set(source, group);
122
125
  }
123
126
  return [...groups.values()].sort((a, b) => b.count - a.count);
@@ -17,7 +17,7 @@ import {
17
17
  writeFileSync,
18
18
  } from "node:fs";
19
19
  import { homedir } from "node:os";
20
- import { dirname, resolve, sep } from "node:path";
20
+ import { dirname, isAbsolute, resolve, sep } from "node:path";
21
21
  import { assets, palHome, platform } from "../hooks/lib/paths";
22
22
  import { declaredTriggers } from "../hooks/lib/skill-triggers";
23
23
 
@@ -186,7 +186,7 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
186
186
  for (const [event, entries] of Object.entries(result.hooks)) {
187
187
  result.hooks[event] = entries.filter((e) => {
188
188
  const cmd = e.hooks?.[0]?.command;
189
- return !cmd || !palCanonical.has(canonicalPalCmd(cmd));
189
+ return !cmd || !isOutdatedPalHook(cmd, palCanonical);
190
190
  });
191
191
  if (result.hooks[event].length === 0) delete result.hooks[event];
192
192
  }
@@ -390,7 +390,7 @@ export function mergeCursorHooks(
390
390
  // Strip existing PAL hooks that match canonically (removes old-path duplicates)
391
391
  for (const [event, entries] of Object.entries(result.hooks)) {
392
392
  result.hooks[event] = entries.filter(
393
- (e) => !palCanonical.has(canonicalPalCmd(e.command))
393
+ (e) => !isOutdatedPalHook(e.command, palCanonical)
394
394
  );
395
395
  if (result.hooks[event].length === 0) delete result.hooks[event];
396
396
  }
@@ -467,6 +467,15 @@ function canonicalPalCmd(cmd: string): string {
467
467
  return withoutEnv;
468
468
  }
469
469
 
470
+ function runsRemovedPalHook(cmd: string): boolean {
471
+ const script = /bun\s+run\s+(\S+\/src\/hooks\/\S+\.ts)/.exec(cmd)?.[1];
472
+ return script !== undefined && isAbsolute(script) && !existsSync(script);
473
+ }
474
+
475
+ function isOutdatedPalHook(cmd: string, palCanonical: Set<string>): boolean {
476
+ return palCanonical.has(canonicalPalCmd(cmd)) || runsRemovedPalHook(cmd);
477
+ }
478
+
470
479
  /**
471
480
  * True for path-scoped Grep()/Glob() allow rules, which Claude Code cannot honor —
472
481
  * it resolves Grep/Glob permission through Read(...) rules, so these never match and
@@ -507,11 +516,11 @@ function stripPalHooks(
507
516
  hooks[event] = (hooks[event] ?? [])
508
517
  .map((g) => {
509
518
  const flat = g as unknown as CodexHookCommand;
510
- if (!g.hooks && flat.command && palCanonical.has(canonicalPalCmd(flat.command))) {
519
+ if (!g.hooks && flat.command && isOutdatedPalHook(flat.command, palCanonical)) {
511
520
  return null;
512
521
  }
513
522
  const filtered = (g.hooks ?? []).filter(
514
- (h) => !palCanonical.has(canonicalPalCmd(h.command))
523
+ (h) => !isOutdatedPalHook(h.command, palCanonical)
515
524
  );
516
525
  return filtered.length > 0 ? { ...g, hooks: filtered } : null;
517
526
  })