portable-agent-layer 0.85.12 → 0.85.14
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 +37 -3
- package/src/cli/doctor/state.ts +7 -1
- package/src/cli/migrate.ts +74 -0
- package/src/hooks/lib/log.ts +22 -9
- 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,15 +11,49 @@ interface PendingMigration {
|
|
|
11
11
|
detail?: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
interface ErrorContext {
|
|
15
|
+
now: number;
|
|
16
|
+
env: Record<string, string | undefined>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const CLAUDE_LOGIN_EXPIRED = /Failed to authenticate/;
|
|
20
|
+
const MINUTE_MS = 60_000;
|
|
21
|
+
|
|
22
|
+
function ago(at: number, now: number): string {
|
|
23
|
+
const minutes = Math.max(0, Math.round((now - at) / MINUTE_MS));
|
|
24
|
+
return minutes < 60 ? `${minutes}m ago` : `${Math.floor(minutes / 60)}h ago`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function claudeLoginExpiredFinding(
|
|
28
|
+
group: HookErrorGroup,
|
|
29
|
+
context: ErrorContext
|
|
30
|
+
): Finding {
|
|
31
|
+
const title = `Background calls to Claude could not log in — ${group.count} failed in the last 24h, last ${ago(group.lastAt, context.now)}: ${group.last}`;
|
|
32
|
+
if (context.env.CLAUDE_CODE_OAUTH_TOKEN)
|
|
33
|
+
return warning(`hook-errors.${group.source}`, title, {
|
|
34
|
+
say: "CLAUDE_CODE_OAUTH_TOKEN is set — if these failures predate it, they clear 24h after the last one; if not, the token is invalid, so create a new one",
|
|
35
|
+
command: "claude setup-token",
|
|
36
|
+
});
|
|
37
|
+
return failing(`hook-errors.${group.source}`, title, {
|
|
38
|
+
say: "Create a year-long token, then export it as CLAUDE_CODE_OAUTH_TOKEN in your shell profile",
|
|
39
|
+
command: "claude setup-token",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function hookErrorFindings(
|
|
44
|
+
groups: HookErrorGroup[],
|
|
45
|
+
context: ErrorContext = { now: Date.now(), env: process.env }
|
|
46
|
+
): Finding[] {
|
|
15
47
|
if (groups.length === 0)
|
|
16
48
|
return [passed("hook-errors", "No hook errors in the last 24h")];
|
|
17
49
|
const log = resolve(palHome(), "debug", "debug.log");
|
|
18
50
|
return groups.map((group) => {
|
|
51
|
+
if (CLAUDE_LOGIN_EXPIRED.test(group.last))
|
|
52
|
+
return claudeLoginExpiredFinding(group, context);
|
|
19
53
|
const times = group.count === 1 ? "once" : `${group.count} times`;
|
|
20
54
|
return warning(
|
|
21
55
|
`hook-errors.${group.source}`,
|
|
22
|
-
`${group.source} failed ${times} in the last 24h
|
|
56
|
+
`${group.source} failed ${times} in the last 24h, last ${ago(group.lastAt, context.now)}: ${group.last}`,
|
|
23
57
|
{ say: `Full trace in ${log}` }
|
|
24
58
|
);
|
|
25
59
|
});
|
package/src/cli/doctor/state.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { palHome, palPkg } from "../../hooks/lib/paths";
|
|
4
4
|
import { auditBindings, type BindingIssue } from "../../hooks/lib/projects";
|
|
5
5
|
import { telosStatus } from "../../hooks/lib/telos-topics";
|
|
6
|
+
import { retiredKeysIn } from "../migrate";
|
|
6
7
|
import { type Finding, failing, optional, passed, warning } from "./finding";
|
|
7
8
|
|
|
8
9
|
interface SchemaNode {
|
|
@@ -142,7 +143,12 @@ function settingsFindings(): Finding[] {
|
|
|
142
143
|
}),
|
|
143
144
|
];
|
|
144
145
|
const schema = readSettingsSchema();
|
|
145
|
-
const
|
|
146
|
+
const leftToMigration = new Set(
|
|
147
|
+
retiredKeysIn(settings as Record<string, Record<string, unknown>>)
|
|
148
|
+
);
|
|
149
|
+
const unknown = schema
|
|
150
|
+
? unknownSettingsKeys(settings, schema).filter((k) => !leftToMigration.has(k.path))
|
|
151
|
+
: [];
|
|
146
152
|
return [identityFinding(settings), unknownKeysFinding(unknown)];
|
|
147
153
|
}
|
|
148
154
|
|
package/src/cli/migrate.ts
CHANGED
|
@@ -642,6 +642,79 @@ 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
|
+
export function retiredKeysIn(
|
|
675
|
+
settings: Record<string, Record<string, unknown>>
|
|
676
|
+
): string[] {
|
|
677
|
+
return Object.entries(RETIRED_SETTINGS_KEYS).flatMap(([section, keys]) => {
|
|
678
|
+
const value = settings[section];
|
|
679
|
+
if (typeof value !== "object" || value === null) return [];
|
|
680
|
+
return keys.filter((key) => key in value).map((key) => `${section}.${key}`);
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const v7RetiredSettingsKeys: Migration = {
|
|
685
|
+
id: "v7-retired-settings-keys",
|
|
686
|
+
description: "Remove settings older templates wrote that PAL no longer reads",
|
|
687
|
+
|
|
688
|
+
check() {
|
|
689
|
+
const settings = readSettingsObject();
|
|
690
|
+
const retired = settings ? retiredKeysIn(settings) : [];
|
|
691
|
+
return {
|
|
692
|
+
pending: retired.length > 0,
|
|
693
|
+
detail: retired.length > 0 ? retired.join(", ") : undefined,
|
|
694
|
+
};
|
|
695
|
+
},
|
|
696
|
+
|
|
697
|
+
run(dryRun = false): MigrationResult {
|
|
698
|
+
const settings = readSettingsObject();
|
|
699
|
+
const retired = settings ? retiredKeysIn(settings) : [];
|
|
700
|
+
if (!settings || retired.length === 0)
|
|
701
|
+
return { migrated: 0, skipped: 0, results: [] };
|
|
702
|
+
if (!dryRun) {
|
|
703
|
+
for (const path of retired) {
|
|
704
|
+
const [section, key] = path.split(".");
|
|
705
|
+
delete settings[section][key];
|
|
706
|
+
}
|
|
707
|
+
writeFileSync(settingsFile(), `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
|
|
708
|
+
}
|
|
709
|
+
const verb = dryRun ? "would remove" : "removed";
|
|
710
|
+
return {
|
|
711
|
+
migrated: retired.length,
|
|
712
|
+
skipped: 0,
|
|
713
|
+
results: [`pal-settings.json: ${verb} ${retired.join(", ")}`],
|
|
714
|
+
};
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
|
|
645
718
|
const MIGRATIONS: Migration[] = [
|
|
646
719
|
v1Projects,
|
|
647
720
|
v2ThreadsToIsc,
|
|
@@ -649,6 +722,7 @@ const MIGRATIONS: Migration[] = [
|
|
|
649
722
|
v4PathsToBindings,
|
|
650
723
|
v5AttributionKeys,
|
|
651
724
|
v6HistorySlugs,
|
|
725
|
+
v7RetiredSettingsKeys,
|
|
652
726
|
];
|
|
653
727
|
|
|
654
728
|
// ── Public API ────────────────────────────────────────────────────
|
package/src/hooks/lib/log.ts
CHANGED
|
@@ -83,6 +83,12 @@ export interface HookErrorGroup {
|
|
|
83
83
|
source: string;
|
|
84
84
|
count: number;
|
|
85
85
|
last: string;
|
|
86
|
+
lastAt: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface LoggedError {
|
|
90
|
+
at: number;
|
|
91
|
+
message: string;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
@@ -108,16 +114,20 @@ function loggedAt(line: string): number {
|
|
|
108
114
|
return match ? Date.parse(`${match[1]}T${match[2]}Z`) : 0;
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
function
|
|
117
|
+
function whatTheAgentSaid(message: string): string {
|
|
118
|
+
const output = / stderr\(\d+\)=(.*?) stdout\(\d+\)=(.*)$/.exec(message);
|
|
119
|
+
if (!output) return message;
|
|
120
|
+
return output[1].trim() || output[2].trim() || message;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function groupBySource(errors: LoggedError[]): HookErrorGroup[] {
|
|
112
124
|
const groups = new Map<string, HookErrorGroup>();
|
|
113
|
-
for (const
|
|
114
|
-
const source = /^([^:]+):/.exec(
|
|
115
|
-
const group = groups.get(source) ?? { source, count: 0, last: "" };
|
|
125
|
+
for (const { at, message } of errors) {
|
|
126
|
+
const source = /^([^:]+):/.exec(message)?.[1] ?? "unknown";
|
|
127
|
+
const group = groups.get(source) ?? { source, count: 0, last: "", lastAt: 0 };
|
|
116
128
|
group.count++;
|
|
117
|
-
group.last =
|
|
118
|
-
|
|
119
|
-
.trim()
|
|
120
|
-
.slice(0, 120);
|
|
129
|
+
group.last = whatTheAgentSaid(message.slice(source.length + 1).trim()).slice(0, 120);
|
|
130
|
+
group.lastAt = at;
|
|
121
131
|
groups.set(source, group);
|
|
122
132
|
}
|
|
123
133
|
return [...groups.values()].sort((a, b) => b.count - a.count);
|
|
@@ -128,7 +138,10 @@ export function recentHookErrors(now: number = Date.now()): HookErrorGroup[] {
|
|
|
128
138
|
const recent = readAllLogs()
|
|
129
139
|
.split("\n")
|
|
130
140
|
.filter((line) => line.includes("] ERROR ") && loggedAt(line) > now - DAY_MS)
|
|
131
|
-
.map((line) =>
|
|
141
|
+
.map((line) => ({
|
|
142
|
+
at: loggedAt(line),
|
|
143
|
+
message: line.replace(/^\[.*?\] ERROR /, ""),
|
|
144
|
+
}));
|
|
132
145
|
return groupBySource(recent);
|
|
133
146
|
} catch {
|
|
134
147
|
return [];
|
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
|
})
|