portable-agent-layer 0.35.0 → 0.37.0
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 -1
- package/assets/skills/analyze-pdf/tools/pdf-download.ts +1 -1
- package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +1 -1
- package/assets/skills/consulting-report/tools/dev.ts +2 -2
- package/assets/skills/consulting-report/tools/generate-pdf.ts +9 -9
- package/assets/skills/consulting-report/tools/scaffold.ts +2 -2
- package/assets/skills/create-pdf/tools/md-to-html-pdf.ts +2 -2
- package/assets/skills/opinion/tools/opinion.ts +3 -2
- package/assets/skills/presentation/SKILL.md +1 -1
- package/assets/skills/presentation/tools/doctor.ts +2 -5
- package/assets/skills/presentation/tools/lib/inline.ts +6 -11
- package/assets/skills/presentation/tools/lib/lint-helpers.ts +2 -2
- package/assets/skills/presentation/tools/lib/lint-rules.ts +5 -2
- package/assets/skills/presentation/tools/setup-template.ts +10 -7
- package/assets/skills/projects/SKILL.md +44 -21
- package/assets/skills/research/tools/gemini-search.ts +2 -2
- package/assets/skills/research/tools/grok-search.ts +2 -2
- package/assets/skills/research/tools/perplexity-search.ts +2 -2
- package/assets/skills/telos/SKILL.md +7 -52
- package/assets/skills/telos/tools/update-telos.ts +0 -1
- package/assets/templates/PAL/ALGORITHM.md +54 -5
- package/assets/templates/PAL/PROJECT_LIFECYCLE.md +48 -0
- package/assets/templates/PAL/README.md +1 -1
- package/assets/templates/PAL/STEERING_RULES.md +4 -0
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +32 -17
- package/assets/templates/PAL/WORK_TRACKING.md +1 -1
- package/assets/templates/hooks.codex.json +44 -0
- package/assets/templates/hooks.cursor.json +11 -5
- package/assets/templates/pal-settings.json +1 -3
- package/assets/templates/settings.claude.json +2 -1
- package/package.json +2 -1
- package/src/cli/index.ts +112 -14
- package/src/cli/migrate.ts +299 -0
- package/src/cli/setup-identity.ts +3 -3
- package/src/cli/setup-telos.ts +12 -80
- package/src/hooks/CompactRecover.ts +11 -5
- package/src/hooks/LoadContext.ts +35 -11
- package/src/hooks/PreCompactPersist.ts +26 -34
- package/src/hooks/SecurityValidator.ts +43 -21
- package/src/hooks/StopOrchestrator.ts +4 -1
- package/src/hooks/UserPromptOrchestrator.ts +4 -2
- package/src/hooks/handlers/auto-graduate.ts +2 -2
- package/src/hooks/handlers/backup.ts +3 -3
- package/src/hooks/handlers/context-digests.ts +74 -0
- package/src/hooks/handlers/failure.ts +5 -3
- package/src/hooks/handlers/inject-retrieval.ts +29 -6
- package/src/hooks/handlers/persist-last-exchange.ts +76 -0
- package/src/hooks/handlers/rating.ts +2 -1
- package/src/hooks/handlers/readme-sync.ts +3 -2
- package/src/hooks/handlers/session-intelligence.ts +17 -93
- package/src/hooks/handlers/session-name.ts +2 -2
- package/src/hooks/handlers/synthesis.ts +5 -2
- package/src/hooks/handlers/update-counts.ts +3 -2
- package/src/hooks/lib/agent.ts +20 -18
- package/src/hooks/lib/claude-md.ts +69 -14
- package/src/hooks/lib/context.ts +92 -246
- package/src/hooks/lib/entities.ts +7 -7
- package/src/hooks/lib/frontmatter.ts +4 -4
- package/src/hooks/lib/graduation.ts +7 -6
- package/src/hooks/lib/inference.ts +6 -2
- package/src/hooks/lib/learning-category.ts +1 -1
- package/src/hooks/lib/learning-store.ts +6 -1
- package/src/hooks/lib/notify.ts +2 -2
- package/src/hooks/lib/opinions.ts +3 -3
- package/src/hooks/lib/paths.ts +2 -0
- package/src/hooks/lib/projects.ts +142 -74
- package/src/hooks/lib/readme-sync.ts +1 -1
- package/src/hooks/lib/relationship.ts +4 -16
- package/src/hooks/lib/retrieval-index.ts +5 -3
- package/src/hooks/lib/retrieval.ts +11 -12
- package/src/hooks/lib/security.ts +24 -18
- package/src/hooks/lib/semi-static.ts +188 -0
- package/src/hooks/lib/session-names.ts +1 -1
- package/src/hooks/lib/settings.ts +1 -1
- package/src/hooks/lib/setup.ts +2 -65
- package/src/hooks/lib/signals.ts +2 -2
- package/src/hooks/lib/stdin.ts +1 -1
- package/src/hooks/lib/stop.ts +16 -6
- package/src/hooks/lib/token-usage.ts +1 -2
- package/src/hooks/lib/transcript.ts +1 -1
- package/src/hooks/lib/wisdom.ts +5 -5
- package/src/hooks/lib/work-tracking.ts +8 -14
- package/src/targets/claude/uninstall.ts +1 -1
- package/src/targets/codex/install.ts +95 -0
- package/src/targets/codex/uninstall.ts +70 -0
- package/src/targets/copilot/install.ts +39 -8
- package/src/targets/copilot/uninstall.ts +58 -17
- package/src/targets/cursor/install.ts +8 -0
- package/src/targets/cursor/uninstall.ts +18 -1
- package/src/targets/lib.ts +166 -14
- package/src/targets/opencode/install.ts +29 -1
- package/src/targets/opencode/plugin.ts +23 -12
- package/src/targets/opencode/uninstall.ts +30 -3
- package/src/tools/agent/algorithm-reflect.ts +1 -1
- package/src/tools/agent/analyze.ts +18 -18
- package/src/tools/agent/handoff-note.ts +116 -0
- package/src/tools/agent/project.ts +375 -75
- package/src/tools/agent/relationship-note.ts +51 -0
- package/src/tools/agent/synthesize.ts +6 -42
- package/src/tools/agent/thread.ts +15 -14
- package/src/tools/agent/wisdom-frame.ts +9 -3
- package/src/tools/import.ts +1 -1
- package/src/tools/relationship-reflect.ts +15 -13
- package/src/tools/self-model.ts +23 -19
- package/src/tools/session-summary.ts +3 -3
- package/src/tools/token-cost.ts +15 -16
- package/assets/skills/telos/tools/update-projects.ts +0 -106
- package/assets/templates/telos/PROJECTS.md +0 -7
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAL — Codex uninstaller
|
|
3
|
+
* Removes only PAL-owned hooks from ~/.codex/hooks.json. Preserves user hooks.
|
|
4
|
+
* Removes PAL skill symlinks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { assets, palPkg, platform } from "../../hooks/lib/paths";
|
|
10
|
+
import {
|
|
11
|
+
loadCodexHooksTemplate,
|
|
12
|
+
log,
|
|
13
|
+
readJson,
|
|
14
|
+
removeSkills,
|
|
15
|
+
unmergeCodexHooks,
|
|
16
|
+
writeJson,
|
|
17
|
+
} from "../lib";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Remove `hooks = true` from config.toml.
|
|
21
|
+
* Also removes a now-empty [features] section header.
|
|
22
|
+
*/
|
|
23
|
+
function disableCodexHooks(configPath: string): void {
|
|
24
|
+
if (!existsSync(configPath)) return;
|
|
25
|
+
let content = readFileSync(configPath, "utf-8");
|
|
26
|
+
if (!/^\s*hooks\s*=\s*true/m.test(content)) return;
|
|
27
|
+
|
|
28
|
+
// Remove the hooks line
|
|
29
|
+
content = content.replace(/^[ \t]*hooks\s*=\s*true[ \t]*\n?/m, "");
|
|
30
|
+
|
|
31
|
+
// Remove [features] header if it's now empty (nothing between it and next section / EOF)
|
|
32
|
+
content = content.replace(/\[features\]\n(?=\[|$)/m, "");
|
|
33
|
+
|
|
34
|
+
writeFileSync(configPath, content, "utf-8");
|
|
35
|
+
log.success("Removed hooks from ~/.codex/config.toml");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PKG_ROOT = palPkg().replaceAll("\\", "/");
|
|
39
|
+
const CODEX_DIR = platform.codexDir();
|
|
40
|
+
const HOOKS_FILE = resolve(CODEX_DIR, "hooks.json");
|
|
41
|
+
|
|
42
|
+
// --- Remove PAL hooks from hooks.json ---
|
|
43
|
+
if (existsSync(HOOKS_FILE)) {
|
|
44
|
+
copyFileSync(HOOKS_FILE, `${HOOKS_FILE}.bak.${Date.now()}`);
|
|
45
|
+
log.info("Backed up hooks.json");
|
|
46
|
+
|
|
47
|
+
const template = loadCodexHooksTemplate(assets.codexHooksTemplate(), PKG_ROOT);
|
|
48
|
+
const existing = readJson<Record<string, unknown>>(HOOKS_FILE, {});
|
|
49
|
+
const cleaned = unmergeCodexHooks(existing, template);
|
|
50
|
+
|
|
51
|
+
writeJson(HOOKS_FILE, cleaned);
|
|
52
|
+
log.success("Removed PAL hooks from ~/.codex/hooks.json");
|
|
53
|
+
} else {
|
|
54
|
+
log.info("No hooks.json found, nothing to do");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- Remove PAL skill symlinks ---
|
|
58
|
+
const codexSkillsDir = resolve(CODEX_DIR, "skills");
|
|
59
|
+
const removed = removeSkills(codexSkillsDir);
|
|
60
|
+
if (removed.length > 0) {
|
|
61
|
+
log.success(`Removed ${removed.length} skill(s): ${removed.join(", ")}`);
|
|
62
|
+
} else {
|
|
63
|
+
log.info("No PAL skills found");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- Disable hooks in config.toml ---
|
|
67
|
+
const CONFIG_FILE = resolve(CODEX_DIR, "config.toml");
|
|
68
|
+
disableCodexHooks(CONFIG_FILE);
|
|
69
|
+
|
|
70
|
+
log.success("Codex uninstall complete");
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PAL — Copilot target installer
|
|
3
3
|
* Writes hooks to ~/.copilot/hooks/pal-hooks.json.
|
|
4
|
-
* Copies skills and agents.
|
|
4
|
+
* Copies skills and agents. Writes ~/.copilot/instructions/pal-*.instructions.md.
|
|
5
|
+
* Enables ~/.copilot/instructions in VS Code chat.instructionsFilesLocations.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
8
9
|
import { resolve } from "node:path";
|
|
10
|
+
import { writeContextDigests } from "../../hooks/handlers/context-digests";
|
|
9
11
|
import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
|
|
10
12
|
import { assets, palPkg, platform } from "../../hooks/lib/paths";
|
|
11
13
|
import {
|
|
@@ -16,7 +18,10 @@ import {
|
|
|
16
18
|
generateSkillIndex,
|
|
17
19
|
loadCopilotHooksTemplate,
|
|
18
20
|
log,
|
|
21
|
+
readJson,
|
|
19
22
|
scaffoldPalSettings,
|
|
23
|
+
vscodeSettingsFile,
|
|
24
|
+
writeJson,
|
|
20
25
|
} from "../lib";
|
|
21
26
|
|
|
22
27
|
const PKG_ROOT = palPkg().replaceAll("\\", "/");
|
|
@@ -50,16 +55,42 @@ log.success(`Installed ${palDocsCount} PAL docs to ~/.pal/docs/`);
|
|
|
50
55
|
// --- Scaffold PAL settings ---
|
|
51
56
|
scaffoldPalSettings();
|
|
52
57
|
|
|
53
|
-
// --- Generate AGENTS.md
|
|
54
|
-
// ensureSymlinks() inside regenerateIfNeeded() handles the symlink once ~/.copilot/ exists
|
|
58
|
+
// --- Generate AGENTS.md ---
|
|
55
59
|
regenerateIfNeeded();
|
|
56
|
-
|
|
60
|
+
log.success("Generated AGENTS.md");
|
|
61
|
+
|
|
62
|
+
// --- Write ~/.copilot/instructions/pal-*.instructions.md ---
|
|
63
|
+
mkdirSync(resolve(COPILOT_DIR, "instructions"), { recursive: true });
|
|
64
|
+
writeContextDigests();
|
|
57
65
|
log.success(
|
|
58
|
-
|
|
59
|
-
? "copilot-instructions.md symlink present"
|
|
60
|
-
: "Generated AGENTS.md (copilot-instructions.md symlink will be created on next session)"
|
|
66
|
+
"Written ~/.copilot/instructions/pal-self-model + pal-wisdom + pal-opinions.instructions.md"
|
|
61
67
|
);
|
|
62
68
|
|
|
69
|
+
// --- Enable ~/.copilot/instructions in VS Code settings ---
|
|
70
|
+
const vsSettingsPath = vscodeSettingsFile();
|
|
71
|
+
const manualHint =
|
|
72
|
+
'Add manually: { "chat.instructionsFilesLocations": { "~/.copilot/instructions": true } }';
|
|
73
|
+
if (vsSettingsPath) {
|
|
74
|
+
try {
|
|
75
|
+
const settings = readJson<Record<string, unknown>>(vsSettingsPath, {});
|
|
76
|
+
const existing =
|
|
77
|
+
typeof settings["chat.instructionsFilesLocations"] === "object" &&
|
|
78
|
+
settings["chat.instructionsFilesLocations"] !== null
|
|
79
|
+
? (settings["chat.instructionsFilesLocations"] as Record<string, unknown>)
|
|
80
|
+
: {};
|
|
81
|
+
settings["chat.instructionsFilesLocations"] = {
|
|
82
|
+
...existing,
|
|
83
|
+
"~/.copilot/instructions": true,
|
|
84
|
+
};
|
|
85
|
+
writeJson(vsSettingsPath, settings);
|
|
86
|
+
log.success("Enabled ~/.copilot/instructions in VS Code settings");
|
|
87
|
+
} catch {
|
|
88
|
+
log.warn(`Could not update VS Code settings — ${manualHint}`);
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
log.warn(`Could not detect VS Code settings path — ${manualHint}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
log.success("Copilot installation complete");
|
|
64
95
|
console.log("");
|
|
65
96
|
log.info(`Skills: ${countSkills()}`);
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PAL — Copilot uninstaller
|
|
3
|
-
* Removes pal-hooks.json, skill symlinks, agents,
|
|
3
|
+
* Removes pal-hooks.json, skill symlinks, agents, instruction files,
|
|
4
|
+
* and the VS Code chat.instructionsFilesLocations entry.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import { copyFileSync, existsSync, lstatSync, readlinkSync, unlinkSync } from "node:fs";
|
|
7
8
|
import { resolve } from "node:path";
|
|
8
9
|
import { platform } from "../../hooks/lib/paths";
|
|
9
|
-
import {
|
|
10
|
+
import { copilotFilename, getSemiStaticSources } from "../../hooks/lib/semi-static";
|
|
11
|
+
import {
|
|
12
|
+
log,
|
|
13
|
+
readJson,
|
|
14
|
+
removeAgentsFromCopilot,
|
|
15
|
+
removePalDocs,
|
|
16
|
+
removeSkills,
|
|
17
|
+
vscodeSettingsFile,
|
|
18
|
+
writeJson,
|
|
19
|
+
} from "../lib";
|
|
10
20
|
|
|
11
21
|
const COPILOT_DIR = platform.copilotDir();
|
|
12
22
|
const HOOKS_FILE = resolve(COPILOT_DIR, "hooks", "pal-hooks.json");
|
|
@@ -35,26 +45,57 @@ if (removedAgents.length > 0) {
|
|
|
35
45
|
log.success(`Removed ${removedAgents.length} agent(s): ${removedAgents.join(", ")}`);
|
|
36
46
|
}
|
|
37
47
|
|
|
38
|
-
// --- Remove
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
// --- Remove PAL docs ---
|
|
49
|
+
removePalDocs();
|
|
50
|
+
|
|
51
|
+
// --- Remove ~/.copilot/instructions/pal-*.instructions.md ---
|
|
52
|
+
for (const src of getSemiStaticSources()) {
|
|
53
|
+
try {
|
|
54
|
+
unlinkSync(resolve(COPILOT_DIR, "instructions", copilotFilename(src)));
|
|
55
|
+
} catch {
|
|
56
|
+
/* gone */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// pal-session.instructions.md is written by LoadContext (not a semi-static source)
|
|
60
|
+
try {
|
|
61
|
+
unlinkSync(resolve(COPILOT_DIR, "instructions", "pal-session.instructions.md"));
|
|
62
|
+
} catch {
|
|
63
|
+
/* gone */
|
|
64
|
+
}
|
|
65
|
+
log.success("Removed ~/.copilot/instructions/pal-*.instructions.md");
|
|
66
|
+
|
|
67
|
+
// --- Backward compat: remove old copilot-instructions.md symlink if present ---
|
|
68
|
+
const legacyPath = resolve(COPILOT_DIR, "copilot-instructions.md");
|
|
69
|
+
if (existsSync(legacyPath)) {
|
|
41
70
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
} else {
|
|
49
|
-
log.info("copilot-instructions.md is not a PAL symlink — leaving it");
|
|
50
|
-
}
|
|
71
|
+
if (
|
|
72
|
+
lstatSync(legacyPath).isSymbolicLink() &&
|
|
73
|
+
readlinkSync(legacyPath).includes("AGENTS.md")
|
|
74
|
+
) {
|
|
75
|
+
unlinkSync(legacyPath);
|
|
76
|
+
log.success("Removed legacy copilot-instructions.md symlink");
|
|
51
77
|
}
|
|
52
78
|
} catch {
|
|
53
|
-
|
|
79
|
+
/* ignore */
|
|
54
80
|
}
|
|
55
81
|
}
|
|
56
82
|
|
|
57
|
-
// --- Remove
|
|
58
|
-
|
|
83
|
+
// --- Remove ~/.copilot/instructions entry from VS Code settings ---
|
|
84
|
+
const vsSettingsPath = vscodeSettingsFile();
|
|
85
|
+
if (vsSettingsPath && existsSync(vsSettingsPath)) {
|
|
86
|
+
const settings = readJson<Record<string, unknown>>(vsSettingsPath, {});
|
|
87
|
+
const locs = settings["chat.instructionsFilesLocations"];
|
|
88
|
+
if (typeof locs === "object" && locs !== null) {
|
|
89
|
+
const obj = locs as Record<string, unknown>;
|
|
90
|
+
delete obj["~/.copilot/instructions"];
|
|
91
|
+
if (Object.keys(obj).length === 0) {
|
|
92
|
+
delete settings["chat.instructionsFilesLocations"];
|
|
93
|
+
} else {
|
|
94
|
+
settings["chat.instructionsFilesLocations"] = obj;
|
|
95
|
+
}
|
|
96
|
+
writeJson(vsSettingsPath, settings);
|
|
97
|
+
log.success("Removed ~/.copilot/instructions from VS Code settings");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
59
100
|
|
|
60
101
|
log.success("Copilot uninstall complete");
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
|
+
import { writeContextDigests } from "../../hooks/handlers/context-digests";
|
|
9
10
|
import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
|
|
10
11
|
import { assets, palPkg, platform } from "../../hooks/lib/paths";
|
|
11
12
|
import {
|
|
@@ -64,6 +65,13 @@ scaffoldPalSettings();
|
|
|
64
65
|
regenerateIfNeeded();
|
|
65
66
|
log.success("Generated AGENTS.md");
|
|
66
67
|
|
|
68
|
+
// --- Write ~/.cursor/rules/pal-*.mdc ---
|
|
69
|
+
mkdirSync(resolve(CURSOR_DIR, "rules"), { recursive: true });
|
|
70
|
+
writeContextDigests();
|
|
71
|
+
log.success(
|
|
72
|
+
"Written ~/.cursor/rules/pal-self-model.mdc + pal-wisdom.mdc + pal-opinions.mdc"
|
|
73
|
+
);
|
|
74
|
+
|
|
67
75
|
log.success("Cursor installation complete");
|
|
68
76
|
console.log("");
|
|
69
77
|
log.info(`Skills: ${countSkills()}`);
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Removes PAL skill symlinks.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { copyFileSync, existsSync } from "node:fs";
|
|
7
|
+
import { copyFileSync, existsSync, unlinkSync } from "node:fs";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
9
|
import { assets, palPkg, platform } from "../../hooks/lib/paths";
|
|
10
|
+
import { cursorFilename, getSemiStaticSources } from "../../hooks/lib/semi-static";
|
|
10
11
|
import {
|
|
11
12
|
loadCursorHooksTemplate,
|
|
12
13
|
log,
|
|
@@ -56,4 +57,20 @@ if (removedAgents.length > 0) {
|
|
|
56
57
|
// --- Remove PAL system docs ---
|
|
57
58
|
removePalDocs();
|
|
58
59
|
|
|
60
|
+
// --- Remove ~/.cursor/rules/pal-*.mdc ---
|
|
61
|
+
for (const src of getSemiStaticSources()) {
|
|
62
|
+
try {
|
|
63
|
+
unlinkSync(resolve(CURSOR_DIR, "rules", cursorFilename(src)));
|
|
64
|
+
} catch {
|
|
65
|
+
/* gone */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Backward compat: remove legacy merged file if present
|
|
69
|
+
try {
|
|
70
|
+
unlinkSync(resolve(CURSOR_DIR, "rules", "pal-context.mdc"));
|
|
71
|
+
} catch {
|
|
72
|
+
/* gone */
|
|
73
|
+
}
|
|
74
|
+
log.success("Removed ~/.cursor/rules/pal-*.mdc");
|
|
75
|
+
|
|
59
76
|
log.success("Cursor uninstall complete");
|
package/src/targets/lib.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
unlinkSync,
|
|
15
15
|
writeFileSync,
|
|
16
16
|
} from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
17
18
|
import { resolve } from "node:path";
|
|
18
19
|
import { assets, palHome, platform } from "../hooks/lib/paths";
|
|
19
20
|
|
|
@@ -41,6 +42,31 @@ export function writeJson(path: string, data: unknown): void {
|
|
|
41
42
|
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
/** Resolve the VS Code user settings.json path cross-platform. Returns null on unknown platforms. */
|
|
46
|
+
export function vscodeSettingsFile(): string | null {
|
|
47
|
+
const h = homedir();
|
|
48
|
+
if (process.platform === "darwin") {
|
|
49
|
+
return resolve(h, "Library", "Application Support", "Code", "User", "settings.json");
|
|
50
|
+
}
|
|
51
|
+
if (process.platform === "linux") {
|
|
52
|
+
return resolve(
|
|
53
|
+
process.env.XDG_CONFIG_HOME ?? resolve(h, ".config"),
|
|
54
|
+
"Code",
|
|
55
|
+
"User",
|
|
56
|
+
"settings.json"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (process.platform === "win32") {
|
|
60
|
+
return resolve(
|
|
61
|
+
process.env.APPDATA ?? resolve(h, "AppData", "Roaming"),
|
|
62
|
+
"Code",
|
|
63
|
+
"User",
|
|
64
|
+
"settings.json"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
44
70
|
// --- Settings template merge/unmerge ---
|
|
45
71
|
|
|
46
72
|
type HookEntry = { matcher?: string; hooks?: Array<{ type: string; command: string }> };
|
|
@@ -55,7 +81,11 @@ type Settings = Record<string, unknown> & {
|
|
|
55
81
|
export function loadSettingsTemplate(templatePath: string, pkgRoot: string): Settings {
|
|
56
82
|
const raw = readFileSync(templatePath, "utf-8");
|
|
57
83
|
const resolved = raw.replaceAll("{{PKG_ROOT}}", pkgRoot);
|
|
58
|
-
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(resolved) as Settings;
|
|
86
|
+
} catch (e) {
|
|
87
|
+
throw new Error(`Failed to parse settings template at ${templatePath}: ${e}`);
|
|
88
|
+
}
|
|
59
89
|
}
|
|
60
90
|
|
|
61
91
|
/**
|
|
@@ -69,7 +99,7 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
|
|
|
69
99
|
|
|
70
100
|
// Merge hooks (deduplicate by command)
|
|
71
101
|
if (template.hooks) {
|
|
72
|
-
|
|
102
|
+
result.hooks ??= {};
|
|
73
103
|
for (const [event, entries] of Object.entries(template.hooks)) {
|
|
74
104
|
const current = result.hooks[event] ?? [];
|
|
75
105
|
for (const entry of entries) {
|
|
@@ -84,8 +114,8 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
|
|
|
84
114
|
|
|
85
115
|
// Merge permissions.allow (deduplicate)
|
|
86
116
|
if (template.permissions?.allow) {
|
|
87
|
-
|
|
88
|
-
|
|
117
|
+
result.permissions ??= {};
|
|
118
|
+
result.permissions.allow ??= [];
|
|
89
119
|
for (const perm of template.permissions.allow) {
|
|
90
120
|
if (!result.permissions.allow.includes(perm)) {
|
|
91
121
|
result.permissions.allow.push(perm);
|
|
@@ -158,7 +188,11 @@ export function loadCursorHooksTemplate(
|
|
|
158
188
|
): CursorHooks {
|
|
159
189
|
const raw = readFileSync(templatePath, "utf-8");
|
|
160
190
|
const resolved = raw.replaceAll("{{PKG_ROOT}}", pkgRoot);
|
|
161
|
-
|
|
191
|
+
try {
|
|
192
|
+
return JSON.parse(resolved) as CursorHooks;
|
|
193
|
+
} catch (e) {
|
|
194
|
+
throw new Error(`Failed to parse Cursor hooks template at ${templatePath}: ${e}`);
|
|
195
|
+
}
|
|
162
196
|
}
|
|
163
197
|
|
|
164
198
|
/**
|
|
@@ -172,7 +206,7 @@ export function mergeCursorHooks(
|
|
|
172
206
|
const result: CursorHooks = { ...existing, version: existing.version ?? 1 };
|
|
173
207
|
|
|
174
208
|
if (template.hooks) {
|
|
175
|
-
|
|
209
|
+
result.hooks ??= {};
|
|
176
210
|
for (const [event, entries] of Object.entries(template.hooks)) {
|
|
177
211
|
const current = result.hooks[event] ?? [];
|
|
178
212
|
for (const entry of entries) {
|
|
@@ -215,6 +249,103 @@ export function unmergeCursorHooks(
|
|
|
215
249
|
return result;
|
|
216
250
|
}
|
|
217
251
|
|
|
252
|
+
// --- Codex hooks (nested group format, distinct from Cursor's flat format) ---
|
|
253
|
+
|
|
254
|
+
type CodexHookCommand = { type: string; command: string; timeout?: number };
|
|
255
|
+
type CodexHookGroup = { matcher?: string; hooks: CodexHookCommand[] };
|
|
256
|
+
type CodexHooks = { hooks?: Record<string, CodexHookGroup[]> };
|
|
257
|
+
|
|
258
|
+
/** Strip leading env-var assignments so "PAL_AGENT=x bun run ..." → "bun run ..." */
|
|
259
|
+
function canonicalCmd(cmd: string): string {
|
|
260
|
+
return cmd.replace(/^(?:\w+=\S+\s+)+/, "");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function loadCodexHooksTemplate(
|
|
264
|
+
templatePath: string,
|
|
265
|
+
pkgRoot: string
|
|
266
|
+
): CodexHooks {
|
|
267
|
+
const raw = readFileSync(templatePath, "utf-8");
|
|
268
|
+
const resolved = raw.replaceAll("{{PKG_ROOT}}", pkgRoot);
|
|
269
|
+
try {
|
|
270
|
+
return JSON.parse(resolved) as CodexHooks;
|
|
271
|
+
} catch (e) {
|
|
272
|
+
throw new Error(`Failed to parse Codex hooks template at ${templatePath}: ${e}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Merge PAL hooks into an existing Codex hooks.json. Deduplicates by canonical command path. */
|
|
277
|
+
export function mergeCodexHooks(existing: CodexHooks, template: CodexHooks): CodexHooks {
|
|
278
|
+
const result: CodexHooks = { ...existing };
|
|
279
|
+
if (!template.hooks) return result;
|
|
280
|
+
result.hooks ??= {};
|
|
281
|
+
|
|
282
|
+
// Collect canonical paths of PAL template commands so we can evict stale variants
|
|
283
|
+
const palCanonical = new Set(
|
|
284
|
+
Object.values(template.hooks).flatMap((groups) =>
|
|
285
|
+
groups.flatMap((g) => g.hooks.map((h) => canonicalCmd(h.command)))
|
|
286
|
+
)
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
// Strip any existing entries (nested or flat) whose canonical path matches a PAL command
|
|
290
|
+
for (const event of Object.keys(result.hooks)) {
|
|
291
|
+
result.hooks[event] = (result.hooks[event] ?? [])
|
|
292
|
+
.map((g) => {
|
|
293
|
+
const flat = g as unknown as CodexHookCommand;
|
|
294
|
+
if (!g.hooks && flat.command && palCanonical.has(canonicalCmd(flat.command))) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const filtered = (g.hooks ?? []).filter(
|
|
298
|
+
(h) => !palCanonical.has(canonicalCmd(h.command))
|
|
299
|
+
);
|
|
300
|
+
return filtered.length > 0 ? { ...g, hooks: filtered } : null;
|
|
301
|
+
})
|
|
302
|
+
.filter((g): g is CodexHookGroup => g !== null);
|
|
303
|
+
if (result.hooks[event].length === 0) delete result.hooks[event];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Add fresh template entries
|
|
307
|
+
for (const [event, groups] of Object.entries(template.hooks)) {
|
|
308
|
+
const current = result.hooks[event] ?? [];
|
|
309
|
+
for (const group of groups) current.push(group);
|
|
310
|
+
result.hooks[event] = current;
|
|
311
|
+
}
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Remove PAL hooks from an existing Codex hooks.json. Preserves user hooks. */
|
|
316
|
+
export function unmergeCodexHooks(
|
|
317
|
+
existing: CodexHooks,
|
|
318
|
+
template: CodexHooks
|
|
319
|
+
): CodexHooks {
|
|
320
|
+
const result: CodexHooks = { ...existing };
|
|
321
|
+
if (!template.hooks || !result.hooks) return result;
|
|
322
|
+
|
|
323
|
+
// Match by canonical path so prefix variants (PAL_AGENT=codex, etc.) are all removed
|
|
324
|
+
const palCanonical = new Set(
|
|
325
|
+
Object.values(template.hooks).flatMap((groups) =>
|
|
326
|
+
groups.flatMap((g) => g.hooks.map((h) => canonicalCmd(h.command)))
|
|
327
|
+
)
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
for (const event of Object.keys(result.hooks)) {
|
|
331
|
+
result.hooks[event] = (result.hooks[event] ?? [])
|
|
332
|
+
.map((g) => {
|
|
333
|
+
const flat = g as unknown as CodexHookCommand;
|
|
334
|
+
if (!g.hooks && flat.command && palCanonical.has(canonicalCmd(flat.command))) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
const filtered = (g.hooks ?? []).filter(
|
|
338
|
+
(h) => !palCanonical.has(canonicalCmd(h.command))
|
|
339
|
+
);
|
|
340
|
+
return filtered.length > 0 ? { ...g, hooks: filtered } : null;
|
|
341
|
+
})
|
|
342
|
+
.filter((g): g is CodexHookGroup => g !== null);
|
|
343
|
+
if (result.hooks[event].length === 0) delete result.hooks[event];
|
|
344
|
+
}
|
|
345
|
+
if (Object.keys(result.hooks).length === 0) delete result.hooks;
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
|
|
218
349
|
// --- TELOS scaffolding ---
|
|
219
350
|
|
|
220
351
|
/** Copy template files into telos/ without overwriting existing ones */
|
|
@@ -249,6 +380,21 @@ export function scaffoldPalSettings(): void {
|
|
|
249
380
|
copyFileSync(src, dst);
|
|
250
381
|
log.info("Created pal-settings.json from template");
|
|
251
382
|
}
|
|
383
|
+
|
|
384
|
+
// Strip deprecated loadAtStartup.files entries from existing installs.
|
|
385
|
+
// mergeSettings only adds, never removes — deprecated entries persist indefinitely otherwise.
|
|
386
|
+
try {
|
|
387
|
+
const raw = JSON.parse(readFileSync(dst, "utf-8"));
|
|
388
|
+
const files: string[] = raw?.loadAtStartup?.files ?? [];
|
|
389
|
+
const cleaned = files.filter((f: string) => !f.endsWith("PROJECTS.md"));
|
|
390
|
+
if (cleaned.length !== files.length) {
|
|
391
|
+
raw.loadAtStartup.files = cleaned;
|
|
392
|
+
writeFileSync(dst, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
|
|
393
|
+
log.info("Removed deprecated PROJECTS.md from loadAtStartup.files");
|
|
394
|
+
}
|
|
395
|
+
} catch {
|
|
396
|
+
/* non-fatal — malformed settings left as-is */
|
|
397
|
+
}
|
|
252
398
|
}
|
|
253
399
|
|
|
254
400
|
// --- PAL docs (modular context routing files) ---
|
|
@@ -466,14 +612,14 @@ function extractAgentForPlatform(content: string, platform: AgentPlatform): stri
|
|
|
466
612
|
for (const line of frontmatter.split("\n")) {
|
|
467
613
|
if (!line.trim()) continue;
|
|
468
614
|
|
|
469
|
-
const platformMatch =
|
|
615
|
+
const platformMatch = new RegExp(/^(claude|opencode|cursor):\s*$/).exec(line);
|
|
470
616
|
if (platformMatch) {
|
|
471
617
|
currentPlatform = platformMatch[1] as AgentPlatform;
|
|
472
618
|
continue;
|
|
473
619
|
}
|
|
474
620
|
|
|
475
621
|
if (currentPlatform) {
|
|
476
|
-
if (
|
|
622
|
+
if (new RegExp(/^ {2}/).exec(line)) {
|
|
477
623
|
platformLines[currentPlatform].push(line.slice(2)); // un-indent one level
|
|
478
624
|
continue;
|
|
479
625
|
}
|
|
@@ -554,9 +700,15 @@ export function removeAgentsFromCopilot(copilotAgentsDir: string): string[] {
|
|
|
554
700
|
|
|
555
701
|
/** Load and resolve the Copilot hooks template, substituting PKG_ROOT */
|
|
556
702
|
export function loadCopilotHooksTemplate(templatePath: string, pkgRoot: string): unknown {
|
|
557
|
-
|
|
558
|
-
|
|
703
|
+
const resolved = readFileSync(templatePath, "utf-8").replaceAll(
|
|
704
|
+
"{{PKG_ROOT}}",
|
|
705
|
+
pkgRoot
|
|
559
706
|
);
|
|
707
|
+
try {
|
|
708
|
+
return JSON.parse(resolved);
|
|
709
|
+
} catch (e) {
|
|
710
|
+
throw new Error(`Failed to parse Copilot hooks template at ${templatePath}: ${e}`);
|
|
711
|
+
}
|
|
560
712
|
}
|
|
561
713
|
|
|
562
714
|
// --- Skill Index ---
|
|
@@ -578,7 +730,7 @@ function extractTriggers(description: string): string[] {
|
|
|
578
730
|
// Extract "Use when ..." phrases and key terms
|
|
579
731
|
const triggers = new Set<string>();
|
|
580
732
|
|
|
581
|
-
const useWhen =
|
|
733
|
+
const useWhen = new RegExp(/Use when\s+(.+?)(?:\.|$)/i).exec(description);
|
|
582
734
|
if (useWhen) {
|
|
583
735
|
const words = useWhen[1]
|
|
584
736
|
.toLowerCase()
|
|
@@ -621,12 +773,12 @@ export function generateSkillIndex(): number {
|
|
|
621
773
|
|
|
622
774
|
try {
|
|
623
775
|
const content = readFileSync(skillMd, "utf-8");
|
|
624
|
-
const fmMatch =
|
|
776
|
+
const fmMatch = new RegExp(/^---\n([\s\S]*?)\n---/).exec(content);
|
|
625
777
|
if (!fmMatch) continue;
|
|
626
778
|
|
|
627
779
|
const fm = fmMatch[1];
|
|
628
|
-
const nameMatch =
|
|
629
|
-
const descMatch =
|
|
780
|
+
const nameMatch = new RegExp(/^name:\s*(.+)$/m).exec(fm);
|
|
781
|
+
const descMatch = new RegExp(/^description:\s*"?(.+?)"?\s*$/m).exec(fm);
|
|
630
782
|
if (!nameMatch) continue;
|
|
631
783
|
|
|
632
784
|
const skillName = nameMatch[1].trim();
|
|
@@ -3,10 +3,18 @@
|
|
|
3
3
|
* Deploys plugin, installs skills, generates AGENTS.md.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
statSync,
|
|
11
|
+
unlinkSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
7
14
|
import { resolve } from "node:path";
|
|
8
15
|
import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
|
|
9
16
|
import { palPkg, platform } from "../../hooks/lib/paths";
|
|
17
|
+
import { getSemiStaticSources } from "../../hooks/lib/semi-static";
|
|
10
18
|
import {
|
|
11
19
|
copyAgentsForOpencode,
|
|
12
20
|
copyPalDocs,
|
|
@@ -70,6 +78,26 @@ log.success(`Installed ${palDocsCount} PAL docs to ~/.pal/docs/`);
|
|
|
70
78
|
regenerateIfNeeded();
|
|
71
79
|
log.success("Generated ~/.config/opencode/AGENTS.md");
|
|
72
80
|
|
|
81
|
+
// --- 7. Add semi-static digest files to instructions[] in config.json ---
|
|
82
|
+
const configPath = resolve(OC_GLOBAL_DIR, "config.json");
|
|
83
|
+
const staticFiles = getSemiStaticSources().map((s) => s.path);
|
|
84
|
+
let ocConfig: Record<string, unknown> = {};
|
|
85
|
+
if (existsSync(configPath) && statSync(configPath).size > 0) {
|
|
86
|
+
try {
|
|
87
|
+
ocConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
88
|
+
} catch {
|
|
89
|
+
/* start fresh */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const existingInstructions = Array.isArray(ocConfig.instructions)
|
|
93
|
+
? (ocConfig.instructions as string[])
|
|
94
|
+
: [];
|
|
95
|
+
ocConfig.instructions = [...new Set([...existingInstructions, ...staticFiles])];
|
|
96
|
+
writeFileSync(configPath, `${JSON.stringify(ocConfig, null, 2)}\n`, "utf-8");
|
|
97
|
+
log.success(
|
|
98
|
+
`Updated config.json: ${(ocConfig.instructions as string[]).length} instructions`
|
|
99
|
+
);
|
|
100
|
+
|
|
73
101
|
log.success("opencode installation complete");
|
|
74
102
|
console.log("");
|
|
75
103
|
log.info(`Plugin: ${pluginDst}`);
|
|
@@ -34,6 +34,9 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
34
34
|
const { captureRating } = await lib<typeof import("../../hooks/handlers/rating")>(
|
|
35
35
|
"../handlers/rating.ts"
|
|
36
36
|
);
|
|
37
|
+
const { getRetrievalReminder } = await lib<
|
|
38
|
+
typeof import("../../hooks/handlers/inject-retrieval")
|
|
39
|
+
>("../handlers/inject-retrieval.ts");
|
|
37
40
|
|
|
38
41
|
function partsToText(parts: Array<Record<string, unknown>>): string {
|
|
39
42
|
return parts
|
|
@@ -74,7 +77,7 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
74
77
|
return {
|
|
75
78
|
// --- Per-message: Inject dynamic system reminder ---
|
|
76
79
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
77
|
-
const reminder = buildSystemReminder();
|
|
80
|
+
const reminder = buildSystemReminder({ agent: "opencode" });
|
|
78
81
|
if (reminder) output.system.push(reminder);
|
|
79
82
|
},
|
|
80
83
|
|
|
@@ -120,17 +123,25 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
120
123
|
|
|
121
124
|
// --- Capture ratings + session naming from user messages (shared handlers) ---
|
|
122
125
|
"chat.message": async (input, output) => {
|
|
123
|
-
const text =
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
126
|
+
const text = partsToText(output.parts ?? []);
|
|
127
|
+
if (!text.trim()) return;
|
|
128
|
+
|
|
129
|
+
const [, , retrievalResult] = await Promise.allSettled([
|
|
130
|
+
captureRating(text, input.sessionID),
|
|
131
|
+
captureSessionName(text, input.sessionID),
|
|
132
|
+
getRetrievalReminder(text),
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
if (retrievalResult.status === "fulfilled" && retrievalResult.value) {
|
|
136
|
+
const injected = {
|
|
137
|
+
id: `pal-retrieval-${Date.now()}`,
|
|
138
|
+
sessionID: input.sessionID,
|
|
139
|
+
messageID: input.messageID ?? `pal-msg-${Date.now()}`,
|
|
140
|
+
type: "text" as const,
|
|
141
|
+
text: retrievalResult.value,
|
|
142
|
+
synthetic: true,
|
|
143
|
+
};
|
|
144
|
+
output.parts = [injected, ...(output.parts ?? [])];
|
|
134
145
|
}
|
|
135
146
|
},
|
|
136
147
|
|