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 +2 -2
- package/package.json +1 -1
- package/src/cli/doctor/environment.ts +29 -9
- package/src/cli/doctor/health.ts +15 -1
- package/src/cli/doctor/state.ts +35 -5
- package/src/cli/migrate.ts +72 -0
- package/src/hooks/lib/log.ts +7 -4
- package/src/targets/lib.ts +14 -5
package/README.md
CHANGED
|
@@ -51,10 +51,10 @@ bun install
|
|
|
51
51
|
bun run install:all
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
|
|
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
|
-
|
|
57
|
+
bun link
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
---
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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(),
|
package/src/cli/doctor/health.ts
CHANGED
|
@@ -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}`,
|
package/src/cli/doctor/state.ts
CHANGED
|
@@ -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
|
-
|
|
238
|
+
const missing = unresolvedDependencies(palPkg());
|
|
239
|
+
return missing.length === 0
|
|
214
240
|
? passed("dependencies", "Dependencies installed")
|
|
215
|
-
: failing(
|
|
216
|
-
|
|
217
|
-
|
|
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[] {
|
package/src/cli/migrate.ts
CHANGED
|
@@ -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 ────────────────────────────────────────────────────
|
package/src/hooks/lib/log.ts
CHANGED
|
@@ -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);
|
package/src/targets/lib.ts
CHANGED
|
@@ -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 || !
|
|
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) => !
|
|
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 &&
|
|
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) => !
|
|
523
|
+
(h) => !isOutdatedPalHook(h.command, palCanonical)
|
|
515
524
|
);
|
|
516
525
|
return filtered.length > 0 ? { ...g, hooks: filtered } : null;
|
|
517
526
|
})
|