portable-agent-layer 0.24.2 → 0.26.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/skills/telos/tools/update-projects.ts +58 -53
- package/assets/templates/AGENTS.md.template +0 -1
- package/assets/templates/hooks.copilot.json +33 -0
- package/package.json +1 -1
- package/src/cli/index.ts +70 -26
- package/src/cli/setup-telos.ts +120 -0
- package/src/hooks/LoadContext.ts +20 -3
- package/src/hooks/lib/claude-md.ts +6 -5
- package/src/hooks/lib/context.ts +4 -4
- package/src/hooks/lib/paths.ts +2 -0
- package/src/hooks/lib/setup.ts +21 -75
- package/src/hooks/lib/token-usage.ts +2 -1
- package/src/targets/copilot/install.ts +66 -0
- package/src/targets/copilot/uninstall.ts +60 -0
- package/src/targets/lib.ts +17 -1
- package/src/tools/self-model.ts +3 -0
- package/src/tools/token-cost.ts +31 -10
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
|
|
|
@@ -38,64 +38,69 @@ function isoDate(): string {
|
|
|
38
38
|
return new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
console.error(
|
|
49
|
-
'\nExample: bun update-projects.ts my-proj "| my-proj | My Project | In progress | High | Notes |" "Added My Project"'
|
|
50
|
-
);
|
|
51
|
-
process.exit(1);
|
|
41
|
+
export interface UpsertProjectResult {
|
|
42
|
+
file: string;
|
|
43
|
+
id: string;
|
|
44
|
+
mode: "replaced" | "appended";
|
|
45
|
+
backed_up: boolean;
|
|
46
|
+
logged: boolean;
|
|
47
|
+
description: string;
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
50
|
+
export function upsertProject(
|
|
51
|
+
id: string,
|
|
52
|
+
row: string,
|
|
53
|
+
description: string
|
|
54
|
+
): UpsertProjectResult {
|
|
55
|
+
mkdirSync(BACKUPS_DIR, { recursive: true });
|
|
56
|
+
if (existsSync(PROJECTS_FILE)) {
|
|
57
|
+
const backupName = `PROJECTS-${timestamp()}.md`;
|
|
58
|
+
copyFileSync(PROJECTS_FILE, resolve(BACKUPS_DIR, backupName));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const existing = existsSync(PROJECTS_FILE) ? readFileSync(PROJECTS_FILE, "utf-8") : "";
|
|
62
|
+
|
|
63
|
+
const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
64
|
+
const idPattern = new RegExp(`^\\|\\s*${escapedId}\\s*\\|.*$`, "m");
|
|
65
|
+
let mode: "replaced" | "appended";
|
|
60
66
|
|
|
61
|
-
|
|
67
|
+
if (idPattern.test(existing)) {
|
|
68
|
+
writeFileSync(PROJECTS_FILE, existing.replace(idPattern, row.trim()), "utf-8");
|
|
69
|
+
mode = "replaced";
|
|
70
|
+
} else {
|
|
71
|
+
const separator = existing.trim() ? "\n" : "";
|
|
72
|
+
writeFileSync(
|
|
73
|
+
PROJECTS_FILE,
|
|
74
|
+
`${existing.trimEnd()}${separator}${row.trim()}\n`,
|
|
75
|
+
"utf-8"
|
|
76
|
+
);
|
|
77
|
+
mode = "appended";
|
|
78
|
+
}
|
|
62
79
|
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
80
|
+
const logEntry = `- **${isoDate()}** — \`PROJECTS.md\` [${id}]: ${description}`;
|
|
81
|
+
const existingLog = existsSync(UPDATES_LOG)
|
|
82
|
+
? readFileSync(UPDATES_LOG, "utf-8")
|
|
83
|
+
: "# TELOS Updates\n";
|
|
84
|
+
writeFileSync(UPDATES_LOG, `${existingLog.trimEnd()}\n${logEntry}\n`, "utf-8");
|
|
66
85
|
|
|
67
|
-
|
|
68
|
-
const updated = existing.replace(idPattern, row.trim());
|
|
69
|
-
writeFileSync(PROJECTS_FILE, updated, "utf-8");
|
|
70
|
-
mode = "replaced";
|
|
71
|
-
} else {
|
|
72
|
-
const separator = existing.trim() ? "\n" : "";
|
|
73
|
-
writeFileSync(
|
|
74
|
-
PROJECTS_FILE,
|
|
75
|
-
`${existing.trimEnd()}${separator}${row.trim()}\n`,
|
|
76
|
-
"utf-8"
|
|
77
|
-
);
|
|
78
|
-
mode = "appended";
|
|
86
|
+
return { file: "PROJECTS.md", id, mode, backed_up: true, logged: true, description };
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
function run() {
|
|
90
|
+
const args = process.argv.slice(2);
|
|
91
|
+
const id = args[0];
|
|
92
|
+
const row = args[1];
|
|
93
|
+
const description = args[2];
|
|
94
|
+
|
|
95
|
+
if (!id || !row || !description) {
|
|
96
|
+
console.error('Usage: bun update-projects.ts <id> "<row>" "<description>"');
|
|
97
|
+
console.error(
|
|
98
|
+
'\nExample: bun update-projects.ts my-proj "| my-proj | My Project | In progress | High | Notes |" "Added My Project"'
|
|
99
|
+
);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(JSON.stringify(upsertProject(id, row, description), null, 2));
|
|
104
|
+
}
|
|
87
105
|
|
|
88
|
-
|
|
89
|
-
JSON.stringify(
|
|
90
|
-
{
|
|
91
|
-
file: "PROJECTS.md",
|
|
92
|
-
id,
|
|
93
|
-
mode,
|
|
94
|
-
backed_up: true,
|
|
95
|
-
logged: true,
|
|
96
|
-
description,
|
|
97
|
-
},
|
|
98
|
-
null,
|
|
99
|
-
2
|
|
100
|
-
)
|
|
101
|
-
);
|
|
106
|
+
if (import.meta.main) run();
|
|
@@ -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
|
}
|
|
@@ -320,6 +329,14 @@ function checkOpencodePluginInstalled(): boolean {
|
|
|
320
329
|
return existsSync(resolve(platform.opencodeDir(), "plugins", "pal-plugin.ts"));
|
|
321
330
|
}
|
|
322
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
|
+
|
|
323
340
|
function checkHookHealth(home: string): HookHealth {
|
|
324
341
|
const logPath = resolve(home, "memory", "state", "debug.log");
|
|
325
342
|
|
|
@@ -357,6 +374,7 @@ interface DoctorResult {
|
|
|
357
374
|
claude: ToolCheck;
|
|
358
375
|
opencode: ToolCheck;
|
|
359
376
|
cursor: ToolCheck;
|
|
377
|
+
copilot: ToolCheck;
|
|
360
378
|
hasAgent: boolean;
|
|
361
379
|
}
|
|
362
380
|
|
|
@@ -368,6 +386,7 @@ function doctor(silent = false): DoctorResult {
|
|
|
368
386
|
claude: { name: "claude", available: true },
|
|
369
387
|
opencode: { name: "opencode", available: true },
|
|
370
388
|
cursor: { name: "cursor", available: true },
|
|
389
|
+
copilot: { name: "copilot", available: true },
|
|
371
390
|
hasAgent: true,
|
|
372
391
|
};
|
|
373
392
|
}
|
|
@@ -376,7 +395,9 @@ function doctor(silent = false): DoctorResult {
|
|
|
376
395
|
const claude = checkTool("claude");
|
|
377
396
|
const opencode = checkTool("opencode");
|
|
378
397
|
const cursor = checkTool("cursor");
|
|
379
|
-
const
|
|
398
|
+
const copilot = checkTool("copilot", ["version"]);
|
|
399
|
+
const hasAgent =
|
|
400
|
+
claude.available || opencode.available || cursor.available || copilot.available;
|
|
380
401
|
|
|
381
402
|
const home = palHome();
|
|
382
403
|
const telosCount = (() => {
|
|
@@ -404,6 +425,9 @@ function doctor(silent = false): DoctorResult {
|
|
|
404
425
|
cursor.available
|
|
405
426
|
? ok(`Cursor ${cursor.version || ""}`.trim())
|
|
406
427
|
: fail("Cursor — not found");
|
|
428
|
+
copilot.available
|
|
429
|
+
? ok(`Copilot ${copilot.version || ""}`.trim())
|
|
430
|
+
: fail("Copilot — not found");
|
|
407
431
|
ok(`PAL home: ${home}`);
|
|
408
432
|
telosCount > 0 ? ok(`TELOS: ${telosCount} files`) : fail("TELOS: not scaffolded");
|
|
409
433
|
|
|
@@ -474,6 +498,12 @@ function doctor(silent = false): DoctorResult {
|
|
|
474
498
|
? ok(`Cursor skills: ${n}`)
|
|
475
499
|
: warn("Cursor skills — none found (run 'pal cli install --cursor')");
|
|
476
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
|
+
}
|
|
477
507
|
|
|
478
508
|
// Dependencies
|
|
479
509
|
const nodeModulesPath = resolve(palPkg(), "node_modules");
|
|
@@ -497,6 +527,14 @@ function doctor(silent = false): DoctorResult {
|
|
|
497
527
|
? ok("Cursor hooks registered")
|
|
498
528
|
: fail("Cursor hooks — not registered (run 'pal cli install --cursor')");
|
|
499
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
|
+
}
|
|
500
538
|
|
|
501
539
|
// API key checks
|
|
502
540
|
process.env.PAL_ANTHROPIC_API_KEY
|
|
@@ -530,13 +568,12 @@ function doctor(silent = false): DoctorResult {
|
|
|
530
568
|
console.log("");
|
|
531
569
|
}
|
|
532
570
|
|
|
533
|
-
return { bun, claude, opencode, cursor, hasAgent };
|
|
571
|
+
return { bun, claude, opencode, cursor, copilot, hasAgent };
|
|
534
572
|
}
|
|
535
573
|
|
|
536
574
|
// ── Commands ──
|
|
537
575
|
|
|
538
576
|
async function init(args: string[]) {
|
|
539
|
-
const { ensureSetupState, isSetupComplete } = await import("../hooks/lib/setup");
|
|
540
577
|
const { scaffoldTelos } = await import("../targets/lib");
|
|
541
578
|
|
|
542
579
|
banner();
|
|
@@ -553,20 +590,13 @@ async function init(args: string[]) {
|
|
|
553
590
|
mkdirSync(resolve(home, "memory"), { recursive: true });
|
|
554
591
|
|
|
555
592
|
scaffoldTelos();
|
|
556
|
-
ensureSetupState();
|
|
557
593
|
|
|
558
594
|
// Auto-detect available targets
|
|
559
595
|
const targets = resolveTargets(args, health);
|
|
560
596
|
await install(targets);
|
|
561
|
-
|
|
562
|
-
console.log("");
|
|
563
|
-
const state = ensureSetupState();
|
|
564
|
-
if (!isSetupComplete(state)) {
|
|
565
|
-
log.info("Start a session — PAL will guide you through first-run setup");
|
|
566
|
-
}
|
|
567
597
|
}
|
|
568
598
|
|
|
569
|
-
async function install(targets:
|
|
599
|
+
async function install(targets: Targets) {
|
|
570
600
|
// Ensure dependencies are installed
|
|
571
601
|
const pkg = palPkg();
|
|
572
602
|
log.info("Installing dependencies...");
|
|
@@ -582,9 +612,11 @@ async function install(targets: { claude: boolean; opencode: boolean; cursor: bo
|
|
|
582
612
|
// Scaffold TELOS + PAL settings, then prompt for missing identity
|
|
583
613
|
const { scaffoldTelos, scaffoldPalSettings } = await import("../targets/lib");
|
|
584
614
|
const { promptIdentity } = await import("./setup-identity");
|
|
615
|
+
const { promptTelos } = await import("./setup-telos");
|
|
585
616
|
scaffoldTelos();
|
|
586
617
|
scaffoldPalSettings();
|
|
587
618
|
await promptIdentity();
|
|
619
|
+
await promptTelos();
|
|
588
620
|
|
|
589
621
|
if (targets.claude) {
|
|
590
622
|
console.log("━━━ Claude Code ━━━");
|
|
@@ -604,6 +636,12 @@ async function install(targets: { claude: boolean; opencode: boolean; cursor: bo
|
|
|
604
636
|
console.log("");
|
|
605
637
|
}
|
|
606
638
|
|
|
639
|
+
if (targets.copilot) {
|
|
640
|
+
console.log("━━━ Copilot ━━━");
|
|
641
|
+
await import("../targets/copilot/install");
|
|
642
|
+
console.log("");
|
|
643
|
+
}
|
|
644
|
+
|
|
607
645
|
log.success("Done. Existing config was preserved — only new entries were added.");
|
|
608
646
|
}
|
|
609
647
|
|
|
@@ -628,6 +666,12 @@ async function uninstall(args: string[]) {
|
|
|
628
666
|
console.log("");
|
|
629
667
|
}
|
|
630
668
|
|
|
669
|
+
if (targets.copilot) {
|
|
670
|
+
console.log("━━━ Copilot ━━━");
|
|
671
|
+
await import("../targets/copilot/uninstall");
|
|
672
|
+
console.log("");
|
|
673
|
+
}
|
|
674
|
+
|
|
631
675
|
log.success(
|
|
632
676
|
`PAL uninstalled. Your TELOS, skills, and memory are still in ${palHome()}.`
|
|
633
677
|
);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive TELOS setup — prompts for personal context during `pal install`.
|
|
3
|
+
* Skips any step whose TELOS file already has real content.
|
|
4
|
+
* Projects use the upsertProject tool directly with a structured add-another loop.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { writeFileSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import * as clack from "@clack/prompts";
|
|
10
|
+
import { upsertProject } from "../../assets/skills/telos/tools/update-projects";
|
|
11
|
+
import { palHome } from "../hooks/lib/paths";
|
|
12
|
+
import { hasRealContent, SETUP_STEPS, STEP_ORDER } from "../hooks/lib/setup";
|
|
13
|
+
|
|
14
|
+
function toKebabCase(name: string): string {
|
|
15
|
+
return name
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
18
|
+
.replace(/^-|-$/g, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function promptProjectsLoop(): Promise<void> {
|
|
22
|
+
const addFirst = await clack.confirm({
|
|
23
|
+
message: "Do you want to add any projects now?",
|
|
24
|
+
initialValue: true,
|
|
25
|
+
});
|
|
26
|
+
if (clack.isCancel(addFirst) || !addFirst) return;
|
|
27
|
+
|
|
28
|
+
let addMore = true;
|
|
29
|
+
while (addMore) {
|
|
30
|
+
const name = await clack.text({
|
|
31
|
+
message: "Project name?",
|
|
32
|
+
placeholder: "e.g. PAL, My SaaS, Work Dashboard",
|
|
33
|
+
});
|
|
34
|
+
if (clack.isCancel(name)) return;
|
|
35
|
+
|
|
36
|
+
const status = await clack.select({
|
|
37
|
+
message: "Status?",
|
|
38
|
+
options: [
|
|
39
|
+
{ value: "Active", label: "Active" },
|
|
40
|
+
{ value: "Planning", label: "Planning" },
|
|
41
|
+
{ value: "Paused", label: "Paused" },
|
|
42
|
+
{ value: "Complete", label: "Complete" },
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
if (clack.isCancel(status)) return;
|
|
46
|
+
|
|
47
|
+
const priority = await clack.select({
|
|
48
|
+
message: "Priority?",
|
|
49
|
+
options: [
|
|
50
|
+
{ value: "High", label: "High" },
|
|
51
|
+
{ value: "Medium", label: "Medium" },
|
|
52
|
+
{ value: "Low", label: "Low" },
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
if (clack.isCancel(priority)) return;
|
|
56
|
+
|
|
57
|
+
const notes = await clack.text({
|
|
58
|
+
message: "Notes? (optional — leave blank to skip)",
|
|
59
|
+
placeholder: "e.g. Building the v2 API, blocked on design review",
|
|
60
|
+
});
|
|
61
|
+
if (clack.isCancel(notes)) return;
|
|
62
|
+
|
|
63
|
+
const id = toKebabCase(name as string);
|
|
64
|
+
const row = `| ${id} | ${name} | ${status} | ${priority} | ${notes || ""} |`;
|
|
65
|
+
upsertProject(id, row, `Added ${name} during PAL setup`);
|
|
66
|
+
clack.log.success(`Added: ${name}`);
|
|
67
|
+
|
|
68
|
+
const again = await clack.confirm({
|
|
69
|
+
message: "Add another project?",
|
|
70
|
+
initialValue: false,
|
|
71
|
+
});
|
|
72
|
+
if (clack.isCancel(again) || !again) addMore = false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Prompt for missing TELOS context. Skips any step whose file already has real content. */
|
|
77
|
+
export async function promptTelos(): Promise<void> {
|
|
78
|
+
// Skip interactive prompts in non-TTY environments (tests, CI)
|
|
79
|
+
if (!process.stdin.isTTY) return;
|
|
80
|
+
|
|
81
|
+
const home = palHome();
|
|
82
|
+
const pending = STEP_ORDER.filter(
|
|
83
|
+
(key) => !hasRealContent(resolve(home, SETUP_STEPS[key].file))
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (pending.length === 0) {
|
|
87
|
+
clack.log.info("TELOS already configured");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
clack.intro("Personal Context Setup");
|
|
92
|
+
clack.note(
|
|
93
|
+
"Answer in a sentence or two — you can edit the files in ~/.pal/telos/ for more detail later.",
|
|
94
|
+
"Quick setup"
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
for (const key of pending) {
|
|
98
|
+
if (key === "projects") {
|
|
99
|
+
await promptProjectsLoop();
|
|
100
|
+
} else {
|
|
101
|
+
const step = SETUP_STEPS[key];
|
|
102
|
+
const title = key.charAt(0).toUpperCase() + key.slice(1);
|
|
103
|
+
|
|
104
|
+
const answer = await clack.text({
|
|
105
|
+
message: step.question,
|
|
106
|
+
placeholder: step.hint,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (clack.isCancel(answer)) {
|
|
110
|
+
clack.cancel("Setup cancelled");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const filePath = resolve(home, step.file);
|
|
115
|
+
writeFileSync(filePath, `# ${title}\n\n${answer}\n`, "utf-8");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
clack.outro("Personal context saved ✓");
|
|
120
|
+
}
|
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
|
}
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
} from "node:fs";
|
|
20
20
|
import { dirname, relative, resolve } from "node:path";
|
|
21
21
|
import { assets, ensureDir, paths, platform } from "./paths";
|
|
22
|
-
import { buildSetupPrompt, readSetupState } from "./setup";
|
|
23
22
|
|
|
24
23
|
const TEMPLATE_PATH = assets.agentsMdTemplate();
|
|
25
24
|
|
|
@@ -76,6 +75,11 @@ function ensureSymlinks(): void {
|
|
|
76
75
|
const { outputPath, symlinkPath } = getOutputPaths();
|
|
77
76
|
ensureOneSymlink(symlinkPath, outputPath);
|
|
78
77
|
ensureOneSymlink(resolve(platform.codexDir(), "AGENTS.md"), outputPath);
|
|
78
|
+
// Copilot instructions — only create if ~/.copilot/ already exists (i.e. Copilot is installed)
|
|
79
|
+
const copilotDir = platform.copilotDir();
|
|
80
|
+
if (existsSync(copilotDir)) {
|
|
81
|
+
ensureOneSymlink(resolve(copilotDir, "copilot-instructions.md"), outputPath);
|
|
82
|
+
}
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
/** Returns true if AGENTS.md needs to be regenerated */
|
|
@@ -109,14 +113,11 @@ import { identity } from "./settings";
|
|
|
109
113
|
export function buildClaudeMd(): string {
|
|
110
114
|
const template = existsSync(TEMPLATE_PATH)
|
|
111
115
|
? readFileSync(TEMPLATE_PATH, "utf-8")
|
|
112
|
-
: "# PAL Context\n
|
|
116
|
+
: "# PAL Context\n";
|
|
113
117
|
|
|
114
|
-
const state = readSetupState();
|
|
115
|
-
const setupPrompt = state ? buildSetupPrompt(state) : null;
|
|
116
118
|
const id = identity();
|
|
117
119
|
|
|
118
120
|
return template
|
|
119
|
-
.replace("{{SETUP_PROMPT}}", setupPrompt ? `${setupPrompt}\n` : "")
|
|
120
121
|
.replaceAll("{{IDENTITY_NAME}}", id.ai.name)
|
|
121
122
|
.replaceAll("{{IDENTITY_DISPLAY}}", id.ai.displayName)
|
|
122
123
|
.replaceAll("{{IDENTITY_CATCHPHRASE}}", id.ai.catchphrase)
|
package/src/hooks/lib/context.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { paths } from "./paths";
|
|
|
13
13
|
import { loadRecentNotes } from "./relationship";
|
|
14
14
|
import { readSessionNames } from "./session-names";
|
|
15
15
|
import * as settings from "./settings";
|
|
16
|
-
import {
|
|
16
|
+
import { isSetupComplete, readSetupState, remainingSteps, STEP_ORDER } from "./setup";
|
|
17
17
|
import { computeSignalTrends, formatTrends } from "./signal-trends";
|
|
18
18
|
import { readFramePrinciples } from "./wisdom";
|
|
19
19
|
import { readProjectHistory, readSessions, recentSessions } from "./work-tracking";
|
|
@@ -141,12 +141,12 @@ export function buildGreeting(): string[] {
|
|
|
141
141
|
const counts = loadCachedCounts();
|
|
142
142
|
const work = loadActiveWork();
|
|
143
143
|
const setupState = readSetupState();
|
|
144
|
-
const
|
|
144
|
+
const setupIncomplete = setupState && !isSetupComplete(setupState);
|
|
145
145
|
|
|
146
146
|
const greeting: string[] = [];
|
|
147
147
|
|
|
148
|
-
if (
|
|
149
|
-
const done = STEP_ORDER.length -
|
|
148
|
+
if (setupIncomplete) {
|
|
149
|
+
const done = STEP_ORDER.length - remainingSteps(setupState).length;
|
|
150
150
|
greeting.push(
|
|
151
151
|
`🔧 PAL setup ${done}/${STEP_ORDER.length} | ${counts.signals} signals`
|
|
152
152
|
);
|
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;
|
package/src/hooks/lib/setup.ts
CHANGED
|
@@ -23,31 +23,33 @@ export interface SetupState {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/** Ordered setup steps — defines the wizard flow */
|
|
26
|
-
const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
|
|
26
|
+
export const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
|
|
27
27
|
mission: {
|
|
28
28
|
file: "telos/MISSION.md",
|
|
29
|
-
question:
|
|
30
|
-
|
|
29
|
+
question:
|
|
30
|
+
"What do you do? What's your role and core purpose? (~/.pal/telos/MISSION.md)",
|
|
31
|
+
hint: "e.g. Senior software engineer building developer tooling at Acme Corp",
|
|
31
32
|
},
|
|
32
33
|
goals: {
|
|
33
34
|
file: "telos/GOALS.md",
|
|
34
|
-
question:
|
|
35
|
-
|
|
35
|
+
question:
|
|
36
|
+
"What are your current goals? (short-term, medium-term, long-term) (~/.pal/telos/GOALS.md)",
|
|
37
|
+
hint: "e.g. Ship v2 by Q3, learn Rust, get promoted to staff engineer",
|
|
36
38
|
},
|
|
37
39
|
projects: {
|
|
38
40
|
file: "telos/PROJECTS.md",
|
|
39
|
-
question: "What projects are you currently working on?",
|
|
40
|
-
hint: "
|
|
41
|
+
question: "What projects are you currently working on? (~/.pal/telos/PROJECTS.md)",
|
|
42
|
+
hint: "e.g. PAL (active, high priority), personal blog (paused), side SaaS (early stage)",
|
|
41
43
|
},
|
|
42
44
|
beliefs: {
|
|
43
45
|
file: "telos/BELIEFS.md",
|
|
44
|
-
question: "What principles or values guide your work?",
|
|
45
|
-
hint: "
|
|
46
|
+
question: "What principles or values guide your work? (~/.pal/telos/BELIEFS.md)",
|
|
47
|
+
hint: "e.g. Simple code > clever code, ship early and iterate, always write tests",
|
|
46
48
|
},
|
|
47
49
|
challenges: {
|
|
48
50
|
file: "telos/CHALLENGES.md",
|
|
49
|
-
question: "What are your biggest current challenges?",
|
|
50
|
-
hint: "
|
|
51
|
+
question: "What are your biggest current challenges? (~/.pal/telos/CHALLENGES.md)",
|
|
52
|
+
hint: "e.g. Context switching between projects, unclear requirements, work-life balance",
|
|
51
53
|
},
|
|
52
54
|
};
|
|
53
55
|
|
|
@@ -58,21 +60,17 @@ function setupPath(): string {
|
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/** Check if a TELOS file has real content (not just template scaffolding) */
|
|
61
|
-
function hasRealContent(filePath: string): boolean {
|
|
63
|
+
export function hasRealContent(filePath: string): boolean {
|
|
62
64
|
if (!existsSync(filePath)) return false;
|
|
63
65
|
try {
|
|
64
66
|
const content = readFileSync(filePath, "utf-8").trim();
|
|
65
|
-
return content
|
|
66
|
-
.
|
|
67
|
-
.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
l.trim() &&
|
|
73
|
-
!/^\s*-\s*$/.test(l) &&
|
|
74
|
-
!/^\s*\|/.test(l)
|
|
75
|
-
);
|
|
67
|
+
return content.split("\n").some((l) => {
|
|
68
|
+
if (!l.trim()) return false;
|
|
69
|
+
if (l.startsWith("#")) return false;
|
|
70
|
+
if (l.startsWith("<!--") || l.startsWith("-->")) return false;
|
|
71
|
+
if (/^\s*-\s*$/.test(l)) return false;
|
|
72
|
+
return true; // includes table rows (| ... |) — counts as real content
|
|
73
|
+
});
|
|
76
74
|
} catch {
|
|
77
75
|
return false;
|
|
78
76
|
}
|
|
@@ -123,55 +121,3 @@ export function remainingSteps(state: SetupState): string[] {
|
|
|
123
121
|
export function isSetupComplete(state: SetupState): boolean {
|
|
124
122
|
return state.completed;
|
|
125
123
|
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Build the system-prompt instructions for the current setup state.
|
|
129
|
-
* Returns null if setup is already complete.
|
|
130
|
-
*/
|
|
131
|
-
export function buildSetupPrompt(state: SetupState): string | null {
|
|
132
|
-
if (state.completed) return null;
|
|
133
|
-
|
|
134
|
-
const remaining = remainingSteps(state);
|
|
135
|
-
if (remaining.length === 0) return null;
|
|
136
|
-
|
|
137
|
-
const completedSteps = STEP_ORDER.filter((k) => state.steps[k]?.done);
|
|
138
|
-
const totalSteps = STEP_ORDER.length;
|
|
139
|
-
|
|
140
|
-
const lines: string[] = [
|
|
141
|
-
"## IMPORTANT: PAL First-Run Setup Required",
|
|
142
|
-
"",
|
|
143
|
-
"TELOS files are empty — the user's identity is already configured (via the installer),",
|
|
144
|
-
"but personal context is still needed. You MUST start the setup process immediately.",
|
|
145
|
-
"Greet them, explain that PAL needs to learn about them to personalize future sessions,",
|
|
146
|
-
"and ask the first remaining question below. Do NOT wait for the user to ask about setup.",
|
|
147
|
-
"",
|
|
148
|
-
];
|
|
149
|
-
|
|
150
|
-
if (completedSteps.length > 0) {
|
|
151
|
-
lines.push(
|
|
152
|
-
`Setup in progress — ${completedSteps.length}/${totalSteps} steps complete. Continue from the next remaining step.`,
|
|
153
|
-
""
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
lines.push("### Steps to complete (ask one at a time):", "");
|
|
158
|
-
|
|
159
|
-
for (const key of remaining) {
|
|
160
|
-
const step = state.steps[key];
|
|
161
|
-
lines.push(`- **${key}** — Ask: "${step.question}" → ${step.hint}`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
lines.push(
|
|
165
|
-
"",
|
|
166
|
-
"### After each answer:",
|
|
167
|
-
"1. Write the user's answer to the corresponding TELOS file.",
|
|
168
|
-
`2. Read \`memory/state/setup.json\`, set \`steps.<key>.done = true\`, and write it back.`,
|
|
169
|
-
"3. Ask the next remaining question.",
|
|
170
|
-
"",
|
|
171
|
-
`When all steps are done (or the user wants to skip), set \`completed: true\` in setup.json.`,
|
|
172
|
-
"",
|
|
173
|
-
"Keep it conversational and natural. If the user wants to skip a step, mark it done and move on."
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
return lines.join("\n");
|
|
177
|
-
}
|
|
@@ -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 {
|
package/src/tools/self-model.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { parseArgs } from "node:util";
|
|
|
21
21
|
import { inference } from "../hooks/lib/inference";
|
|
22
22
|
import { SONNET_MODEL } from "../hooks/lib/models";
|
|
23
23
|
import { ensureDir, paths } from "../hooks/lib/paths";
|
|
24
|
+
import { logTokenUsage } from "../hooks/lib/token-usage";
|
|
24
25
|
|
|
25
26
|
// ── Config ──
|
|
26
27
|
|
|
@@ -537,6 +538,8 @@ export async function composeSelfModel(days: number): Promise<string> {
|
|
|
537
538
|
timeout: 30000,
|
|
538
539
|
});
|
|
539
540
|
|
|
541
|
+
if (result.usage) logTokenUsage("self-model", result.usage, SONNET_MODEL);
|
|
542
|
+
|
|
540
543
|
if (result.success && result.output) {
|
|
541
544
|
// Append meta line if inference didn't include it
|
|
542
545
|
const output = result.output;
|
package/src/tools/token-cost.ts
CHANGED
|
@@ -258,10 +258,11 @@ export function readClaudeCode(projectFilter?: string): {
|
|
|
258
258
|
return { buckets, byModel, byProject };
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
// ── PAL
|
|
261
|
+
// ── PAL inference ──
|
|
262
262
|
|
|
263
263
|
export function readPalInference(): {
|
|
264
264
|
buckets: TimeBuckets;
|
|
265
|
+
byModel: Record<string, TimeBuckets>;
|
|
265
266
|
byCaller: Record<string, Bucket>;
|
|
266
267
|
} {
|
|
267
268
|
const now = new Date();
|
|
@@ -270,13 +271,14 @@ export function readPalInference(): {
|
|
|
270
271
|
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
271
272
|
|
|
272
273
|
const buckets = emptyTimeBuckets();
|
|
274
|
+
const byModel: Record<string, TimeBuckets> = {};
|
|
273
275
|
const byCaller: Record<string, Bucket> = {};
|
|
274
276
|
|
|
275
277
|
const filepath = resolve(palHome(), "memory", "signals", "token-usage.jsonl");
|
|
276
|
-
if (!existsSync(filepath)) return { buckets, byCaller };
|
|
278
|
+
if (!existsSync(filepath)) return { buckets, byModel, byCaller };
|
|
277
279
|
|
|
278
280
|
const content = readFileSync(filepath, "utf-8").trim();
|
|
279
|
-
if (!content) return { buckets, byCaller };
|
|
281
|
+
if (!content) return { buckets, byModel, byCaller };
|
|
280
282
|
|
|
281
283
|
for (const line of content.split("\n")) {
|
|
282
284
|
try {
|
|
@@ -299,6 +301,19 @@ export function readPalInference(): {
|
|
|
299
301
|
weekAgo,
|
|
300
302
|
monthAgo
|
|
301
303
|
);
|
|
304
|
+
if (!byModel[e.model]) byModel[e.model] = emptyTimeBuckets();
|
|
305
|
+
addToTimeBuckets(
|
|
306
|
+
byModel[e.model],
|
|
307
|
+
e.ts,
|
|
308
|
+
e.model,
|
|
309
|
+
e.inputTokens,
|
|
310
|
+
e.outputTokens,
|
|
311
|
+
0,
|
|
312
|
+
0,
|
|
313
|
+
todayPrefix,
|
|
314
|
+
weekAgo,
|
|
315
|
+
monthAgo
|
|
316
|
+
);
|
|
302
317
|
if (!byCaller[e.caller]) byCaller[e.caller] = emptyBucket();
|
|
303
318
|
addToBucket(byCaller[e.caller], e.model, e.inputTokens, e.outputTokens, 0, 0);
|
|
304
319
|
} catch {
|
|
@@ -306,7 +321,7 @@ export function readPalInference(): {
|
|
|
306
321
|
}
|
|
307
322
|
}
|
|
308
323
|
|
|
309
|
-
return { buckets, byCaller };
|
|
324
|
+
return { buckets, byModel, byCaller };
|
|
310
325
|
}
|
|
311
326
|
|
|
312
327
|
// ── CLI ──
|
|
@@ -352,12 +367,18 @@ function run() {
|
|
|
352
367
|
}
|
|
353
368
|
}
|
|
354
369
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
370
|
+
for (const [model, tb] of Object.entries(pal.byModel)) {
|
|
371
|
+
if (tb.total.calls === 0) continue;
|
|
372
|
+
const label = model.includes("haiku")
|
|
373
|
+
? "Haiku"
|
|
374
|
+
: model.includes("sonnet")
|
|
375
|
+
? "Sonnet"
|
|
376
|
+
: model.replace("claude-", "");
|
|
377
|
+
console.log(`\n PAL Inference (${label})\n`);
|
|
378
|
+
printRow("Today", tb.today);
|
|
379
|
+
printRow("7d", tb.week);
|
|
380
|
+
printRow("30d", tb.month);
|
|
381
|
+
printRow("Total", tb.total);
|
|
361
382
|
}
|
|
362
383
|
|
|
363
384
|
const grand = emptyBucket();
|