portable-agent-layer 0.15.1 → 0.16.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 +3 -1
- package/assets/templates/AGENTS.md.template +3 -3
- package/assets/templates/hooks.cursor.json +33 -0
- package/package.json +1 -1
- package/src/cli/index.ts +43 -12
- package/src/hooks/LoadContext.ts +14 -4
- package/src/hooks/SecurityValidator.ts +11 -7
- package/src/hooks/SkillGuard.ts +6 -7
- package/src/hooks/StopOrchestrator.ts +8 -1
- package/src/hooks/lib/agent.ts +40 -0
- package/src/hooks/lib/paths.ts +2 -0
- package/src/targets/cursor/install.ts +65 -0
- package/src/targets/cursor/uninstall.ts +52 -0
- package/src/targets/lib.ts +79 -0
package/README.md
CHANGED
|
@@ -90,7 +90,8 @@ pal cli status # check your setup
|
|
|
90
90
|
```bash
|
|
91
91
|
pal cli install --claude # Claude Code only
|
|
92
92
|
pal cli install --opencode # opencode only
|
|
93
|
-
pal cli install
|
|
93
|
+
pal cli install --cursor # Cursor only
|
|
94
|
+
pal cli install # all available (default)
|
|
94
95
|
```
|
|
95
96
|
|
|
96
97
|
---
|
|
@@ -112,6 +113,7 @@ pal cli install # both (default)
|
|
|
112
113
|
| `PAL_PKG` | Override package root |
|
|
113
114
|
| `PAL_CLAUDE_DIR` | Override Claude config dir (default: `~/.claude`) |
|
|
114
115
|
| `PAL_OPENCODE_DIR` | Override opencode config dir (default: `~/.config/opencode`) |
|
|
116
|
+
| `PAL_CURSOR_DIR` | Override Cursor config dir (default: `~/.cursor`) |
|
|
115
117
|
| `PAL_AGENTS_DIR` | Override agents dir (default: `~/.agents`) |
|
|
116
118
|
|
|
117
119
|
---
|
|
@@ -19,7 +19,7 @@ Your first output MUST be the mode header. No freeform output. No skipping this
|
|
|
19
19
|
For greetings and brief acknowledgments only.
|
|
20
20
|
|
|
21
21
|
```
|
|
22
|
-
|
|
22
|
+
══════ PAL ══════
|
|
23
23
|
🗣️ {{IDENTITY_NAME}}: [response]
|
|
24
24
|
```
|
|
25
25
|
|
|
@@ -28,7 +28,7 @@ For greetings and brief acknowledgments only.
|
|
|
28
28
|
FOR: Simple tasks that won't take much effort or time. More advanced tasks use ALGORITHM below.
|
|
29
29
|
|
|
30
30
|
```
|
|
31
|
-
|
|
31
|
+
══════ PAL | NATIVE ══════
|
|
32
32
|
🗒️ TASK: [brief description]
|
|
33
33
|
[work]
|
|
34
34
|
🔄 ITERATION on: [16 words of context if this is a follow-up]
|
|
@@ -47,7 +47,7 @@ FOR: Multi-step, complex, or difficult work. Troubleshooting, debugging, buildin
|
|
|
47
47
|
**MANDATORY FIRST ACTION:** Read `~/.agents/PAL/ALGORITHM.md` and follow its instructions exactly.
|
|
48
48
|
|
|
49
49
|
Start your response with the following header in this mode:
|
|
50
|
-
|
|
50
|
+
══════ PAL | ALGORITHM ══════
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"hooks": {
|
|
4
|
+
"sessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"type": "command",
|
|
7
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts"
|
|
8
|
+
}
|
|
9
|
+
],
|
|
10
|
+
"beforeSubmitPrompt": [
|
|
11
|
+
{
|
|
12
|
+
"type": "command",
|
|
13
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"preToolUse": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"type": "command",
|
|
23
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"stop": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Admin commands (pal cli ...):
|
|
10
10
|
* init Scaffold PAL home, install hooks for all targets
|
|
11
|
-
* install [--claude] [--opencode] Register hooks/skills for targets
|
|
12
|
-
* uninstall [--claude] [--opencode] Remove hooks/skills for targets
|
|
11
|
+
* install [--claude] [--opencode] [--cursor] Register hooks/skills for targets
|
|
12
|
+
* uninstall [--claude] [--opencode] [--cursor] Remove hooks/skills for targets
|
|
13
13
|
* update Update PAL (git pull or npm update)
|
|
14
14
|
* export [path] [--dry-run] Export user state to zip
|
|
15
15
|
* import [path] [--dry-run] Import user state from zip
|
|
@@ -197,9 +197,9 @@ function showHelp() {
|
|
|
197
197
|
pal cli <command> [options] Admin commands
|
|
198
198
|
|
|
199
199
|
Admin commands:
|
|
200
|
-
pal cli init [--claude] [--opencode] Scaffold and install (default: all)
|
|
201
|
-
pal cli install [--claude] [--opencode] Register hooks for targets
|
|
202
|
-
pal cli uninstall [--claude] [--opencode] Remove hooks for targets
|
|
200
|
+
pal cli init [--claude] [--opencode] [--cursor] Scaffold and install (default: all)
|
|
201
|
+
pal cli install [--claude] [--opencode] [--cursor] Register hooks for targets
|
|
202
|
+
pal cli uninstall [--claude] [--opencode] [--cursor] Remove hooks for targets
|
|
203
203
|
pal cli update Update PAL (git pull or npm update)
|
|
204
204
|
pal cli export [path] [--dry-run] Export state to zip
|
|
205
205
|
pal cli import [path] [--dry-run] Import state from zip
|
|
@@ -211,6 +211,7 @@ function showHelp() {
|
|
|
211
211
|
PAL_PKG Override package root
|
|
212
212
|
PAL_CLAUDE_DIR Override Claude config dir (default: ~/.claude)
|
|
213
213
|
PAL_OPENCODE_DIR Override opencode config dir (default: ~/.config/opencode)
|
|
214
|
+
PAL_CURSOR_DIR Override Cursor config dir (default: ~/.cursor)
|
|
214
215
|
PAL_AGENTS_DIR Override agents dir (default: ~/.agents)
|
|
215
216
|
`);
|
|
216
217
|
}
|
|
@@ -218,30 +219,35 @@ function showHelp() {
|
|
|
218
219
|
function parseTargets(args: string[]): {
|
|
219
220
|
claude: boolean;
|
|
220
221
|
opencode: boolean;
|
|
222
|
+
cursor: boolean;
|
|
221
223
|
} {
|
|
222
224
|
let claude = false;
|
|
223
225
|
let opencode = false;
|
|
226
|
+
let cursor = false;
|
|
224
227
|
for (const arg of args) {
|
|
225
228
|
if (arg === "--claude") claude = true;
|
|
226
229
|
else if (arg === "--opencode") opencode = true;
|
|
230
|
+
else if (arg === "--cursor") cursor = true;
|
|
227
231
|
else if (arg === "--all") {
|
|
228
232
|
claude = true;
|
|
229
233
|
opencode = true;
|
|
234
|
+
cursor = true;
|
|
230
235
|
}
|
|
231
236
|
}
|
|
232
|
-
if (!claude && !opencode
|
|
233
|
-
|
|
237
|
+
if (!claude && !opencode && !cursor)
|
|
238
|
+
return { claude: true, opencode: true, cursor: true };
|
|
239
|
+
return { claude, opencode, cursor };
|
|
234
240
|
}
|
|
235
241
|
|
|
236
242
|
/** Resolve targets against available agents. Errors if explicitly requested but missing. */
|
|
237
243
|
function resolveTargets(
|
|
238
244
|
args: string[],
|
|
239
245
|
health?: DoctorResult
|
|
240
|
-
): { claude: boolean; opencode: boolean } {
|
|
246
|
+
): { claude: boolean; opencode: boolean; cursor: boolean } {
|
|
241
247
|
const requested = parseTargets(args);
|
|
242
248
|
const h = health || doctor(true);
|
|
243
249
|
const explicit = args.some(
|
|
244
|
-
(a) => a === "--claude" || a === "--opencode" || a === "--all"
|
|
250
|
+
(a) => a === "--claude" || a === "--opencode" || a === "--cursor" || a === "--all"
|
|
245
251
|
);
|
|
246
252
|
|
|
247
253
|
if (explicit) {
|
|
@@ -254,6 +260,10 @@ function resolveTargets(
|
|
|
254
260
|
log.error("opencode is not installed. Run 'pal cli doctor' for details.");
|
|
255
261
|
process.exit(1);
|
|
256
262
|
}
|
|
263
|
+
if (requested.cursor && !h.cursor.available) {
|
|
264
|
+
log.error("Cursor is not installed. Run 'pal cli doctor' for details.");
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
257
267
|
return requested;
|
|
258
268
|
}
|
|
259
269
|
|
|
@@ -261,10 +271,12 @@ function resolveTargets(
|
|
|
261
271
|
const targets = {
|
|
262
272
|
claude: h.claude.available,
|
|
263
273
|
opencode: h.opencode.available,
|
|
274
|
+
cursor: h.cursor.available,
|
|
264
275
|
};
|
|
265
276
|
|
|
266
277
|
if (!targets.claude) log.info("Skipping Claude Code (not installed)");
|
|
267
278
|
if (!targets.opencode) log.info("Skipping opencode (not installed)");
|
|
279
|
+
if (!targets.cursor) log.info("Skipping Cursor (not installed)");
|
|
268
280
|
|
|
269
281
|
return targets;
|
|
270
282
|
}
|
|
@@ -312,6 +324,7 @@ interface DoctorResult {
|
|
|
312
324
|
bun: ToolCheck;
|
|
313
325
|
claude: ToolCheck;
|
|
314
326
|
opencode: ToolCheck;
|
|
327
|
+
cursor: ToolCheck;
|
|
315
328
|
hasAgent: boolean;
|
|
316
329
|
}
|
|
317
330
|
|
|
@@ -322,6 +335,7 @@ function doctor(silent = false): DoctorResult {
|
|
|
322
335
|
bun: { name: "bun", available: true, version: Bun.version },
|
|
323
336
|
claude: { name: "claude", available: true },
|
|
324
337
|
opencode: { name: "opencode", available: true },
|
|
338
|
+
cursor: { name: "cursor", available: true },
|
|
325
339
|
hasAgent: true,
|
|
326
340
|
};
|
|
327
341
|
}
|
|
@@ -329,7 +343,8 @@ function doctor(silent = false): DoctorResult {
|
|
|
329
343
|
const bun = { name: "bun", available: true, version: Bun.version };
|
|
330
344
|
const claude = checkTool("claude");
|
|
331
345
|
const opencode = checkTool("opencode");
|
|
332
|
-
const
|
|
346
|
+
const cursor = checkTool("cursor");
|
|
347
|
+
const hasAgent = claude.available || opencode.available || cursor.available;
|
|
333
348
|
|
|
334
349
|
const home = palHome();
|
|
335
350
|
const isRepo = existsSync(resolve(palPkg(), ".palroot"));
|
|
@@ -355,6 +370,9 @@ function doctor(silent = false): DoctorResult {
|
|
|
355
370
|
opencode.available
|
|
356
371
|
? ok(`opencode ${opencode.version || ""}`.trim())
|
|
357
372
|
: fail("opencode — not found");
|
|
373
|
+
cursor.available
|
|
374
|
+
? ok(`Cursor ${cursor.version || ""}`.trim())
|
|
375
|
+
: fail("Cursor — not found");
|
|
358
376
|
ok(`PAL home: ${home} (${isRepo ? "repo" : "package"} mode)`);
|
|
359
377
|
telosCount > 0 ? ok(`TELOS: ${telosCount} files`) : fail("TELOS: not scaffolded");
|
|
360
378
|
|
|
@@ -384,7 +402,7 @@ function doctor(silent = false): DoctorResult {
|
|
|
384
402
|
console.log("");
|
|
385
403
|
}
|
|
386
404
|
|
|
387
|
-
return { bun, claude, opencode, hasAgent };
|
|
405
|
+
return { bun, claude, opencode, cursor, hasAgent };
|
|
388
406
|
}
|
|
389
407
|
|
|
390
408
|
// ── Commands ──
|
|
@@ -424,7 +442,7 @@ async function init(args: string[]) {
|
|
|
424
442
|
}
|
|
425
443
|
}
|
|
426
444
|
|
|
427
|
-
async function install(targets: { claude: boolean; opencode: boolean }) {
|
|
445
|
+
async function install(targets: { claude: boolean; opencode: boolean; cursor: boolean }) {
|
|
428
446
|
// Scaffold TELOS + PAL settings, then prompt for missing identity
|
|
429
447
|
const { scaffoldTelos, scaffoldPalSettings } = await import("../targets/lib");
|
|
430
448
|
const { promptIdentity } = await import("./setup-identity");
|
|
@@ -444,6 +462,12 @@ async function install(targets: { claude: boolean; opencode: boolean }) {
|
|
|
444
462
|
console.log("");
|
|
445
463
|
}
|
|
446
464
|
|
|
465
|
+
if (targets.cursor) {
|
|
466
|
+
console.log("━━━ Cursor ━━━");
|
|
467
|
+
await import("../targets/cursor/install");
|
|
468
|
+
console.log("");
|
|
469
|
+
}
|
|
470
|
+
|
|
447
471
|
log.success("Done. Existing config was preserved — only new entries were added.");
|
|
448
472
|
}
|
|
449
473
|
|
|
@@ -462,6 +486,12 @@ async function uninstall(args: string[]) {
|
|
|
462
486
|
console.log("");
|
|
463
487
|
}
|
|
464
488
|
|
|
489
|
+
if (targets.cursor) {
|
|
490
|
+
console.log("━━━ Cursor ━━━");
|
|
491
|
+
await import("../targets/cursor/uninstall");
|
|
492
|
+
console.log("");
|
|
493
|
+
}
|
|
494
|
+
|
|
465
495
|
log.success(
|
|
466
496
|
`PAL uninstalled. Your TELOS, skills, and memory are still in ${palHome()}.`
|
|
467
497
|
);
|
|
@@ -641,6 +671,7 @@ async function status() {
|
|
|
641
671
|
|
|
642
672
|
log.info(`Claude: ${platform.claudeDir()}`);
|
|
643
673
|
log.info(`opencode: ${platform.opencodeDir()}`);
|
|
674
|
+
log.info(`Cursor: ${platform.cursorDir()}`);
|
|
644
675
|
log.info(`Agents: ${platform.agentsDir()}`);
|
|
645
676
|
console.log("");
|
|
646
677
|
|
package/src/hooks/LoadContext.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* learning digest, signal trends, failure patterns, active work state.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { regenerateIfNeeded } from "./lib/claude-md";
|
|
9
|
+
import { buildClaudeMd, regenerateIfNeeded } from "./lib/claude-md";
|
|
10
10
|
import { buildSystemReminder } from "./lib/context";
|
|
11
11
|
import { logDebug, logError } from "./lib/log";
|
|
12
12
|
|
|
@@ -28,11 +28,21 @@ try {
|
|
|
28
28
|
logError("LoadContext:regenerate", err);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// ---
|
|
31
|
+
// --- Context to stdout ---
|
|
32
32
|
try {
|
|
33
33
|
const reminder = buildSystemReminder();
|
|
34
|
-
if (reminder)
|
|
35
|
-
|
|
34
|
+
if (!reminder) process.exit(0);
|
|
35
|
+
|
|
36
|
+
if (process.env.CURSOR_VERSION) {
|
|
37
|
+
// Cursor: no native user-level rules — inject AGENTS.md + dynamic context
|
|
38
|
+
const agentsMd = buildClaudeMd();
|
|
39
|
+
const context = [agentsMd, reminder].filter(Boolean).join("\n\n");
|
|
40
|
+
process.stdout.write(JSON.stringify({ additional_context: context }));
|
|
41
|
+
} else {
|
|
42
|
+
// Claude Code: raw text
|
|
43
|
+
console.log(reminder);
|
|
44
|
+
}
|
|
45
|
+
logDebug("LoadContext", `Reminder injected: ${reminder.length} chars`);
|
|
36
46
|
} catch (err) {
|
|
37
47
|
logError("LoadContext:reminder", err);
|
|
38
48
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Fail-open design: if anything goes wrong, the command is allowed through.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { blockResponse } from "./lib/agent";
|
|
8
9
|
import { checkBashCommand, checkFilePath, WARN_COMMANDS } from "./lib/security";
|
|
9
10
|
import { readStdinJSON } from "./lib/stdin";
|
|
10
11
|
|
|
@@ -22,27 +23,30 @@ try {
|
|
|
22
23
|
|
|
23
24
|
const { tool_name, tool_input } = input;
|
|
24
25
|
|
|
25
|
-
//
|
|
26
|
-
|
|
26
|
+
// Normalize tool names: Claude uses "Bash", Cursor uses "Shell"
|
|
27
|
+
const isBash = tool_name === "Bash" || tool_name === "Shell";
|
|
28
|
+
const isFileWrite = tool_name === "Write" || tool_name === "Edit";
|
|
29
|
+
|
|
30
|
+
// Check shell commands
|
|
31
|
+
if (isBash && tool_input.command) {
|
|
27
32
|
const reason = checkBashCommand(tool_input.command);
|
|
28
33
|
if (reason) {
|
|
29
|
-
|
|
34
|
+
process.stdout.write(blockResponse(`Blocked: ${reason}`));
|
|
30
35
|
process.exit(0);
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
for (const pattern of WARN_COMMANDS) {
|
|
34
39
|
if (pattern.test(tool_input.command)) {
|
|
35
|
-
// Log but don't block — Claude Code's own permission system handles confirmation
|
|
36
40
|
break;
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
// Check file path operations
|
|
42
|
-
if (
|
|
45
|
+
// Check file path operations
|
|
46
|
+
if (isFileWrite && tool_input.file_path) {
|
|
43
47
|
const fileReason = checkFilePath(tool_input.file_path);
|
|
44
48
|
if (fileReason) {
|
|
45
|
-
|
|
49
|
+
process.stdout.write(blockResponse(fileReason));
|
|
46
50
|
process.exit(0);
|
|
47
51
|
}
|
|
48
52
|
}
|
package/src/hooks/SkillGuard.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Fail-open: on any error, the skill is allowed through.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { blockResponse } from "./lib/agent";
|
|
12
13
|
import { readStdinJSON } from "./lib/stdin";
|
|
13
14
|
|
|
14
15
|
const BLOCKED_SKILLS = ["keybindings-help"];
|
|
@@ -27,13 +28,11 @@ try {
|
|
|
27
28
|
const skill = (input.tool_input?.skill || "").toLowerCase().trim();
|
|
28
29
|
|
|
29
30
|
if (BLOCKED_SKILLS.includes(skill)) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"The user did NOT ask about keybindings. Continue with their ACTUAL request.",
|
|
36
|
-
})
|
|
31
|
+
process.stdout.write(
|
|
32
|
+
blockResponse(
|
|
33
|
+
'BLOCKED: "keybindings-help" is a known false-positive triggered by position bias. ' +
|
|
34
|
+
"The user did NOT ask about keybindings. Continue with their ACTUAL request."
|
|
35
|
+
)
|
|
37
36
|
);
|
|
38
37
|
}
|
|
39
38
|
} catch {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { checkReadmeSync } from "./handlers/readme-sync";
|
|
10
|
+
import { isCursor } from "./lib/agent";
|
|
10
11
|
import { logError } from "./lib/log";
|
|
11
12
|
import { readStdinJSON } from "./lib/stdin";
|
|
12
13
|
import { runStopHandlers } from "./lib/stop";
|
|
@@ -22,7 +23,13 @@ interface StopHookInput {
|
|
|
22
23
|
try {
|
|
23
24
|
const decision = checkReadmeSync();
|
|
24
25
|
if (decision.decision === "block") {
|
|
25
|
-
|
|
26
|
+
if (isCursor()) {
|
|
27
|
+
// Cursor stop hook: followup_message auto-sends to the agent
|
|
28
|
+
process.stdout.write(JSON.stringify({ followup_message: decision.reason }));
|
|
29
|
+
} else {
|
|
30
|
+
// Claude Code: block decision
|
|
31
|
+
process.stdout.write(JSON.stringify(decision));
|
|
32
|
+
}
|
|
26
33
|
process.exit(0);
|
|
27
34
|
}
|
|
28
35
|
} catch (err) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent detection and output format adapters.
|
|
3
|
+
*
|
|
4
|
+
* Cursor and Claude Code use different JSON contracts for hook I/O.
|
|
5
|
+
* These helpers normalize the differences so hook handlers stay clean.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type AgentType = "claude" | "cursor";
|
|
9
|
+
|
|
10
|
+
/** Detect which agent is running via environment variables */
|
|
11
|
+
export function detectAgent(): AgentType {
|
|
12
|
+
if (process.env.CURSOR_VERSION) return "cursor";
|
|
13
|
+
return "claude";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const isCursor = () => detectAgent() === "cursor";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format a "block this action" response for the current agent.
|
|
20
|
+
* Claude Code: { decision: "block", reason }
|
|
21
|
+
* Cursor: { permission: "deny", user_message }
|
|
22
|
+
*/
|
|
23
|
+
export function blockResponse(reason: string): string {
|
|
24
|
+
if (isCursor()) {
|
|
25
|
+
return JSON.stringify({ permission: "deny", user_message: reason });
|
|
26
|
+
}
|
|
27
|
+
return JSON.stringify({ decision: "block", reason });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Format sessionStart context injection for the current agent.
|
|
32
|
+
* Claude Code: raw text to stdout
|
|
33
|
+
* Cursor: { additional_context: "..." }
|
|
34
|
+
*/
|
|
35
|
+
export function sessionStartOutput(context: string): string {
|
|
36
|
+
if (isCursor()) {
|
|
37
|
+
return JSON.stringify({ additional_context: context });
|
|
38
|
+
}
|
|
39
|
+
return context;
|
|
40
|
+
}
|
package/src/hooks/lib/paths.ts
CHANGED
|
@@ -67,6 +67,7 @@ const h = homedir();
|
|
|
67
67
|
export const platform = {
|
|
68
68
|
claudeDir: () => process.env.PAL_CLAUDE_DIR || resolve(h, ".claude"),
|
|
69
69
|
opencodeDir: () => process.env.PAL_OPENCODE_DIR || resolve(h, ".config", "opencode"),
|
|
70
|
+
cursorDir: () => process.env.PAL_CURSOR_DIR || resolve(h, ".cursor"),
|
|
70
71
|
agentsDir: () => process.env.PAL_AGENTS_DIR || resolve(h, ".agents"),
|
|
71
72
|
} as const;
|
|
72
73
|
|
|
@@ -78,5 +79,6 @@ export const assets = {
|
|
|
78
79
|
telosTemplates: () => pkg("assets", "templates", "telos"),
|
|
79
80
|
agentsMdTemplate: () => pkg("assets", "templates", "AGENTS.md.template"),
|
|
80
81
|
claudeSettingsTemplate: () => pkg("assets", "templates", "settings.claude.json"),
|
|
82
|
+
cursorHooksTemplate: () => pkg("assets", "templates", "hooks.cursor.json"),
|
|
81
83
|
palDocs: () => pkg("assets", "templates", "PAL"),
|
|
82
84
|
} as const;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAL — Cursor target installer (TypeScript)
|
|
3
|
+
* Merges hooks template into existing hooks.json (never overwrites).
|
|
4
|
+
* Symlinks skills. Generates AGENTS.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
|
|
10
|
+
import { assets, palPkg, platform } from "../../hooks/lib/paths";
|
|
11
|
+
import {
|
|
12
|
+
copyPalDocs,
|
|
13
|
+
copySkills,
|
|
14
|
+
countSkills,
|
|
15
|
+
loadCursorHooksTemplate,
|
|
16
|
+
log,
|
|
17
|
+
mergeCursorHooks,
|
|
18
|
+
readJson,
|
|
19
|
+
scaffoldPalSettings,
|
|
20
|
+
writeJson,
|
|
21
|
+
} from "../lib";
|
|
22
|
+
|
|
23
|
+
const PKG_ROOT = palPkg().replaceAll("\\", "/");
|
|
24
|
+
const CURSOR_DIR = platform.cursorDir();
|
|
25
|
+
const HOOKS_FILE = resolve(CURSOR_DIR, "hooks.json");
|
|
26
|
+
|
|
27
|
+
// --- Ensure ~/.cursor/ exists ---
|
|
28
|
+
mkdirSync(CURSOR_DIR, { recursive: true });
|
|
29
|
+
|
|
30
|
+
// --- Merge hooks ---
|
|
31
|
+
if (existsSync(HOOKS_FILE)) {
|
|
32
|
+
const backup = `${HOOKS_FILE}.bak.${Date.now()}`;
|
|
33
|
+
copyFileSync(HOOKS_FILE, backup);
|
|
34
|
+
log.info("Backed up hooks.json");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const template = loadCursorHooksTemplate(assets.cursorHooksTemplate(), PKG_ROOT);
|
|
38
|
+
const existing = readJson<Record<string, unknown>>(HOOKS_FILE, {});
|
|
39
|
+
const merged = mergeCursorHooks(existing, template);
|
|
40
|
+
|
|
41
|
+
writeJson(HOOKS_FILE, merged);
|
|
42
|
+
log.success("Merged PAL hooks into hooks.json");
|
|
43
|
+
|
|
44
|
+
// --- Symlink skills to ~/.cursor/skills/ ---
|
|
45
|
+
const cursorSkillsDir = resolve(CURSOR_DIR, "skills");
|
|
46
|
+
copySkills(cursorSkillsDir);
|
|
47
|
+
|
|
48
|
+
// --- Copy PAL system docs ---
|
|
49
|
+
const palDocsCount = copyPalDocs();
|
|
50
|
+
log.success(`Installed ${palDocsCount} PAL docs to ~/.agents/PAL/`);
|
|
51
|
+
|
|
52
|
+
// --- Scaffold PAL settings ---
|
|
53
|
+
scaffoldPalSettings();
|
|
54
|
+
|
|
55
|
+
// --- Generate AGENTS.md ---
|
|
56
|
+
regenerateIfNeeded();
|
|
57
|
+
log.success("Generated AGENTS.md");
|
|
58
|
+
|
|
59
|
+
log.success("Cursor installation complete");
|
|
60
|
+
console.log("");
|
|
61
|
+
log.info(`Skills: ${countSkills()}`);
|
|
62
|
+
log.info(`Hooks: ${HOOKS_FILE}`);
|
|
63
|
+
log.info(
|
|
64
|
+
"Note: Cursor tool matchers may need tuning — verify hook behavior after first use"
|
|
65
|
+
);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAL — Cursor uninstaller (TypeScript)
|
|
3
|
+
* Removes only PAL-owned hooks from hooks.json. Preserves user hooks.
|
|
4
|
+
* Removes PAL skill symlinks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { copyFileSync, existsSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { assets, palPkg, platform } from "../../hooks/lib/paths";
|
|
10
|
+
import {
|
|
11
|
+
loadCursorHooksTemplate,
|
|
12
|
+
log,
|
|
13
|
+
readJson,
|
|
14
|
+
removePalDocs,
|
|
15
|
+
removeSkills,
|
|
16
|
+
unmergeCursorHooks,
|
|
17
|
+
writeJson,
|
|
18
|
+
} from "../lib";
|
|
19
|
+
|
|
20
|
+
const PKG_ROOT = palPkg().replaceAll("\\", "/");
|
|
21
|
+
const CURSOR_DIR = platform.cursorDir();
|
|
22
|
+
const HOOKS_FILE = resolve(CURSOR_DIR, "hooks.json");
|
|
23
|
+
|
|
24
|
+
// --- Remove PAL hooks from hooks.json ---
|
|
25
|
+
if (existsSync(HOOKS_FILE)) {
|
|
26
|
+
// Backup before modifying
|
|
27
|
+
copyFileSync(HOOKS_FILE, `${HOOKS_FILE}.bak.${Date.now()}`);
|
|
28
|
+
log.info("Backed up hooks.json");
|
|
29
|
+
|
|
30
|
+
const template = loadCursorHooksTemplate(assets.cursorHooksTemplate(), PKG_ROOT);
|
|
31
|
+
const existing = readJson<Record<string, unknown>>(HOOKS_FILE, {});
|
|
32
|
+
const cleaned = unmergeCursorHooks(existing, template);
|
|
33
|
+
|
|
34
|
+
writeJson(HOOKS_FILE, cleaned);
|
|
35
|
+
log.success("Removed PAL hooks from hooks.json");
|
|
36
|
+
} else {
|
|
37
|
+
log.info("No hooks.json found, nothing to do");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- Remove PAL skill symlinks ---
|
|
41
|
+
const cursorSkillsDir = resolve(CURSOR_DIR, "skills");
|
|
42
|
+
const removed = removeSkills(cursorSkillsDir);
|
|
43
|
+
if (removed.length > 0) {
|
|
44
|
+
log.success(`Removed ${removed.length} skill(s): ${removed.join(", ")}`);
|
|
45
|
+
} else {
|
|
46
|
+
log.info("No PAL skills found");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- Remove PAL system docs ---
|
|
50
|
+
removePalDocs();
|
|
51
|
+
|
|
52
|
+
log.success("Cursor uninstall complete");
|
package/src/targets/lib.ts
CHANGED
|
@@ -136,6 +136,85 @@ export function unmergeSettings(existing: Settings, template: Settings): Setting
|
|
|
136
136
|
return result;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// --- Cursor hooks.json merge/unmerge ---
|
|
140
|
+
|
|
141
|
+
type CursorHookEntry = {
|
|
142
|
+
type: string;
|
|
143
|
+
command: string;
|
|
144
|
+
matcher?: string;
|
|
145
|
+
timeout?: number;
|
|
146
|
+
};
|
|
147
|
+
type CursorHooks = {
|
|
148
|
+
version?: number;
|
|
149
|
+
hooks?: Record<string, CursorHookEntry[]>;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Load a Cursor hooks template, replacing {{PKG_ROOT}} with the actual path.
|
|
154
|
+
*/
|
|
155
|
+
export function loadCursorHooksTemplate(
|
|
156
|
+
templatePath: string,
|
|
157
|
+
pkgRoot: string
|
|
158
|
+
): CursorHooks {
|
|
159
|
+
const raw = readFileSync(templatePath, "utf-8");
|
|
160
|
+
const resolved = raw.replaceAll("{{PKG_ROOT}}", pkgRoot);
|
|
161
|
+
return JSON.parse(resolved) as CursorHooks;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Merge PAL hooks into an existing Cursor hooks.json.
|
|
166
|
+
* Deduplicates by command string within each event.
|
|
167
|
+
*/
|
|
168
|
+
export function mergeCursorHooks(
|
|
169
|
+
existing: CursorHooks,
|
|
170
|
+
template: CursorHooks
|
|
171
|
+
): CursorHooks {
|
|
172
|
+
const result: CursorHooks = { ...existing, version: existing.version ?? 1 };
|
|
173
|
+
|
|
174
|
+
if (template.hooks) {
|
|
175
|
+
if (!result.hooks) result.hooks = {};
|
|
176
|
+
for (const [event, entries] of Object.entries(template.hooks)) {
|
|
177
|
+
const current = result.hooks[event] ?? [];
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (!current.some((e) => e.command === entry.command)) {
|
|
180
|
+
current.push(entry);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
result.hooks[event] = current;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Remove PAL hooks from an existing Cursor hooks.json.
|
|
192
|
+
* Only removes entries whose command matches the template. Preserves user hooks.
|
|
193
|
+
*/
|
|
194
|
+
export function unmergeCursorHooks(
|
|
195
|
+
existing: CursorHooks,
|
|
196
|
+
template: CursorHooks
|
|
197
|
+
): CursorHooks {
|
|
198
|
+
const result: CursorHooks = { ...existing };
|
|
199
|
+
|
|
200
|
+
if (template.hooks && result.hooks) {
|
|
201
|
+
const palCommands = new Set<string>();
|
|
202
|
+
for (const entries of Object.values(template.hooks)) {
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
palCommands.add(entry.command);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const [event, entries] of Object.entries(result.hooks)) {
|
|
209
|
+
result.hooks[event] = entries.filter((e) => !palCommands.has(e.command));
|
|
210
|
+
if (result.hooks[event].length === 0) delete result.hooks[event];
|
|
211
|
+
}
|
|
212
|
+
if (Object.keys(result.hooks).length === 0) delete result.hooks;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
|
|
139
218
|
// --- TELOS scaffolding ---
|
|
140
219
|
|
|
141
220
|
/** Copy template files into telos/ without overwriting existing ones */
|