portable-agent-layer 0.24.1 → 0.25.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 -0
- package/assets/templates/hooks.copilot.json +33 -0
- package/package.json +1 -1
- package/src/cli/index.ts +191 -18
- package/src/hooks/LoadContext.ts +20 -3
- package/src/hooks/lib/claude-md.ts +5 -0
- package/src/hooks/lib/paths.ts +2 -0
- package/src/targets/copilot/install.ts +66 -0
- package/src/targets/copilot/uninstall.ts +60 -0
- package/src/targets/lib.ts +17 -1
package/README.md
CHANGED
|
@@ -101,6 +101,7 @@ pal cli install # all available (default)
|
|
|
101
101
|
| Claude Code | Full | Yes | Yes | Yes | Yes |
|
|
102
102
|
| opencode | Full | Yes | Yes (plugin) | Yes | Yes |
|
|
103
103
|
| Cursor | Full | Yes | Yes | Yes (injected via hook) | Yes |
|
|
104
|
+
| GitHub Copilot | Full | Yes | Yes | Yes (via copilot-instructions.md) | Yes |
|
|
104
105
|
| Codex | Partial | Yes | No | Yes | No |
|
|
105
106
|
|
|
106
107
|
---
|
|
@@ -125,6 +126,7 @@ pal cli install # all available (default)
|
|
|
125
126
|
| `PAL_CLAUDE_DIR` | Override Claude config dir (default: `~/.claude`) |
|
|
126
127
|
| `PAL_OPENCODE_DIR` | Override opencode config dir (default: `~/.config/opencode`) |
|
|
127
128
|
| `PAL_CURSOR_DIR` | Override Cursor config dir (default: `~/.cursor`) |
|
|
129
|
+
| `PAL_COPILOT_DIR` | Override Copilot config dir (default: `~/.copilot`) |
|
|
128
130
|
| `PAL_CODEX_DIR` | Override Codex config dir (default: `~/.codex`) |
|
|
129
131
|
| `PAL_AGENTS_DIR` | Override agents dir (default: `~/.agents`) |
|
|
130
132
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"hooks": {
|
|
4
|
+
"sessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"type": "command",
|
|
7
|
+
"bash": "PAL_AGENT=copilot bun run {{PKG_ROOT}}/src/hooks/LoadContext.ts"
|
|
8
|
+
}
|
|
9
|
+
],
|
|
10
|
+
"userPromptSubmitted": [
|
|
11
|
+
{
|
|
12
|
+
"type": "command",
|
|
13
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/UserPromptOrchestrator.ts"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"preToolUse": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/SecurityValidator.ts"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"type": "command",
|
|
23
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/SkillGuard.ts"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"agentStop": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"bash": "bun run {{PKG_ROOT}}/src/hooks/StopOrchestrator.ts"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -212,46 +212,49 @@ function showHelp() {
|
|
|
212
212
|
PAL_CLAUDE_DIR Override Claude config dir (default: ~/.claude)
|
|
213
213
|
PAL_OPENCODE_DIR Override opencode config dir (default: ~/.config/opencode)
|
|
214
214
|
PAL_CURSOR_DIR Override Cursor config dir (default: ~/.cursor)
|
|
215
|
+
PAL_COPILOT_DIR Override Copilot config dir (default: ~/.copilot)
|
|
215
216
|
PAL_AGENTS_DIR Override agents dir (default: ~/.agents)
|
|
216
217
|
`);
|
|
217
218
|
}
|
|
218
219
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
cursor: boolean;
|
|
223
|
-
} {
|
|
220
|
+
type Targets = { claude: boolean; opencode: boolean; cursor: boolean; copilot: boolean };
|
|
221
|
+
|
|
222
|
+
function parseTargets(args: string[]): Targets {
|
|
224
223
|
let claude = false;
|
|
225
224
|
let opencode = false;
|
|
226
225
|
let cursor = false;
|
|
226
|
+
let copilot = false;
|
|
227
227
|
for (const arg of args) {
|
|
228
228
|
if (arg === "--claude") claude = true;
|
|
229
229
|
else if (arg === "--opencode") opencode = true;
|
|
230
230
|
else if (arg === "--cursor") cursor = true;
|
|
231
|
+
else if (arg === "--copilot") copilot = true;
|
|
231
232
|
else if (arg === "--all") {
|
|
232
233
|
claude = true;
|
|
233
234
|
opencode = true;
|
|
234
235
|
cursor = true;
|
|
236
|
+
copilot = true;
|
|
235
237
|
}
|
|
236
238
|
}
|
|
237
|
-
if (!claude && !opencode && !cursor)
|
|
238
|
-
return { claude: true, opencode: true, cursor: true };
|
|
239
|
-
return { claude, opencode, cursor };
|
|
239
|
+
if (!claude && !opencode && !cursor && !copilot)
|
|
240
|
+
return { claude: true, opencode: true, cursor: true, copilot: true };
|
|
241
|
+
return { claude, opencode, cursor, copilot };
|
|
240
242
|
}
|
|
241
243
|
|
|
242
244
|
/** Resolve targets against available agents. Errors if explicitly requested but missing. */
|
|
243
|
-
function resolveTargets(
|
|
244
|
-
args: string[],
|
|
245
|
-
health?: DoctorResult
|
|
246
|
-
): { claude: boolean; opencode: boolean; cursor: boolean } {
|
|
245
|
+
function resolveTargets(args: string[], health?: DoctorResult): Targets {
|
|
247
246
|
const requested = parseTargets(args);
|
|
248
247
|
const h = health || doctor(true);
|
|
249
248
|
const explicit = args.some(
|
|
250
|
-
(a) =>
|
|
249
|
+
(a) =>
|
|
250
|
+
a === "--claude" ||
|
|
251
|
+
a === "--opencode" ||
|
|
252
|
+
a === "--cursor" ||
|
|
253
|
+
a === "--copilot" ||
|
|
254
|
+
a === "--all"
|
|
251
255
|
);
|
|
252
256
|
|
|
253
257
|
if (explicit) {
|
|
254
|
-
// User explicitly requested — error if not available
|
|
255
258
|
if (requested.claude && !h.claude.available) {
|
|
256
259
|
log.error("Claude Code is not installed. Run 'pal cli doctor' for details.");
|
|
257
260
|
process.exit(1);
|
|
@@ -264,19 +267,25 @@ function resolveTargets(
|
|
|
264
267
|
log.error("Cursor is not installed. Run 'pal cli doctor' for details.");
|
|
265
268
|
process.exit(1);
|
|
266
269
|
}
|
|
270
|
+
if (requested.copilot && !h.copilot.available) {
|
|
271
|
+
log.error("Copilot is not installed. Run 'pal cli doctor' for details.");
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
267
274
|
return requested;
|
|
268
275
|
}
|
|
269
276
|
|
|
270
277
|
// Default (no flags) — install for available agents only
|
|
271
|
-
const targets = {
|
|
278
|
+
const targets: Targets = {
|
|
272
279
|
claude: h.claude.available,
|
|
273
280
|
opencode: h.opencode.available,
|
|
274
281
|
cursor: h.cursor.available,
|
|
282
|
+
copilot: h.copilot.available,
|
|
275
283
|
};
|
|
276
284
|
|
|
277
285
|
if (!targets.claude) log.info("Skipping Claude Code (not installed)");
|
|
278
286
|
if (!targets.opencode) log.info("Skipping opencode (not installed)");
|
|
279
287
|
if (!targets.cursor) log.info("Skipping Cursor (not installed)");
|
|
288
|
+
if (!targets.copilot) log.info("Skipping Copilot (not installed)");
|
|
280
289
|
|
|
281
290
|
return targets;
|
|
282
291
|
}
|
|
@@ -288,6 +297,46 @@ interface HookHealth {
|
|
|
288
297
|
lastError: string | null;
|
|
289
298
|
}
|
|
290
299
|
|
|
300
|
+
function checkClaudeHooksRegistered(): boolean {
|
|
301
|
+
const settingsPath = resolve(platform.claudeDir(), "settings.json");
|
|
302
|
+
if (!existsSync(settingsPath)) return false;
|
|
303
|
+
try {
|
|
304
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
305
|
+
const groups = settings?.hooks?.SessionStart;
|
|
306
|
+
if (!Array.isArray(groups)) return false;
|
|
307
|
+
return groups.some((g: { hooks?: { command?: string }[] }) =>
|
|
308
|
+
g?.hooks?.some((h) => h?.command?.includes("LoadContext"))
|
|
309
|
+
);
|
|
310
|
+
} catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function checkCursorHooksRegistered(): boolean {
|
|
316
|
+
const hooksPath = resolve(platform.cursorDir(), "hooks.json");
|
|
317
|
+
if (!existsSync(hooksPath)) return false;
|
|
318
|
+
try {
|
|
319
|
+
const data = JSON.parse(readFileSync(hooksPath, "utf-8"));
|
|
320
|
+
const hooks = data?.hooks?.sessionStart;
|
|
321
|
+
if (!Array.isArray(hooks)) return false;
|
|
322
|
+
return hooks.some((h: { command?: string }) => h?.command?.includes("LoadContext"));
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function checkOpencodePluginInstalled(): boolean {
|
|
329
|
+
return existsSync(resolve(platform.opencodeDir(), "plugins", "pal-plugin.ts"));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function checkCopilotHooksRegistered(): boolean {
|
|
333
|
+
return existsSync(resolve(platform.copilotDir(), "hooks", "pal-hooks.json"));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function checkCopilotInstructionsPresent(): boolean {
|
|
337
|
+
return existsSync(resolve(platform.copilotDir(), "copilot-instructions.md"));
|
|
338
|
+
}
|
|
339
|
+
|
|
291
340
|
function checkHookHealth(home: string): HookHealth {
|
|
292
341
|
const logPath = resolve(home, "memory", "state", "debug.log");
|
|
293
342
|
|
|
@@ -325,6 +374,7 @@ interface DoctorResult {
|
|
|
325
374
|
claude: ToolCheck;
|
|
326
375
|
opencode: ToolCheck;
|
|
327
376
|
cursor: ToolCheck;
|
|
377
|
+
copilot: ToolCheck;
|
|
328
378
|
hasAgent: boolean;
|
|
329
379
|
}
|
|
330
380
|
|
|
@@ -336,6 +386,7 @@ function doctor(silent = false): DoctorResult {
|
|
|
336
386
|
claude: { name: "claude", available: true },
|
|
337
387
|
opencode: { name: "opencode", available: true },
|
|
338
388
|
cursor: { name: "cursor", available: true },
|
|
389
|
+
copilot: { name: "copilot", available: true },
|
|
339
390
|
hasAgent: true,
|
|
340
391
|
};
|
|
341
392
|
}
|
|
@@ -344,7 +395,9 @@ function doctor(silent = false): DoctorResult {
|
|
|
344
395
|
const claude = checkTool("claude");
|
|
345
396
|
const opencode = checkTool("opencode");
|
|
346
397
|
const cursor = checkTool("cursor");
|
|
347
|
-
const
|
|
398
|
+
const copilot = checkTool("copilot", ["version"]);
|
|
399
|
+
const hasAgent =
|
|
400
|
+
claude.available || opencode.available || cursor.available || copilot.available;
|
|
348
401
|
|
|
349
402
|
const home = palHome();
|
|
350
403
|
const telosCount = (() => {
|
|
@@ -372,9 +425,117 @@ function doctor(silent = false): DoctorResult {
|
|
|
372
425
|
cursor.available
|
|
373
426
|
? ok(`Cursor ${cursor.version || ""}`.trim())
|
|
374
427
|
: fail("Cursor — not found");
|
|
428
|
+
copilot.available
|
|
429
|
+
? ok(`Copilot ${copilot.version || ""}`.trim())
|
|
430
|
+
: fail("Copilot — not found");
|
|
375
431
|
ok(`PAL home: ${home}`);
|
|
376
432
|
telosCount > 0 ? ok(`TELOS: ${telosCount} files`) : fail("TELOS: not scaffolded");
|
|
377
433
|
|
|
434
|
+
// Identity
|
|
435
|
+
const palSettingsPath = resolve(home, "memory", "pal-settings.json");
|
|
436
|
+
if (existsSync(palSettingsPath)) {
|
|
437
|
+
try {
|
|
438
|
+
const s = JSON.parse(readFileSync(palSettingsPath, "utf-8"));
|
|
439
|
+
const hasIdentity = s?.identity?.principal?.name && s?.identity?.ai?.name;
|
|
440
|
+
hasIdentity
|
|
441
|
+
? ok("Identity configured")
|
|
442
|
+
: warn("Identity — incomplete (run 'pal cli install')");
|
|
443
|
+
} catch {
|
|
444
|
+
warn("Identity — could not read pal-settings.json");
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
warn("Identity — pal-settings.json missing (run 'pal cli install')");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// AGENTS.md
|
|
451
|
+
const agentsMdPath = resolve(platform.opencodeDir(), "AGENTS.md");
|
|
452
|
+
existsSync(agentsMdPath)
|
|
453
|
+
? ok("AGENTS.md present")
|
|
454
|
+
: fail("AGENTS.md — missing (run 'pal cli install')");
|
|
455
|
+
|
|
456
|
+
if (claude.available) {
|
|
457
|
+
const claudeMdPath = resolve(platform.claudeDir(), "CLAUDE.md");
|
|
458
|
+
existsSync(claudeMdPath)
|
|
459
|
+
? ok("CLAUDE.md present")
|
|
460
|
+
: fail("CLAUDE.md — missing (run 'pal cli install --claude')");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Setup state
|
|
464
|
+
const setupPath = resolve(home, "memory", "state", "setup.json");
|
|
465
|
+
if (existsSync(setupPath)) {
|
|
466
|
+
try {
|
|
467
|
+
const setup = JSON.parse(readFileSync(setupPath, "utf-8"));
|
|
468
|
+
setup?.completed
|
|
469
|
+
? ok("TELOS setup complete")
|
|
470
|
+
: warn("TELOS setup incomplete — run 'pal cli install' or start a session");
|
|
471
|
+
} catch {
|
|
472
|
+
warn("TELOS setup — could not read setup.json");
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
warn("TELOS setup — setup.json missing (run 'pal cli install')");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Skills (per installed agent)
|
|
479
|
+
const countSkillsIn = (dir: string) =>
|
|
480
|
+
existsSync(dir)
|
|
481
|
+
? readdirSync(dir).filter((f) => existsSync(resolve(dir, f, "SKILL.md"))).length
|
|
482
|
+
: 0;
|
|
483
|
+
if (claude.available) {
|
|
484
|
+
const n = countSkillsIn(resolve(platform.claudeDir(), "skills"));
|
|
485
|
+
n > 0
|
|
486
|
+
? ok(`Claude Code skills: ${n}`)
|
|
487
|
+
: warn("Claude Code skills — none found (run 'pal cli install --claude')");
|
|
488
|
+
}
|
|
489
|
+
if (opencode.available) {
|
|
490
|
+
const n = countSkillsIn(resolve(platform.agentsDir(), "skills"));
|
|
491
|
+
n > 0
|
|
492
|
+
? ok(`opencode skills: ${n}`)
|
|
493
|
+
: warn("opencode skills — none found (run 'pal cli install --opencode')");
|
|
494
|
+
}
|
|
495
|
+
if (cursor.available) {
|
|
496
|
+
const n = countSkillsIn(resolve(platform.cursorDir(), "skills"));
|
|
497
|
+
n > 0
|
|
498
|
+
? ok(`Cursor skills: ${n}`)
|
|
499
|
+
: warn("Cursor skills — none found (run 'pal cli install --cursor')");
|
|
500
|
+
}
|
|
501
|
+
if (copilot.available) {
|
|
502
|
+
const n = countSkillsIn(resolve(platform.copilotDir(), "skills"));
|
|
503
|
+
n > 0
|
|
504
|
+
? ok(`Copilot skills: ${n}`)
|
|
505
|
+
: warn("Copilot skills — none found (run 'pal cli install --copilot')");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Dependencies
|
|
509
|
+
const nodeModulesPath = resolve(palPkg(), "node_modules");
|
|
510
|
+
existsSync(nodeModulesPath)
|
|
511
|
+
? ok("Dependencies installed")
|
|
512
|
+
: fail("Dependencies missing — run 'pal cli install'");
|
|
513
|
+
|
|
514
|
+
// Hook registration (per installed agent)
|
|
515
|
+
if (claude.available) {
|
|
516
|
+
checkClaudeHooksRegistered()
|
|
517
|
+
? ok("Claude Code hooks registered")
|
|
518
|
+
: fail("Claude Code hooks — not registered (run 'pal cli install --claude')");
|
|
519
|
+
}
|
|
520
|
+
if (opencode.available) {
|
|
521
|
+
checkOpencodePluginInstalled()
|
|
522
|
+
? ok("opencode plugin installed")
|
|
523
|
+
: fail("opencode plugin — not installed (run 'pal cli install --opencode')");
|
|
524
|
+
}
|
|
525
|
+
if (cursor.available) {
|
|
526
|
+
checkCursorHooksRegistered()
|
|
527
|
+
? ok("Cursor hooks registered")
|
|
528
|
+
: fail("Cursor hooks — not registered (run 'pal cli install --cursor')");
|
|
529
|
+
}
|
|
530
|
+
if (copilot.available) {
|
|
531
|
+
checkCopilotHooksRegistered()
|
|
532
|
+
? ok("Copilot hooks registered")
|
|
533
|
+
: fail("Copilot hooks — not registered (run 'pal cli install --copilot')");
|
|
534
|
+
checkCopilotInstructionsPresent()
|
|
535
|
+
? ok("copilot-instructions.md present")
|
|
536
|
+
: warn("copilot-instructions.md missing (run 'pal cli install --copilot')");
|
|
537
|
+
}
|
|
538
|
+
|
|
378
539
|
// API key checks
|
|
379
540
|
process.env.PAL_ANTHROPIC_API_KEY
|
|
380
541
|
? ok("PAL_ANTHROPIC_API_KEY is set")
|
|
@@ -407,7 +568,7 @@ function doctor(silent = false): DoctorResult {
|
|
|
407
568
|
console.log("");
|
|
408
569
|
}
|
|
409
570
|
|
|
410
|
-
return { bun, claude, opencode, cursor, hasAgent };
|
|
571
|
+
return { bun, claude, opencode, cursor, copilot, hasAgent };
|
|
411
572
|
}
|
|
412
573
|
|
|
413
574
|
// ── Commands ──
|
|
@@ -443,7 +604,7 @@ async function init(args: string[]) {
|
|
|
443
604
|
}
|
|
444
605
|
}
|
|
445
606
|
|
|
446
|
-
async function install(targets:
|
|
607
|
+
async function install(targets: Targets) {
|
|
447
608
|
// Ensure dependencies are installed
|
|
448
609
|
const pkg = palPkg();
|
|
449
610
|
log.info("Installing dependencies...");
|
|
@@ -481,6 +642,12 @@ async function install(targets: { claude: boolean; opencode: boolean; cursor: bo
|
|
|
481
642
|
console.log("");
|
|
482
643
|
}
|
|
483
644
|
|
|
645
|
+
if (targets.copilot) {
|
|
646
|
+
console.log("━━━ Copilot ━━━");
|
|
647
|
+
await import("../targets/copilot/install");
|
|
648
|
+
console.log("");
|
|
649
|
+
}
|
|
650
|
+
|
|
484
651
|
log.success("Done. Existing config was preserved — only new entries were added.");
|
|
485
652
|
}
|
|
486
653
|
|
|
@@ -505,6 +672,12 @@ async function uninstall(args: string[]) {
|
|
|
505
672
|
console.log("");
|
|
506
673
|
}
|
|
507
674
|
|
|
675
|
+
if (targets.copilot) {
|
|
676
|
+
console.log("━━━ Copilot ━━━");
|
|
677
|
+
await import("../targets/copilot/uninstall");
|
|
678
|
+
console.log("");
|
|
679
|
+
}
|
|
680
|
+
|
|
508
681
|
log.success(
|
|
509
682
|
`PAL uninstalled. Your TELOS, skills, and memory are still in ${palHome()}.`
|
|
510
683
|
);
|
package/src/hooks/LoadContext.ts
CHANGED
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
* Static context (TELOS, setup prompt) is loaded natively from AGENTS.md / CLAUDE.md.
|
|
5
5
|
* This hook injects dynamic context only: wisdom principles, relationship notes,
|
|
6
6
|
* learning digest, signal trends, failure patterns, active work state.
|
|
7
|
+
*
|
|
8
|
+
* Copilot: sessionStart output is ignored by the runtime. Instead, we write the merged
|
|
9
|
+
* context directly to ~/.copilot/copilot-instructions.md so it is picked up on load.
|
|
7
10
|
*/
|
|
8
11
|
|
|
12
|
+
import { existsSync, lstatSync, unlinkSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { resolve } from "node:path";
|
|
9
14
|
import { buildClaudeMd, regenerateIfNeeded } from "./lib/claude-md";
|
|
10
15
|
import { buildSystemReminder } from "./lib/context";
|
|
11
16
|
import { logDebug, logError } from "./lib/log";
|
|
17
|
+
import { platform } from "./lib/paths";
|
|
12
18
|
|
|
13
19
|
// --- Skip heavy context for subagents ---
|
|
14
20
|
const isSubagent =
|
|
@@ -28,21 +34,32 @@ try {
|
|
|
28
34
|
logError("LoadContext:regenerate", err);
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
// --- Context to stdout ---
|
|
37
|
+
// --- Context to stdout (or file for Copilot) ---
|
|
32
38
|
try {
|
|
33
39
|
const reminder = buildSystemReminder();
|
|
34
40
|
if (!reminder) process.exit(0);
|
|
35
41
|
|
|
36
|
-
if (process.env.
|
|
42
|
+
if (process.env.PAL_AGENT === "copilot") {
|
|
43
|
+
// Copilot: sessionStart output is ignored — write merged context to copilot-instructions.md
|
|
44
|
+
const instructionsPath = resolve(platform.copilotDir(), "copilot-instructions.md");
|
|
45
|
+
const agentsMd = buildClaudeMd();
|
|
46
|
+
const context = [agentsMd, reminder].filter(Boolean).join("\n\n");
|
|
47
|
+
if (existsSync(instructionsPath) && lstatSync(instructionsPath).isSymbolicLink()) {
|
|
48
|
+
unlinkSync(instructionsPath);
|
|
49
|
+
}
|
|
50
|
+
writeFileSync(instructionsPath, context, "utf-8");
|
|
51
|
+
logDebug("LoadContext", `Copilot instructions written: ${context.length} chars`);
|
|
52
|
+
} else if (process.env.CURSOR_VERSION) {
|
|
37
53
|
// Cursor: no native user-level rules — inject AGENTS.md + dynamic context
|
|
38
54
|
const agentsMd = buildClaudeMd();
|
|
39
55
|
const context = [agentsMd, reminder].filter(Boolean).join("\n\n");
|
|
40
56
|
process.stdout.write(JSON.stringify({ additional_context: context }));
|
|
57
|
+
logDebug("LoadContext", `Reminder injected: ${reminder.length} chars`);
|
|
41
58
|
} else {
|
|
42
59
|
// Claude Code: raw text
|
|
43
60
|
console.log(reminder);
|
|
61
|
+
logDebug("LoadContext", `Reminder injected: ${reminder.length} chars`);
|
|
44
62
|
}
|
|
45
|
-
logDebug("LoadContext", `Reminder injected: ${reminder.length} chars`);
|
|
46
63
|
} catch (err) {
|
|
47
64
|
logError("LoadContext:reminder", err);
|
|
48
65
|
}
|
|
@@ -76,6 +76,11 @@ function ensureSymlinks(): void {
|
|
|
76
76
|
const { outputPath, symlinkPath } = getOutputPaths();
|
|
77
77
|
ensureOneSymlink(symlinkPath, outputPath);
|
|
78
78
|
ensureOneSymlink(resolve(platform.codexDir(), "AGENTS.md"), outputPath);
|
|
79
|
+
// Copilot instructions — only create if ~/.copilot/ already exists (i.e. Copilot is installed)
|
|
80
|
+
const copilotDir = platform.copilotDir();
|
|
81
|
+
if (existsSync(copilotDir)) {
|
|
82
|
+
ensureOneSymlink(resolve(copilotDir, "copilot-instructions.md"), outputPath);
|
|
83
|
+
}
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
/** Returns true if AGENTS.md needs to be regenerated */
|
package/src/hooks/lib/paths.ts
CHANGED
|
@@ -61,6 +61,7 @@ export const platform = {
|
|
|
61
61
|
claudeDir: () => process.env.PAL_CLAUDE_DIR || resolve(h, ".claude"),
|
|
62
62
|
opencodeDir: () => process.env.PAL_OPENCODE_DIR || resolve(h, ".config", "opencode"),
|
|
63
63
|
cursorDir: () => process.env.PAL_CURSOR_DIR || resolve(h, ".cursor"),
|
|
64
|
+
copilotDir: () => process.env.PAL_COPILOT_DIR || resolve(h, ".copilot"),
|
|
64
65
|
codexDir: () => process.env.PAL_CODEX_DIR || resolve(h, ".codex"),
|
|
65
66
|
agentsDir: () => process.env.PAL_AGENTS_DIR || resolve(h, ".agents"),
|
|
66
67
|
} as const;
|
|
@@ -74,6 +75,7 @@ export const assets = {
|
|
|
74
75
|
agentsMdTemplate: () => pkg("assets", "templates", "AGENTS.md.template"),
|
|
75
76
|
claudeSettingsTemplate: () => pkg("assets", "templates", "settings.claude.json"),
|
|
76
77
|
cursorHooksTemplate: () => pkg("assets", "templates", "hooks.cursor.json"),
|
|
78
|
+
copilotHooksTemplate: () => pkg("assets", "templates", "hooks.copilot.json"),
|
|
77
79
|
agentTools: () => pkg("src", "tools", "agent"),
|
|
78
80
|
palDocs: () => pkg("assets", "templates", "PAL"),
|
|
79
81
|
} as const;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAL — Copilot target installer
|
|
3
|
+
* Writes hooks to ~/.copilot/hooks/pal-hooks.json.
|
|
4
|
+
* Copies skills and agents. Symlinks copilot-instructions.md to AGENTS.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync } 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
|
+
copyAgentsForCopilot,
|
|
13
|
+
copyPalDocs,
|
|
14
|
+
copySkills,
|
|
15
|
+
countSkills,
|
|
16
|
+
generateSkillIndex,
|
|
17
|
+
loadCopilotHooksTemplate,
|
|
18
|
+
log,
|
|
19
|
+
scaffoldPalSettings,
|
|
20
|
+
} from "../lib";
|
|
21
|
+
|
|
22
|
+
const PKG_ROOT = palPkg().replaceAll("\\", "/");
|
|
23
|
+
const COPILOT_DIR = platform.copilotDir();
|
|
24
|
+
const HOOKS_DIR = resolve(COPILOT_DIR, "hooks");
|
|
25
|
+
const HOOKS_FILE = resolve(HOOKS_DIR, "pal-hooks.json");
|
|
26
|
+
|
|
27
|
+
// --- Ensure dirs ---
|
|
28
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
29
|
+
|
|
30
|
+
// --- Write hooks file ---
|
|
31
|
+
const template = loadCopilotHooksTemplate(assets.copilotHooksTemplate(), PKG_ROOT);
|
|
32
|
+
writeFileSync(HOOKS_FILE, `${JSON.stringify(template, null, 2)}\n`, "utf-8");
|
|
33
|
+
log.success(`Written hooks to ${HOOKS_FILE}`);
|
|
34
|
+
|
|
35
|
+
// --- Install skills ---
|
|
36
|
+
const copilotSkillsDir = resolve(COPILOT_DIR, "skills");
|
|
37
|
+
copySkills(copilotSkillsDir);
|
|
38
|
+
generateSkillIndex();
|
|
39
|
+
log.success("Installed skills to ~/.copilot/skills/");
|
|
40
|
+
|
|
41
|
+
// --- Install agents ---
|
|
42
|
+
const copilotAgentsDir = resolve(COPILOT_DIR, "agents");
|
|
43
|
+
const agentCount = copyAgentsForCopilot(copilotAgentsDir);
|
|
44
|
+
if (agentCount > 0) log.success(`Installed ${agentCount} agents to ~/.copilot/agents/`);
|
|
45
|
+
|
|
46
|
+
// --- Copy PAL docs ---
|
|
47
|
+
const palDocsCount = copyPalDocs();
|
|
48
|
+
log.success(`Installed ${palDocsCount} PAL docs to ~/.pal/docs/`);
|
|
49
|
+
|
|
50
|
+
// --- Scaffold PAL settings ---
|
|
51
|
+
scaffoldPalSettings();
|
|
52
|
+
|
|
53
|
+
// --- Generate AGENTS.md + copilot-instructions.md symlink ---
|
|
54
|
+
// ensureSymlinks() inside regenerateIfNeeded() handles the symlink once ~/.copilot/ exists
|
|
55
|
+
regenerateIfNeeded();
|
|
56
|
+
const instructionsPath = resolve(COPILOT_DIR, "copilot-instructions.md");
|
|
57
|
+
log.success(
|
|
58
|
+
existsSync(instructionsPath)
|
|
59
|
+
? "copilot-instructions.md symlink present"
|
|
60
|
+
: "Generated AGENTS.md (copilot-instructions.md symlink will be created on next session)"
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
log.success("Copilot installation complete");
|
|
64
|
+
console.log("");
|
|
65
|
+
log.info(`Skills: ${countSkills()}`);
|
|
66
|
+
log.info(`Hooks: ${HOOKS_FILE}`);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAL — Copilot uninstaller
|
|
3
|
+
* Removes pal-hooks.json, skill symlinks, agents, and copilot-instructions.md symlink.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { copyFileSync, existsSync, lstatSync, readlinkSync, unlinkSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { platform } from "../../hooks/lib/paths";
|
|
9
|
+
import { log, removeAgentsFromCopilot, removePalDocs, removeSkills } from "../lib";
|
|
10
|
+
|
|
11
|
+
const COPILOT_DIR = platform.copilotDir();
|
|
12
|
+
const HOOKS_FILE = resolve(COPILOT_DIR, "hooks", "pal-hooks.json");
|
|
13
|
+
|
|
14
|
+
// --- Remove hooks file ---
|
|
15
|
+
if (existsSync(HOOKS_FILE)) {
|
|
16
|
+
copyFileSync(HOOKS_FILE, `${HOOKS_FILE}.bak.${Date.now()}`);
|
|
17
|
+
unlinkSync(HOOKS_FILE);
|
|
18
|
+
log.success("Removed pal-hooks.json");
|
|
19
|
+
} else {
|
|
20
|
+
log.info("No pal-hooks.json found, nothing to do");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// --- Remove skill symlinks ---
|
|
24
|
+
const copilotSkillsDir = resolve(COPILOT_DIR, "skills");
|
|
25
|
+
const removed = removeSkills(copilotSkillsDir);
|
|
26
|
+
if (removed.length > 0) {
|
|
27
|
+
log.success(`Removed ${removed.length} skill(s): ${removed.join(", ")}`);
|
|
28
|
+
} else {
|
|
29
|
+
log.info("No PAL skills found");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// --- Remove agents ---
|
|
33
|
+
const removedAgents = removeAgentsFromCopilot(resolve(COPILOT_DIR, "agents"));
|
|
34
|
+
if (removedAgents.length > 0) {
|
|
35
|
+
log.success(`Removed ${removedAgents.length} agent(s): ${removedAgents.join(", ")}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Remove copilot-instructions.md symlink (only if it points to AGENTS.md) ---
|
|
39
|
+
const instructionsPath = resolve(COPILOT_DIR, "copilot-instructions.md");
|
|
40
|
+
if (existsSync(instructionsPath)) {
|
|
41
|
+
try {
|
|
42
|
+
const stat = lstatSync(instructionsPath);
|
|
43
|
+
if (stat.isSymbolicLink()) {
|
|
44
|
+
const target = readlinkSync(instructionsPath);
|
|
45
|
+
if (target.includes("AGENTS.md")) {
|
|
46
|
+
unlinkSync(instructionsPath);
|
|
47
|
+
log.success("Removed copilot-instructions.md symlink");
|
|
48
|
+
} else {
|
|
49
|
+
log.info("copilot-instructions.md is not a PAL symlink — leaving it");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- Remove PAL docs ---
|
|
58
|
+
removePalDocs();
|
|
59
|
+
|
|
60
|
+
log.success("Copilot uninstall complete");
|
package/src/targets/lib.ts
CHANGED
|
@@ -432,7 +432,7 @@ export function countAgents(): number {
|
|
|
432
432
|
|
|
433
433
|
// --- Agent platform extraction ---
|
|
434
434
|
|
|
435
|
-
const AGENT_PLATFORMS = ["claude", "opencode", "cursor"] as const;
|
|
435
|
+
const AGENT_PLATFORMS = ["claude", "opencode", "cursor", "copilot"] as const;
|
|
436
436
|
type AgentPlatform = (typeof AGENT_PLATFORMS)[number];
|
|
437
437
|
|
|
438
438
|
/**
|
|
@@ -462,6 +462,7 @@ export function extractAgentForPlatform(
|
|
|
462
462
|
claude: [],
|
|
463
463
|
opencode: [],
|
|
464
464
|
cursor: [],
|
|
465
|
+
copilot: [],
|
|
465
466
|
};
|
|
466
467
|
let currentPlatform: AgentPlatform | null = null;
|
|
467
468
|
|
|
@@ -542,10 +543,25 @@ export function copyAgentsForCursor(cursorAgentsDir: string): number {
|
|
|
542
543
|
return installAgents(cursorAgentsDir, "cursor");
|
|
543
544
|
}
|
|
544
545
|
|
|
546
|
+
export function copyAgentsForCopilot(copilotAgentsDir: string): number {
|
|
547
|
+
return installAgents(copilotAgentsDir, "copilot");
|
|
548
|
+
}
|
|
549
|
+
|
|
545
550
|
export function removeAgentsFromCursor(cursorAgentsDir: string): string[] {
|
|
546
551
|
return uninstallAgents(cursorAgentsDir, "cursor");
|
|
547
552
|
}
|
|
548
553
|
|
|
554
|
+
export function removeAgentsFromCopilot(copilotAgentsDir: string): string[] {
|
|
555
|
+
return uninstallAgents(copilotAgentsDir, "copilot");
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Load and resolve the Copilot hooks template, substituting PKG_ROOT */
|
|
559
|
+
export function loadCopilotHooksTemplate(templatePath: string, pkgRoot: string): unknown {
|
|
560
|
+
return JSON.parse(
|
|
561
|
+
readFileSync(templatePath, "utf-8").replaceAll("{{PKG_ROOT}}", pkgRoot)
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
549
565
|
// --- Skill Index ---
|
|
550
566
|
|
|
551
567
|
interface SkillIndexEntry {
|