portable-agent-layer 0.85.15 → 0.85.16
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 +1 -1
- package/src/cli/index.ts +3 -0
- package/src/cli/leftovers.ts +118 -0
- package/src/cli/migrate.ts +38 -6
- package/src/hooks/lib/log.ts +1 -13
- package/src/targets/copilot/uninstall.ts +1 -17
- package/src/targets/lib.ts +1 -34
- package/src/targets/opencode/install.ts +2 -13
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -524,6 +524,9 @@ async function install(targets: Targets, args: string[]): Promise<number> {
|
|
|
524
524
|
log.warn("bun install failed — continuing anyway, but hooks may not work");
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
+
const { applyPendingMigrations } = await import("./migrate");
|
|
528
|
+
for (const line of applyPendingMigrations()) log.info(line);
|
|
529
|
+
|
|
527
530
|
// Uses `bun x` (not `bunx`) for Windows compatibility — bunx resolves unreliably under cmd.exe.
|
|
528
531
|
if (process.env.PAL_SKIP_BROWSER_INSTALL !== "1") installChromium();
|
|
529
532
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readlinkSync, renameSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { paths, platform } from "../hooks/lib/paths";
|
|
4
|
+
import { readJson, writeJson } from "../targets/lib";
|
|
5
|
+
|
|
6
|
+
/** Something an older PAL wrote that the current one neither writes nor reads. */
|
|
7
|
+
export interface Leftover {
|
|
8
|
+
id: string;
|
|
9
|
+
description: string;
|
|
10
|
+
find(): string[];
|
|
11
|
+
remove(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type Json = Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
const claudeSettings = () => resolve(platform.claudeDir(), "settings.json");
|
|
17
|
+
const palSettings = () => resolve(paths.memory(), "pal-settings.json");
|
|
18
|
+
|
|
19
|
+
function isPathScopedFileToolRule(rule: string): boolean {
|
|
20
|
+
return /^(?:Grep|Glob)\(/.test(rule);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function claudeAllowRules(): string[] {
|
|
24
|
+
const settings = readJson<Json>(claudeSettings(), {});
|
|
25
|
+
const allow = (settings.permissions as { allow?: unknown } | undefined)?.allow;
|
|
26
|
+
return Array.isArray(allow)
|
|
27
|
+
? allow.filter((r): r is string => typeof r === "string")
|
|
28
|
+
: [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const claudeFileToolRules: Leftover = {
|
|
32
|
+
id: "claude-file-tool-rules",
|
|
33
|
+
description:
|
|
34
|
+
"Remove Grep()/Glob() allow rules older templates wrote — Claude Code ignores them and warns on every prompt",
|
|
35
|
+
find: () => claudeAllowRules().filter(isPathScopedFileToolRule),
|
|
36
|
+
remove() {
|
|
37
|
+
const settings = readJson<Json>(claudeSettings(), {});
|
|
38
|
+
const permissions = settings.permissions as { allow: string[] };
|
|
39
|
+
permissions.allow = permissions.allow.filter(
|
|
40
|
+
(rule) => !isPathScopedFileToolRule(rule)
|
|
41
|
+
);
|
|
42
|
+
writeJson(claudeSettings(), settings);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function startupFiles(): string[] {
|
|
47
|
+
const settings = readJson<Json>(palSettings(), {});
|
|
48
|
+
const files = (settings.loadAtStartup as { files?: unknown } | undefined)?.files;
|
|
49
|
+
return Array.isArray(files)
|
|
50
|
+
? files.filter((f): f is string => typeof f === "string")
|
|
51
|
+
: [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isProjectsFile = (file: string) => file.endsWith("PROJECTS.md");
|
|
55
|
+
|
|
56
|
+
const startupProjectsFile: Leftover = {
|
|
57
|
+
id: "startup-projects-md",
|
|
58
|
+
description:
|
|
59
|
+
"Stop loading PROJECTS.md at startup — projects moved to the project store",
|
|
60
|
+
find: () => startupFiles().filter(isProjectsFile),
|
|
61
|
+
remove() {
|
|
62
|
+
const settings = readJson<Json>(palSettings(), {});
|
|
63
|
+
const startup = settings.loadAtStartup as { files: string[] };
|
|
64
|
+
startup.files = startup.files.filter((file) => !isProjectsFile(file));
|
|
65
|
+
writeJson(palSettings(), settings);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const oldOpencodePlugin = () =>
|
|
70
|
+
resolve(platform.opencodeDir(), "plugins", "pai-plugin.ts");
|
|
71
|
+
|
|
72
|
+
const opencodePluginOldName: Leftover = {
|
|
73
|
+
id: "opencode-pai-plugin",
|
|
74
|
+
description: "Remove the opencode plugin under its old name, pai-plugin.ts",
|
|
75
|
+
find: () => (existsSync(oldOpencodePlugin()) ? [oldOpencodePlugin()] : []),
|
|
76
|
+
remove: () => unlinkSync(oldOpencodePlugin()),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const oldCopilotInstructions = () =>
|
|
80
|
+
resolve(platform.copilotDir(), "copilot-instructions.md");
|
|
81
|
+
|
|
82
|
+
function linksToAgentsMd(path: string): boolean {
|
|
83
|
+
try {
|
|
84
|
+
return lstatSync(path).isSymbolicLink() && readlinkSync(path).includes("AGENTS.md");
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const copilotInstructionsLink: Leftover = {
|
|
91
|
+
id: "copilot-instructions-link",
|
|
92
|
+
description:
|
|
93
|
+
"Remove the copilot-instructions.md link to AGENTS.md — Copilot now reads PAL's own instruction files",
|
|
94
|
+
find: () =>
|
|
95
|
+
linksToAgentsMd(oldCopilotInstructions()) ? [oldCopilotInstructions()] : [],
|
|
96
|
+
remove: () => unlinkSync(oldCopilotInstructions()),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const debugLog = () => resolve(paths.debug(), "debug.log");
|
|
100
|
+
|
|
101
|
+
const debugLogPrev: Leftover = {
|
|
102
|
+
id: "debug-log-prev",
|
|
103
|
+
description: "Rename the old single rotated debug log, debug.log.prev, to debug.log.1",
|
|
104
|
+
find: () => (existsSync(`${debugLog()}.prev`) ? [`${debugLog()}.prev`] : []),
|
|
105
|
+
remove() {
|
|
106
|
+
const prev = `${debugLog()}.prev`;
|
|
107
|
+
if (existsSync(`${debugLog()}.1`)) unlinkSync(prev);
|
|
108
|
+
else renameSync(prev, `${debugLog()}.1`);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const LEFTOVERS: Leftover[] = [
|
|
113
|
+
claudeFileToolRules,
|
|
114
|
+
startupProjectsFile,
|
|
115
|
+
opencodePluginOldName,
|
|
116
|
+
copilotInstructionsLink,
|
|
117
|
+
debugLogPrev,
|
|
118
|
+
];
|
package/src/cli/migrate.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pal cli migrate —
|
|
2
|
+
* pal cli migrate — the only place PAL knows about its own past.
|
|
3
3
|
*
|
|
4
4
|
* Each Migration has:
|
|
5
|
-
* check() —
|
|
6
|
-
* run() — applies
|
|
5
|
+
* check() — reads what is on disk, so it is safe to call on every install
|
|
6
|
+
* run() — applies it; user data is moved, never deleted, while leftovers
|
|
7
|
+
* PAL itself wrote and no longer reads are removed
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* pending work without running anything.
|
|
9
|
+
* Retiring a setting, hook, file or name means adding a migration that removes
|
|
10
|
+
* its leftovers. Install runs pending migrations first; the doctor lists them.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
slugify,
|
|
41
41
|
} from "../tools/knowledge/lib";
|
|
42
42
|
import { readThreads, type Thread, writeThreads } from "../tools/lib/thread";
|
|
43
|
+
import { LEFTOVERS, type Leftover } from "./leftovers";
|
|
43
44
|
|
|
44
45
|
// ── Types ─────────────────────────────────────────────────────────
|
|
45
46
|
|
|
@@ -715,6 +716,29 @@ const v7RetiredSettingsKeys: Migration = {
|
|
|
715
716
|
},
|
|
716
717
|
};
|
|
717
718
|
|
|
719
|
+
function leftoverMigration(leftover: Leftover): Migration {
|
|
720
|
+
return {
|
|
721
|
+
id: leftover.id,
|
|
722
|
+
description: leftover.description,
|
|
723
|
+
movesSources: true,
|
|
724
|
+
check() {
|
|
725
|
+
const found = leftover.find();
|
|
726
|
+
return { pending: found.length > 0, detail: found.join(", ") || undefined };
|
|
727
|
+
},
|
|
728
|
+
run(dryRun = false): MigrationResult {
|
|
729
|
+
const found = leftover.find();
|
|
730
|
+
if (found.length === 0) return { migrated: 0, skipped: 0, results: [] };
|
|
731
|
+
if (!dryRun) leftover.remove();
|
|
732
|
+
const verb = dryRun ? "would remove" : "removed";
|
|
733
|
+
return {
|
|
734
|
+
migrated: found.length,
|
|
735
|
+
skipped: 0,
|
|
736
|
+
results: [`${verb} ${found.join(", ")}`],
|
|
737
|
+
};
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
718
742
|
const MIGRATIONS: Migration[] = [
|
|
719
743
|
v1Projects,
|
|
720
744
|
v2ThreadsToIsc,
|
|
@@ -723,6 +747,7 @@ const MIGRATIONS: Migration[] = [
|
|
|
723
747
|
v5AttributionKeys,
|
|
724
748
|
v6HistorySlugs,
|
|
725
749
|
v7RetiredSettingsKeys,
|
|
750
|
+
...LEFTOVERS.map(leftoverMigration),
|
|
726
751
|
];
|
|
727
752
|
|
|
728
753
|
// ── Public API ────────────────────────────────────────────────────
|
|
@@ -733,6 +758,13 @@ interface PendingMigration {
|
|
|
733
758
|
detail?: string;
|
|
734
759
|
}
|
|
735
760
|
|
|
761
|
+
/** Install runs these first, so one install leaves nothing an older PAL wrote. */
|
|
762
|
+
export function applyPendingMigrations(): string[] {
|
|
763
|
+
return MIGRATIONS.filter((m) => m.check().pending).flatMap((m) =>
|
|
764
|
+
m.run(false).results.map((line) => `${m.id}: ${line}`)
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
736
768
|
/** Returns migrations that have pending work. Used by doctor. */
|
|
737
769
|
export function checkPendingMigrations(): PendingMigration[] {
|
|
738
770
|
return MIGRATIONS.flatMap((m) => {
|
package/src/hooks/lib/log.ts
CHANGED
|
@@ -36,22 +36,10 @@ function timestamp(): string {
|
|
|
36
36
|
* .MAX_ROTATED (oldest). Each rotation shifts .N-1 → .N, drops the oldest,
|
|
37
37
|
* and renames the current log to .1. Total disk footprint bounded at
|
|
38
38
|
* (MAX_ROTATED + 1) * MAX_LOG_SIZE ≈ 300KB.
|
|
39
|
-
*
|
|
40
|
-
* Migrates legacy .prev → .1 on first new rotation so existing history
|
|
41
|
-
* survives the format change.
|
|
42
39
|
*/
|
|
43
40
|
function rotateIfNeeded(path: string): void {
|
|
44
41
|
try {
|
|
45
42
|
if (!existsSync(path) || statSync(path).size <= MAX_LOG_SIZE) return;
|
|
46
|
-
// Backward-compat migration: legacy .prev was the single old rotation file.
|
|
47
|
-
const legacyPrev = `${path}.prev`;
|
|
48
|
-
if (existsSync(legacyPrev) && !existsSync(`${path}.1`)) {
|
|
49
|
-
try {
|
|
50
|
-
renameSync(legacyPrev, `${path}.1`);
|
|
51
|
-
} catch {
|
|
52
|
-
/* ignore */
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
43
|
// Drop the oldest, then shift .N-1 → .N for N from MAX_ROTATED down to 2.
|
|
56
44
|
const oldest = `${path}.${MAX_ROTATED}`;
|
|
57
45
|
if (existsSync(oldest)) {
|
|
@@ -99,7 +87,7 @@ function logFilesOldestFirst(): string[] {
|
|
|
99
87
|
{ length: MAX_ROTATED },
|
|
100
88
|
(_, i) => `${path}.${MAX_ROTATED - i}`
|
|
101
89
|
);
|
|
102
|
-
return [...rotated,
|
|
90
|
+
return [...rotated, path];
|
|
103
91
|
}
|
|
104
92
|
|
|
105
93
|
function readAllLogs(): string {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* and the VS Code chat.instructionsFilesLocations entry.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { copyFileSync, existsSync,
|
|
7
|
+
import { copyFileSync, existsSync, unlinkSync } from "node:fs";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
9
|
import { platform } from "../../hooks/lib/paths";
|
|
10
10
|
import {
|
|
@@ -63,22 +63,6 @@ log.success(
|
|
|
63
63
|
`Removed ${removedInstructions.length} ~/.copilot/instructions/pal-*.instructions.md`
|
|
64
64
|
);
|
|
65
65
|
|
|
66
|
-
// --- Backward compat: remove old copilot-instructions.md symlink if present ---
|
|
67
|
-
const legacyPath = resolve(COPILOT_DIR, "copilot-instructions.md");
|
|
68
|
-
if (existsSync(legacyPath)) {
|
|
69
|
-
try {
|
|
70
|
-
if (
|
|
71
|
-
lstatSync(legacyPath).isSymbolicLink() &&
|
|
72
|
-
readlinkSync(legacyPath).includes("AGENTS.md")
|
|
73
|
-
) {
|
|
74
|
-
unlinkSync(legacyPath);
|
|
75
|
-
log.success("Removed legacy copilot-instructions.md symlink");
|
|
76
|
-
}
|
|
77
|
-
} catch {
|
|
78
|
-
/* ignore */
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
66
|
// --- Remove ~/.copilot/instructions entry from VS Code settings ---
|
|
83
67
|
const vsSettingsPath = vscodeSettingsFile();
|
|
84
68
|
if (vsSettingsPath && existsSync(vsSettingsPath)) {
|
package/src/targets/lib.ts
CHANGED
|
@@ -211,7 +211,7 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
// Merge permissions.allow (deduplicate)
|
|
214
|
+
// Merge permissions.allow (deduplicate)
|
|
215
215
|
if (template.permissions?.allow) {
|
|
216
216
|
result.permissions ??= {};
|
|
217
217
|
result.permissions.allow ??= [];
|
|
@@ -221,15 +221,6 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
|
-
// Strip ineffective Grep(...)/Glob(...) rules left by older templates. Claude Code
|
|
225
|
-
// governs Grep/Glob via Read(...) rules, so a path-scoped Grep()/Glob() entry never
|
|
226
|
-
// matches and Claude Code warns about it on every prompt. Read(//*) already covers them.
|
|
227
|
-
if (result.permissions?.allow) {
|
|
228
|
-
result.permissions.allow = result.permissions.allow.filter(
|
|
229
|
-
(perm) => !isIneffectiveFileToolRule(perm)
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
224
|
// Merge skillOverrides (object with skill name keys, add if not present)
|
|
234
225
|
if (template.skillOverrides && typeof template.skillOverrides === "object") {
|
|
235
226
|
result.skillOverrides ??= {};
|
|
@@ -488,15 +479,6 @@ function isOutdatedPalHook(cmd: string, palCanonical: Set<string>): boolean {
|
|
|
488
479
|
return palCanonical.has(canonicalPalCmd(cmd)) || runsRemovedPalHook(cmd);
|
|
489
480
|
}
|
|
490
481
|
|
|
491
|
-
/**
|
|
492
|
-
* True for path-scoped Grep()/Glob() allow rules, which Claude Code cannot honor —
|
|
493
|
-
* it resolves Grep/Glob permission through Read(...) rules, so these never match and
|
|
494
|
-
* trigger a warning on every prompt. Bare "Grep"/"Glob" tool allows are left intact.
|
|
495
|
-
*/
|
|
496
|
-
function isIneffectiveFileToolRule(perm: string): boolean {
|
|
497
|
-
return /^(?:Grep|Glob)\(/.test(perm);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
482
|
export function loadCodexHooksTemplate(
|
|
501
483
|
templatePath: string,
|
|
502
484
|
pkgRoot: string
|
|
@@ -697,21 +679,6 @@ export function scaffoldPalSettings(): void {
|
|
|
697
679
|
copyFileSync(src, dst);
|
|
698
680
|
log.info("Created pal-settings.json from template");
|
|
699
681
|
}
|
|
700
|
-
|
|
701
|
-
// Strip deprecated loadAtStartup.files entries from existing installs.
|
|
702
|
-
// mergeSettings only adds, never removes — deprecated entries persist indefinitely otherwise.
|
|
703
|
-
try {
|
|
704
|
-
const raw = JSON.parse(readFileSync(dst, "utf-8"));
|
|
705
|
-
const files: string[] = raw?.loadAtStartup?.files ?? [];
|
|
706
|
-
const cleaned = files.filter((f: string) => !f.endsWith("PROJECTS.md"));
|
|
707
|
-
if (cleaned.length !== files.length) {
|
|
708
|
-
raw.loadAtStartup.files = cleaned;
|
|
709
|
-
writeFileSync(dst, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
|
|
710
|
-
log.info("Removed deprecated PROJECTS.md from loadAtStartup.files");
|
|
711
|
-
}
|
|
712
|
-
} catch {
|
|
713
|
-
/* non-fatal — malformed settings left as-is */
|
|
714
|
-
}
|
|
715
682
|
}
|
|
716
683
|
|
|
717
684
|
// --- PAL docs (modular context routing files) ---
|
|
@@ -3,14 +3,7 @@
|
|
|
3
3
|
* Deploys plugin, installs skills, generates AGENTS.md.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
existsSync,
|
|
8
|
-
mkdirSync,
|
|
9
|
-
readFileSync,
|
|
10
|
-
statSync,
|
|
11
|
-
unlinkSync,
|
|
12
|
-
writeFileSync,
|
|
13
|
-
} from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
14
7
|
import { resolve } from "node:path";
|
|
15
8
|
import { palPkg, platform } from "../../hooks/lib/paths";
|
|
16
9
|
import { getSemiStaticSources } from "../../hooks/lib/semi-static";
|
|
@@ -22,11 +15,7 @@ const OC_PLUGINS_DIR = resolve(OC_GLOBAL_DIR, "plugins");
|
|
|
22
15
|
|
|
23
16
|
mkdirSync(OC_PLUGINS_DIR, { recursive: true });
|
|
24
17
|
|
|
25
|
-
// --- 1. Deploy plugin
|
|
26
|
-
const legacyPlugin = resolve(OC_PLUGINS_DIR, "pai-plugin.ts");
|
|
27
|
-
if (existsSync(legacyPlugin)) {
|
|
28
|
-
unlinkSync(legacyPlugin);
|
|
29
|
-
}
|
|
18
|
+
// --- 1. Deploy plugin ---
|
|
30
19
|
const pluginSrc = resolve(PKG_ROOT, "src", "targets", "opencode", "plugin.ts");
|
|
31
20
|
const pluginDst = resolve(OC_PLUGINS_DIR, "pal-plugin.ts");
|
|
32
21
|
// Embed PKG_ROOT as a hardcoded constant so no env config is needed
|