portable-agent-layer 0.85.13 → 0.85.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.85.13",
3
+ "version": "0.85.15",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
3
  import { homedir } from "node:os";
3
4
  import { dirname, resolve } from "node:path";
@@ -67,22 +68,47 @@ function playwrightPlatformOverride(host: BrowserHost): string {
67
68
  return `PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-${host.arch === "arm64" ? "arm64" : "x64"} `;
68
69
  }
69
70
 
71
+ function chromiumInstallCommand(host: BrowserHost): string {
72
+ const pinned = host.playwright ? `playwright@${host.playwright.version}` : "playwright";
73
+ return `${playwrightPlatformOverride(host)}bun x ${pinned} install chromium`;
74
+ }
75
+
70
76
  export function playwrightFinding(host: BrowserHost): Finding {
71
77
  if (hasChromium(host)) return passed("playwright", "Playwright Chromium installed");
72
- const pinned = host.playwright ? `playwright@${host.playwright.version}` : "playwright";
73
- const override = playwrightPlatformOverride(host);
74
78
  return warning(
75
79
  "playwright.missing",
76
80
  "Playwright's Chromium is not installed — create-pdf, consulting-report and screenshots fail",
77
81
  {
78
- say: override
82
+ say: playwrightPlatformOverride(host)
79
83
  ? "Install it as Ubuntu 24.04, which Playwright supports"
80
84
  : "Install it",
81
- command: `${override}bun x ${pinned} install chromium`,
85
+ command: chromiumInstallCommand(host),
82
86
  }
83
87
  );
84
88
  }
85
89
 
90
+ function thisBrowserHost(): BrowserHost {
91
+ return {
92
+ browsersPath: playwrightBrowsersPath(),
93
+ playwright: installedPlaywright(),
94
+ platform: process.platform,
95
+ arch: process.arch,
96
+ osRelease: readOsRelease(),
97
+ };
98
+ }
99
+
100
+ function runInShell(command: string): boolean {
101
+ return spawnSync(command, { cwd: palPkg(), shell: true, stdio: "ignore" }).status === 0;
102
+ }
103
+
104
+ /** A failure is left to the doctor's report that follows, which names the fix. */
105
+ export function installChromium(
106
+ host: BrowserHost = thisBrowserHost(),
107
+ run: (command: string) => boolean = runInShell
108
+ ): void {
109
+ if (!hasChromium(host)) run(chromiumInstallCommand(host));
110
+ }
111
+
86
112
  interface PalInstall {
87
113
  pal: string | null;
88
114
  pkg: string;
@@ -136,13 +162,7 @@ export function environmentFindings(rtk: ToolCheck): Finding[] {
136
162
  pkg: palPkg(),
137
163
  repoMode: isRepoMode(),
138
164
  }),
139
- playwrightFinding({
140
- browsersPath: playwrightBrowsersPath(),
141
- playwright: installedPlaywright(),
142
- platform: process.platform,
143
- arch: process.arch,
144
- osRelease: readOsRelease(),
145
- }),
165
+ playwrightFinding(thisBrowserHost()),
146
166
  rtkFinding(rtk),
147
167
  ...versionControlFindings(),
148
168
  ];
@@ -11,29 +11,49 @@ interface PendingMigration {
11
11
  detail?: string;
12
12
  }
13
13
 
14
+ interface ErrorContext {
15
+ now: number;
16
+ env: Record<string, string | undefined>;
17
+ }
18
+
14
19
  const CLAUDE_LOGIN_EXPIRED = /Failed to authenticate/;
20
+ const MINUTE_MS = 60_000;
15
21
 
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
+ 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",
22
35
  command: "claude setup-token",
23
- }
24
- );
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
+ });
25
41
  }
26
42
 
27
- export function hookErrorFindings(groups: HookErrorGroup[]): Finding[] {
43
+ export function hookErrorFindings(
44
+ groups: HookErrorGroup[],
45
+ context: ErrorContext = { now: Date.now(), env: process.env }
46
+ ): Finding[] {
28
47
  if (groups.length === 0)
29
48
  return [passed("hook-errors", "No hook errors in the last 24h")];
30
49
  const log = resolve(palHome(), "debug", "debug.log");
31
50
  return groups.map((group) => {
32
- if (CLAUDE_LOGIN_EXPIRED.test(group.last)) return claudeLoginExpiredFinding(group);
51
+ if (CLAUDE_LOGIN_EXPIRED.test(group.last))
52
+ return claudeLoginExpiredFinding(group, context);
33
53
  const times = group.count === 1 ? "once" : `${group.count} times`;
34
54
  return warning(
35
55
  `hook-errors.${group.source}`,
36
- `${group.source} failed ${times} in the last 24h — last: ${group.last}`,
56
+ `${group.source} failed ${times} in the last 24h, last ${ago(group.lastAt, context.now)}: ${group.last}`,
37
57
  { say: `Full trace in ${log}` }
38
58
  );
39
59
  });
@@ -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 unknown = schema ? unknownSettingsKeys(settings, schema) : [];
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/index.ts CHANGED
@@ -50,10 +50,10 @@ import { inference, previewInferenceRoute } from "../hooks/lib/inference";
50
50
  import { logDebug } from "../hooks/lib/log";
51
51
  import { ensureRegistered, writeRegistryEntry } from "../hooks/lib/machine";
52
52
  import { palHome, palPkg, paths, platform, toPath } from "../hooks/lib/paths";
53
- import { telosStatus } from "../hooks/lib/telos-topics";
54
- import { log } from "../targets/lib";
53
+ import { log, narrateSteps } from "../targets/lib";
55
54
  import { builtinToolVerbs, runBuiltinTool } from "./builtin-tools";
56
55
  import { type DoctorResult, detectAgents } from "./doctor/agents";
56
+ import { installChromium } from "./doctor/environment";
57
57
  import { runDoctor } from "./doctor/run";
58
58
  import { findSessionAgent, NO_SESSION_AGENT_MESSAGE } from "./session-agent";
59
59
 
@@ -155,7 +155,7 @@ async function runCli(command: string | undefined, args: string[]) {
155
155
  break;
156
156
  case "install":
157
157
  banner();
158
- await install(resolveTargets(args));
158
+ process.exit(await install(resolveTargets(args), args));
159
159
  break;
160
160
  case "uninstall":
161
161
  await uninstall(args);
@@ -265,24 +265,10 @@ function banner() {
265
265
  console.log("");
266
266
  console.log(" ╔═══════════════════════════════════╗");
267
267
  console.log(" ║ PAL — Portable Agent Layer ║");
268
- console.log(" ║ Non-destructive · Modular ║");
269
268
  console.log(" ╚═══════════════════════════════════╝");
270
269
  console.log("");
271
270
  }
272
271
 
273
- /**
274
- * Install asks for identity and stops. TELOS is a conversation, not a form: a
275
- * user who has just installed a tool cannot yet say what they want from it, and
276
- * answers given under that pressure are worse than none.
277
- */
278
- function pointAtOnboarding(): void {
279
- const unanswered = telosStatus().filter((topic) => topic.priority && !topic.answered);
280
- if (unanswered.length === 0) return;
281
- log.info(
282
- `PAL knows nothing about you yet (${unanswered.length} topics open). When you have half an hour, ask your agent to onboard you.`
283
- );
284
- }
285
-
286
272
  function showHelp() {
287
273
  console.log(`
288
274
  Usage:
@@ -290,8 +276,9 @@ function showHelp() {
290
276
  pal cli <command> [options] Admin commands
291
277
 
292
278
  Admin commands:
293
- pal cli init [--claude] [--opencode] [--cursor] [--codex] Scaffold and install (default: all)
294
- pal cli install [--claude] [--opencode] [--cursor] [--codex] Register hooks for targets
279
+ pal cli init [--claude] [--opencode] [--cursor] [--codex] Scaffold and install (default: all), then run the doctor
280
+ pal cli install [--claude] [--opencode] [--cursor] [--codex] Register hooks for targets, then run the doctor
281
+ Add --verbose to either for the step-by-step log
295
282
  pal cli uninstall [--claude] [--opencode] [--cursor] [--codex] Remove hooks for targets
296
283
  pal cli update Update PAL (git pull or npm update)
297
284
  pal cli export [path] [--dry-run] Export state to zip
@@ -486,12 +473,10 @@ async function init(args: string[]) {
486
473
  const { scaffoldTelos } = await import("../targets/lib");
487
474
 
488
475
  banner();
476
+ narrateSteps(args.includes("--verbose"));
489
477
 
490
478
  const health = detectAgents();
491
- runDoctor([], health);
492
- if (!health.hasAgent) {
493
- process.exit(1);
494
- }
479
+ if (!health.hasAgent) process.exit(runDoctor([], health));
495
480
 
496
481
  const home = palHome();
497
482
  log.info(`Creating PAL home at ${home}`);
@@ -506,7 +491,7 @@ async function init(args: string[]) {
506
491
 
507
492
  // Auto-detect available targets
508
493
  const targets = resolveTargets(args, health);
509
- await install(targets);
494
+ process.exit(await install(targets, args));
510
495
  }
511
496
 
512
497
  /**
@@ -522,25 +507,25 @@ function runQuietly(cmd: string, args: string[], cwd: string): number | null {
522
507
  return r.status;
523
508
  }
524
509
 
525
- async function install(targets: Targets) {
526
- // Ensure dependencies are installed
510
+ function targetInstallers(): [keyof Targets, string, () => Promise<unknown>][] {
511
+ return [
512
+ ["claude", "Claude Code", () => import("../targets/claude/install")],
513
+ ["opencode", "opencode", () => import("../targets/opencode/install")],
514
+ ["cursor", "Cursor", () => import("../targets/cursor/install")],
515
+ ["copilot", "Copilot", () => import("../targets/copilot/install")],
516
+ ["codex", "Codex", () => import("../targets/codex/install")],
517
+ ];
518
+ }
519
+
520
+ async function install(targets: Targets, args: string[]): Promise<number> {
521
+ narrateSteps(args.includes("--verbose"));
527
522
  const pkg = palPkg();
528
523
  if (runQuietly("bun", ["install", "--frozen-lockfile"], pkg) !== 0) {
529
524
  log.warn("bun install failed — continuing anyway, but hooks may not work");
530
525
  }
531
526
 
532
- // Fetch the Chromium build Playwright uses for PDF rendering (create-pdf skill).
533
- // Idempotent — skipped if already cached. Skipped entirely under PAL_SKIP_BROWSER_INSTALL=1
534
- // (used by tests to avoid a ~150MB download on every run).
535
527
  // Uses `bun x` (not `bunx`) for Windows compatibility — bunx resolves unreliably under cmd.exe.
536
- if (process.env.PAL_SKIP_BROWSER_INSTALL !== "1") {
537
- const pw = runQuietly("bun", ["x", "playwright", "install", "chromium"], pkg);
538
- if (pw !== 0) {
539
- log.warn(
540
- `playwright install chromium failed (exit ${pw}) — create-pdf and consulting-report skills won't work. Retry manually: bun x playwright install chromium`
541
- );
542
- }
543
- }
528
+ if (process.env.PAL_SKIP_BROWSER_INSTALL !== "1") installChromium();
544
529
 
545
530
  // Scaffold TELOS + PAL settings, then prompt for missing identity
546
531
  const { scaffoldTelos, scaffoldPalSettings, copyPalDocs, generateSkillIndex } =
@@ -553,7 +538,6 @@ async function install(targets: Targets) {
553
538
  await promptIdentity();
554
539
  await promptAttribution();
555
540
  await promptAutoUpdate();
556
- pointAtOnboarding();
557
541
 
558
542
  // Registers the label loadActor derives, so it travels on the next export.
559
543
  const { ensureActorRegistered } = await import("../hooks/lib/actor");
@@ -566,34 +550,10 @@ async function install(targets: Targets) {
566
550
  const palDocsCount = copyPalDocs();
567
551
  regenerateIfNeeded();
568
552
 
569
- if (targets.claude) {
570
- console.log("━━━ Claude Code ━━━");
571
- await import("../targets/claude/install");
572
- console.log("");
573
- }
574
-
575
- if (targets.opencode) {
576
- console.log("━━━ opencode ━━━");
577
- await import("../targets/opencode/install");
578
- console.log("");
579
- }
580
-
581
- if (targets.cursor) {
582
- console.log("━━━ Cursor ━━━");
583
- await import("../targets/cursor/install");
584
- console.log("");
585
- }
586
-
587
- if (targets.copilot) {
588
- console.log("━━━ Copilot ━━━");
589
- await import("../targets/copilot/install");
590
- console.log("");
591
- }
592
-
593
- if (targets.codex) {
594
- console.log("━━━ Codex ━━━");
595
- await import("../targets/codex/install");
596
- console.log("");
553
+ for (const [target, label, installTarget] of targetInstallers()) {
554
+ if (!targets[target]) continue;
555
+ log.heading(label);
556
+ await installTarget();
597
557
  }
598
558
 
599
559
  // The rest of the shared work reads what the installers just wrote: the index
@@ -608,7 +568,8 @@ async function install(targets: Targets) {
608
568
 
609
569
  await refreshControlRoom();
610
570
 
611
- log.success("Done. Existing config was preserved — only new entries were added.");
571
+ if (args.includes("--verbose")) console.log("");
572
+ return runDoctor(args);
612
573
  }
613
574
 
614
575
  /**
@@ -912,7 +873,7 @@ async function update() {
912
873
  clearUpdateCache();
913
874
 
914
875
  log.info("Reinstalling...");
915
- await install(resolveTargets([]));
876
+ process.exit(await install(resolveTargets([]), []));
916
877
  }
917
878
 
918
879
  function cliDebug(args: string[]) {
@@ -671,7 +671,9 @@ function readSettingsObject(): Record<string, Record<string, unknown>> | null {
671
671
  }
672
672
  }
673
673
 
674
- function retiredKeysIn(settings: Record<string, Record<string, unknown>>): string[] {
674
+ export function retiredKeysIn(
675
+ settings: Record<string, Record<string, unknown>>
676
+ ): string[] {
675
677
  return Object.entries(RETIRED_SETTINGS_KEYS).flatMap(([section, keys]) => {
676
678
  const value = settings[section];
677
679
  if (typeof value !== "object" || value === null) return [];
@@ -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;
@@ -114,13 +120,14 @@ function whatTheAgentSaid(message: string): string {
114
120
  return output[1].trim() || output[2].trim() || message;
115
121
  }
116
122
 
117
- function groupBySource(errors: string[]): HookErrorGroup[] {
123
+ function groupBySource(errors: LoggedError[]): HookErrorGroup[] {
118
124
  const groups = new Map<string, HookErrorGroup>();
119
- for (const error of errors) {
120
- const source = /^([^:]+):/.exec(error)?.[1] ?? "unknown";
121
- 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 };
122
128
  group.count++;
123
- group.last = whatTheAgentSaid(error.slice(source.length + 1).trim()).slice(0, 120);
129
+ group.last = whatTheAgentSaid(message.slice(source.length + 1).trim()).slice(0, 120);
130
+ group.lastAt = at;
124
131
  groups.set(source, group);
125
132
  }
126
133
  return [...groups.values()].sort((a, b) => b.count - a.count);
@@ -131,7 +138,10 @@ export function recentHookErrors(now: number = Date.now()): HookErrorGroup[] {
131
138
  const recent = readAllLogs()
132
139
  .split("\n")
133
140
  .filter((line) => line.includes("] ERROR ") && loggedAt(line) > now - DAY_MS)
134
- .map((line) => line.replace(/^\[.*?\] ERROR /, ""));
141
+ .map((line) => ({
142
+ at: loggedAt(line),
143
+ message: line.replace(/^\[.*?\] ERROR /, ""),
144
+ }));
135
145
  return groupBySource(recent);
136
146
  } catch {
137
147
  return [];
@@ -27,9 +27,21 @@ function runningUnderTest(): boolean {
27
27
  return process.env.PAL_TEST_SANDBOX === "1";
28
28
  }
29
29
 
30
+ let narrating = true;
31
+
32
+ /** Install's step log is for --verbose; the doctor report that follows carries the result. */
33
+ export function narrateSteps(on: boolean): void {
34
+ narrating = on;
35
+ }
36
+
37
+ function narrate(line: string): void {
38
+ if (narrating) console.log(line);
39
+ }
40
+
30
41
  export const log = {
31
- info: (msg: string) => console.log(`\x1b[34m[pal]\x1b[0m ${msg}`),
32
- success: (msg: string) => console.log(`\x1b[32m[pal]\x1b[0m ${msg}`),
42
+ info: (msg: string) => narrate(`\x1b[34m[pal]\x1b[0m ${msg}`),
43
+ success: (msg: string) => narrate(`\x1b[32m[pal]\x1b[0m ${msg}`),
44
+ heading: (label: string) => narrate(`\n━━━ ${label} ━━━`),
33
45
  warn: (msg: string) => console.log(`\x1b[33m[pal]\x1b[0m ${msg}`),
34
46
  error: (msg: string) => console.error(`\x1b[31m[pal]\x1b[0m ${msg}`),
35
47
 
@@ -42,7 +54,7 @@ export const log = {
42
54
  */
43
55
  detail: (msg: string) => {
44
56
  if (runningUnderTest()) return;
45
- console.log(`\x1b[34m[pal]\x1b[0m ${msg}`);
57
+ narrate(`\x1b[34m[pal]\x1b[0m ${msg}`);
46
58
  },
47
59
  };
48
60