portable-agent-layer 0.48.0 → 0.50.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/assets/STATUSLINE.md +124 -0
- package/assets/statusline.ps1 +142 -0
- package/assets/statusline.sh +179 -0
- package/assets/templates/settings.claude.json +21 -0
- package/package.json +1 -1
- package/src/hooks/handlers/update-check.ts +2 -2
- package/src/hooks/lib/paths.ts +2 -0
- package/src/targets/claude/install.ts +9 -1
- package/src/targets/claude/uninstall.ts +9 -1
- package/src/targets/codex/install.ts +13 -0
- package/src/targets/codex/uninstall.ts +11 -0
- package/src/targets/cursor/install.ts +17 -1
- package/src/targets/cursor/uninstall.ts +15 -0
- package/src/targets/lib.ts +317 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# PAL Status Line
|
|
2
|
+
|
|
3
|
+
A customizable status line for Claude Code that displays context usage, active model, session cost, current directory, and git branch.
|
|
4
|
+
|
|
5
|
+
## What It Shows
|
|
6
|
+
|
|
7
|
+
**Line 1:** Model name, current directory, git branch, hook health, open ISCs, signal trend
|
|
8
|
+
**Line 2:** Context usage progress bar, percentage used, session cost, remaining context, rate limits
|
|
9
|
+
**Line 3:** Update notice (only when a new PAL version is available)
|
|
10
|
+
|
|
11
|
+
Example output (macOS/Linux):
|
|
12
|
+
```
|
|
13
|
+
[Opus] 📁 portable-agent-layer 🌿 main 📋 21 open ISCs sig: 3.0/10 ↓
|
|
14
|
+
████████░░░░░░░░░░░░ 45% │ $0.12 │ 55% free
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Example output (Windows):
|
|
18
|
+
```
|
|
19
|
+
[Opus] folder: portable-agent-layer (git: main) 21 open ISCs sig: 3.0/10 v
|
|
20
|
+
########------------ 45% - $0.12 - 55% free
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Colors adapt based on context usage:
|
|
24
|
+
- 🟢 Green: 0-60% used
|
|
25
|
+
- 🟡 Yellow: 60-80% used
|
|
26
|
+
- 🔴 Red: >80% used
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
### On macOS / Linux
|
|
31
|
+
|
|
32
|
+
1. Make the script executable:
|
|
33
|
+
```bash
|
|
34
|
+
chmod +x ~/.claude/statusline.sh
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
2. Add to `~/.claude/settings.json`:
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"statusLine": {
|
|
41
|
+
"type": "command",
|
|
42
|
+
"command": "~/.claude/statusline.sh",
|
|
43
|
+
"padding": 2
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The statusline script is at: `portable-agent-layer/assets/statusline.sh`
|
|
49
|
+
|
|
50
|
+
### Cursor CLI
|
|
51
|
+
|
|
52
|
+
1. Make the script executable:
|
|
53
|
+
```bash
|
|
54
|
+
chmod +x ~/.cursor/statusline.sh
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
2. Add to `~/.cursor/cli-config.json`:
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"statusLine": {
|
|
61
|
+
"type": "command",
|
|
62
|
+
"command": "~/.cursor/statusline.sh",
|
|
63
|
+
"padding": 2
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Copy `assets/statusline.sh` to `~/.cursor/statusline.sh` (or symlink). Restart the Cursor CLI after editing config.
|
|
69
|
+
|
|
70
|
+
On Cursor, session cost and rate limits are usually absent from stdin — the script omits the cost segment and hides rate limits automatically. PAL indicators (hooks, ISCs, signal, update) still read from `~/.pal/memory/state/`.
|
|
71
|
+
|
|
72
|
+
### On Windows
|
|
73
|
+
|
|
74
|
+
1. Add to `~/.claude/settings.json`:
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"statusLine": {
|
|
78
|
+
"type": "command",
|
|
79
|
+
"command": "powershell -NoProfile -Command \"Get-Content -Raw | & 'C:\\path\\to\\statusline.ps1'\"",
|
|
80
|
+
"padding": 2
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Or copy `statusline.ps1` to `~/.claude/statusline.ps1` and reference it from settings.
|
|
86
|
+
|
|
87
|
+
The statusline script is at: `portable-agent-layer/assets/statusline.ps1`
|
|
88
|
+
|
|
89
|
+
## Data Displayed
|
|
90
|
+
|
|
91
|
+
- **Model:** Active Claude model (Opus, Sonnet, Haiku, etc.)
|
|
92
|
+
- **Directory:** Repository name (if in a git repo) or folder name
|
|
93
|
+
- **Git Branch:** Current branch, fetched from git worktree or `git` command
|
|
94
|
+
- **Hook Health:** Count of ERROR lines in `~/.pal/memory/state/debug.log` from last 24h (hidden when 0)
|
|
95
|
+
- **Open ISCs:** Count of unchecked ISCs for the current project (hidden when 0)
|
|
96
|
+
- **Signal Trend:** Session quality score from `~/.pal/memory/state/signal-cache.json` with trend arrow
|
|
97
|
+
- **Context Bar:** Visual progress bar showing token usage
|
|
98
|
+
- **Context %:** Percentage of context window used
|
|
99
|
+
- **Cost:** Estimated session cost in USD (shows "free" if <$0.01 on macOS/Linux, `$0.00` on Windows)
|
|
100
|
+
- **Remaining %:** Percentage of context window remaining
|
|
101
|
+
- **Rate Limits:** 5h and 7d usage percentages (Pro/Max plans only — hidden on other plans)
|
|
102
|
+
- **Update Notice:** Shown on line 3 when `~/.pal/memory/state/update-available.json` flags a new version
|
|
103
|
+
|
|
104
|
+
## Dependencies
|
|
105
|
+
|
|
106
|
+
- **macOS/Linux:** `bash`, `jq`, `git` (for branch detection)
|
|
107
|
+
- **Windows:** PowerShell 7+, `git` (for branch detection)
|
|
108
|
+
|
|
109
|
+
## Customization
|
|
110
|
+
|
|
111
|
+
Both scripts use `jq` to parse the JSON data Claude Code sends. You can modify them to:
|
|
112
|
+
- Add or remove fields
|
|
113
|
+
- Change colors (ANSI escape codes)
|
|
114
|
+
- Adjust progress bar width
|
|
115
|
+
- Show additional data like effort level, rate limits, or PR status
|
|
116
|
+
|
|
117
|
+
Available fields are documented in the Claude Code statusline reference:
|
|
118
|
+
https://code.claude.com/docs/en/statusline#available-data
|
|
119
|
+
|
|
120
|
+
## Updates
|
|
121
|
+
|
|
122
|
+
The status line runs after each assistant message, after `/compact`, and when vim mode toggles. It does not consume API tokens.
|
|
123
|
+
|
|
124
|
+
Changes to the script appear on your next interaction with Claude Code.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# PAL Status Line - Windows PowerShell version
|
|
2
|
+
# Reads JSON from stdin (Claude Code session data) and prints a formatted status line
|
|
3
|
+
|
|
4
|
+
$data = $input | Out-String | ConvertFrom-Json
|
|
5
|
+
|
|
6
|
+
# Extract data with fallbacks (PowerShell 5.1 compatible)
|
|
7
|
+
$MODEL = if ($data.model.display_name) { $data.model.display_name } else { "Unknown" }
|
|
8
|
+
$USED_RAW = $data.context_window.used_percentage
|
|
9
|
+
$USED = if ($USED_RAW -ne $null) { [int]$USED_RAW } else { 0 }
|
|
10
|
+
$REM_RAW = $data.context_window.remaining_percentage
|
|
11
|
+
$REM = if ($REM_RAW -ne $null) { [int]$REM_RAW } else { 0 }
|
|
12
|
+
$COST_RAW = $data.cost.total_cost_usd
|
|
13
|
+
$COST = if ($COST_RAW -ne $null) { [double]$COST_RAW } else { 0 }
|
|
14
|
+
$CWD = if ($data.workspace.current_dir) { $data.workspace.current_dir } else { "~" }
|
|
15
|
+
$REPO = if ($data.workspace.repo.name) { $data.workspace.repo.name } else { "" }
|
|
16
|
+
|
|
17
|
+
# Extract git branch
|
|
18
|
+
$BRANCH = ""
|
|
19
|
+
try { $BRANCH = & git -C $CWD rev-parse --abbrev-ref HEAD 2>$null } catch { $BRANCH = "" }
|
|
20
|
+
|
|
21
|
+
# Format directory
|
|
22
|
+
$DIR_DISPLAY = if ($REPO) { $REPO } else { Split-Path -Leaf $CWD }
|
|
23
|
+
|
|
24
|
+
# Format git branch indicator
|
|
25
|
+
$GIT_INDICATOR = if ($BRANCH) { " (git: $BRANCH)" } else { "" }
|
|
26
|
+
|
|
27
|
+
# Format cost
|
|
28
|
+
$COST_STR = '$' + ([math]::Round($COST, 2)).ToString("0.00")
|
|
29
|
+
|
|
30
|
+
# PAL: Hook health - count ERROR lines in debug.log from last 24h
|
|
31
|
+
$HOOK_ERRORS = 0
|
|
32
|
+
$debugLog = Join-Path $env:USERPROFILE ".pal\memory\state\debug.log"
|
|
33
|
+
if (Test-Path $debugLog) {
|
|
34
|
+
$cutoff = (Get-Date).AddHours(-24)
|
|
35
|
+
try {
|
|
36
|
+
$HOOK_ERRORS = (Get-Content $debugLog -ErrorAction SilentlyContinue |
|
|
37
|
+
Where-Object { $_ -match '\] ERROR ' } |
|
|
38
|
+
Where-Object {
|
|
39
|
+
$m = [regex]::Match($_, '^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]')
|
|
40
|
+
if ($m.Success) { [datetime]$m.Groups[1].Value -gt $cutoff } else { $false }
|
|
41
|
+
}).Count
|
|
42
|
+
} catch { $HOOK_ERRORS = 0 }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# PAL: Open ISC count - find project matching current directory
|
|
46
|
+
$OPEN_ISCS = 0
|
|
47
|
+
$PROJECT_NAME = ""
|
|
48
|
+
$projectsDir = Join-Path $env:USERPROFILE ".pal\memory\projects"
|
|
49
|
+
if (Test-Path $projectsDir) {
|
|
50
|
+
try {
|
|
51
|
+
$CWD_NORM = $CWD.Replace('/', '\').TrimEnd('\')
|
|
52
|
+
foreach ($slug in Get-ChildItem $projectsDir -Directory | Select-Object -ExpandProperty Name) {
|
|
53
|
+
$isa = Join-Path $projectsDir "$slug\ISA.md"
|
|
54
|
+
if (-not (Test-Path $isa)) { continue }
|
|
55
|
+
$content = Get-Content $isa -Raw -ErrorAction SilentlyContinue
|
|
56
|
+
$pathMatch = [regex]::Match($content, 'path:\s*"?([^"\n\r]+)"?')
|
|
57
|
+
if ($pathMatch.Success) {
|
|
58
|
+
$projPath = $pathMatch.Groups[1].Value.Trim().Replace('/', '\').TrimEnd('\')
|
|
59
|
+
if ($projPath -eq $CWD_NORM -or $CWD_NORM.StartsWith($projPath)) {
|
|
60
|
+
$PROJECT_NAME = $slug
|
|
61
|
+
$OPEN_ISCS = ([regex]::Matches($content, '- \[ \] ISC-')).Count
|
|
62
|
+
break
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch { $OPEN_ISCS = 0 }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Rate limits (Pro/Max only - absent for other plans)
|
|
70
|
+
$FIVE_H_RAW = $data.rate_limits.five_hour.used_percentage
|
|
71
|
+
$SEVEN_D_RAW = $data.rate_limits.seven_day.used_percentage
|
|
72
|
+
$RATE_PARTS = @()
|
|
73
|
+
if ($FIVE_H_RAW -ne $null) { $RATE_PARTS += "5h: $([int]$FIVE_H_RAW)%" }
|
|
74
|
+
if ($SEVEN_D_RAW -ne $null) { $RATE_PARTS += "7d: $([int]$SEVEN_D_RAW)%" }
|
|
75
|
+
$RATE_STR = if ($RATE_PARTS.Count -gt 0) { " - " + ($RATE_PARTS -join " | ") } else { "" }
|
|
76
|
+
|
|
77
|
+
# Create context progress bar - if both are 0, data not yet available (pre-first API call)
|
|
78
|
+
$NO_DATA = ($USED_RAW -eq $null -and $REM_RAW -eq $null)
|
|
79
|
+
if ($NO_DATA) { $USED = 0; $REM = 100 }
|
|
80
|
+
$FILLED = [int]($USED / 5)
|
|
81
|
+
$EMPTY = 20 - $FILLED
|
|
82
|
+
$BAR = ("#" * $FILLED) + ("-" * $EMPTY)
|
|
83
|
+
|
|
84
|
+
# ANSI color codes (PowerShell 5.1 compatible - use concatenation to avoid array-index expansion)
|
|
85
|
+
$ESC = [char]27
|
|
86
|
+
$CYAN = $ESC + "[36m"
|
|
87
|
+
$GREEN = $ESC + "[32m"
|
|
88
|
+
$YELLOW = $ESC + "[33m"
|
|
89
|
+
$RED = $ESC + "[31m"
|
|
90
|
+
$DIM = $ESC + "[90m"
|
|
91
|
+
$RESET = $ESC + "[0m"
|
|
92
|
+
|
|
93
|
+
# Choose bar color based on context usage
|
|
94
|
+
$BAR_COLOR = if ($USED -gt 80) { $RED } elseif ($USED -gt 60) { $YELLOW } else { $GREEN }
|
|
95
|
+
|
|
96
|
+
# PAL: Signal trend (reads 10-min cache written by session intelligence)
|
|
97
|
+
$SIGNAL_STR = ""
|
|
98
|
+
$signalCache = Join-Path $env:USERPROFILE ".pal\memory\state\signal-cache.json"
|
|
99
|
+
if (Test-Path $signalCache) {
|
|
100
|
+
try {
|
|
101
|
+
$sc = Get-Content $signalCache -Raw -ErrorAction SilentlyContinue | ConvertFrom-Json
|
|
102
|
+
if ($sc.today -ne $null) {
|
|
103
|
+
$ARROW = switch ($sc.trend) {
|
|
104
|
+
"up" { "^"; break }
|
|
105
|
+
"down" { "v"; break }
|
|
106
|
+
"stable" { "-"; break }
|
|
107
|
+
default { "?"; break }
|
|
108
|
+
}
|
|
109
|
+
$SIG_TODAY = ([math]::Round([double]$sc.today, 1)).ToString("0.0")
|
|
110
|
+
$SIG_COLOR = if ($sc.trend -eq "up") { $GREEN } elseif ($sc.trend -eq "down") { $RED } else { $DIM }
|
|
111
|
+
$SIGNAL_STR = " " + $SIG_COLOR + "sig: $SIG_TODAY/10 $ARROW" + $RESET
|
|
112
|
+
}
|
|
113
|
+
} catch {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# PAL: Update available check (reads cached result, no network call)
|
|
117
|
+
# Emoji constructed at runtime to avoid PS5.1 UTF-8-without-BOM encoding issues
|
|
118
|
+
$UPDATE_LINE = ""
|
|
119
|
+
$updateCache = Join-Path $env:USERPROFILE ".pal\memory\state\update-available.json"
|
|
120
|
+
if (Test-Path $updateCache) {
|
|
121
|
+
try {
|
|
122
|
+
$uc = Get-Content $updateCache -Raw -ErrorAction SilentlyContinue | ConvertFrom-Json
|
|
123
|
+
if ($uc.available -eq $true) {
|
|
124
|
+
$versionStr = if ($uc.current -ne $uc.latest) { "$($uc.current) -> $($uc.latest)" } else { $uc.current + " (new commits)" }
|
|
125
|
+
$UPDATE_LINE = "[update] $versionStr run: pal cli update"
|
|
126
|
+
}
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Build PAL indicators
|
|
131
|
+
$HOOK_STR = if ($HOOK_ERRORS -gt 0) { " " + $RED + "! $HOOK_ERRORS hook err" + $RESET } else { "" }
|
|
132
|
+
$ISC_STR = if ($OPEN_ISCS -gt 0) { " " + $DIM + "$OPEN_ISCS open ISCs" + $RESET } else { "" }
|
|
133
|
+
|
|
134
|
+
# Line 1: Model, Directory, Git Branch, Hook Health, Open ISCs
|
|
135
|
+
Write-Host ($CYAN + "[$MODEL]" + $RESET + " folder: $DIR_DISPLAY" + $DIM + $GIT_INDICATOR + $RESET + $HOOK_STR + $ISC_STR + $SIGNAL_STR)
|
|
136
|
+
|
|
137
|
+
# Line 2: Context bar with usage percentage, cost, and rate limits
|
|
138
|
+
$CTX_STR = "$USED% - $COST_STR - ${REM}% free"
|
|
139
|
+
Write-Host ($BAR_COLOR + $BAR + $RESET + " " + $CTX_STR + $DIM + $RATE_STR + $RESET)
|
|
140
|
+
|
|
141
|
+
# Line 3: Update available (only when cache says available AND versions differ)
|
|
142
|
+
if ($UPDATE_LINE) { Write-Host ($YELLOW + $UPDATE_LINE + $RESET) }
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PAL Status Line — macOS/Linux
|
|
3
|
+
# Reads JSON from stdin (Claude Code / Cursor CLI session data) and prints a formatted status line
|
|
4
|
+
|
|
5
|
+
input=$(cat)
|
|
6
|
+
|
|
7
|
+
# Empty or invalid stdin — keep previous status line (Cursor may invoke early)
|
|
8
|
+
if [ -z "$input" ] || ! echo "$input" | jq -e . >/dev/null 2>&1; then
|
|
9
|
+
exit 0
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# Extract data with fallbacks (Cursor: cwd, worktree.name; Claude: workspace, worktree.branch)
|
|
13
|
+
MODEL=$(echo "$input" | jq -r '.model.display_name // "Unknown"')
|
|
14
|
+
USED_RAW=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
|
|
15
|
+
REM_RAW=$(echo "$input" | jq -r '.context_window.remaining_percentage // empty')
|
|
16
|
+
USED=$( [ -n "$USED_RAW" ] && echo "${USED_RAW%.*}" || echo 0 )
|
|
17
|
+
REMAINING=$( [ -n "$REM_RAW" ] && echo "${REM_RAW%.*}" || echo 0 )
|
|
18
|
+
CWD=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // "~"')
|
|
19
|
+
REPO=$(echo "$input" | jq -r '.workspace.repo.name // ""')
|
|
20
|
+
|
|
21
|
+
# No data yet (pre-first API call)
|
|
22
|
+
if [ -z "$USED_RAW" ] && [ -z "$REM_RAW" ]; then
|
|
23
|
+
USED=0
|
|
24
|
+
REMAINING=100
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Git branch — payload first, then git in cwd
|
|
28
|
+
BRANCH=$(echo "$input" | jq -r '.worktree.branch // .worktree.name // ""')
|
|
29
|
+
if [ -z "$BRANCH" ] || [ "$BRANCH" = "null" ]; then
|
|
30
|
+
BRANCH=$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Directory label
|
|
34
|
+
if [ -n "$REPO" ] && [ "$REPO" != "null" ]; then
|
|
35
|
+
DIR_DISPLAY="$REPO"
|
|
36
|
+
else
|
|
37
|
+
DIR_DISPLAY="${CWD##*/}"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if [ -n "$BRANCH" ]; then
|
|
41
|
+
GIT_INDICATOR=" (🌿 ${BRANCH})"
|
|
42
|
+
else
|
|
43
|
+
GIT_INDICATOR=""
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Session cost — Claude only; omit segment when absent (Cursor does not send cost)
|
|
47
|
+
COST_STR=""
|
|
48
|
+
if echo "$input" | jq -e '.cost.total_cost_usd != null' >/dev/null 2>&1; then
|
|
49
|
+
COST=$(echo "$input" | jq -r '.cost.total_cost_usd')
|
|
50
|
+
if (( $(echo "$COST < 0.01" | bc -l) )); then
|
|
51
|
+
COST_STR="free"
|
|
52
|
+
else
|
|
53
|
+
COST_STR=$(printf '$%.2f' "$COST")
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# PAL: Hook health — count ERROR lines in debug.log from last 24h
|
|
58
|
+
HOOK_ERRORS=0
|
|
59
|
+
DEBUG_LOG="$HOME/.pal/memory/state/debug.log"
|
|
60
|
+
if [ -f "$DEBUG_LOG" ]; then
|
|
61
|
+
# Cross-platform 24h cutoff: macOS uses date -v, GNU Linux uses date -d
|
|
62
|
+
CUTOFF=$(date -v-24H "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -d '24 hours ago' "+%Y-%m-%d %H:%M:%S" 2>/dev/null)
|
|
63
|
+
if [ -n "$CUTOFF" ]; then
|
|
64
|
+
# Log format: [YYYY-MM-DD HH:MM:SS] LEVEL ... — split on [ or ] to get $2 = timestamp
|
|
65
|
+
HOOK_ERRORS=$(grep '\] ERROR ' "$DEBUG_LOG" | awk -F'[][]]' '$2 > "'"$CUTOFF"'"' | wc -l | tr -d ' ')
|
|
66
|
+
else
|
|
67
|
+
HOOK_ERRORS=$(grep -c '\] ERROR ' "$DEBUG_LOG" 2>/dev/null || echo 0)
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# PAL: Open ISC count — find project matching current directory
|
|
72
|
+
OPEN_ISCS=0
|
|
73
|
+
PROJECTS_DIR="$HOME/.pal/memory/projects"
|
|
74
|
+
if [ -d "$PROJECTS_DIR" ]; then
|
|
75
|
+
CWD_NORM="${CWD%/}"
|
|
76
|
+
for ISA in "$PROJECTS_DIR"/*/ISA.md; do
|
|
77
|
+
[ -f "$ISA" ] || continue
|
|
78
|
+
PROJ_PATH=$(grep -m1 'path:' "$ISA" | sed 's/.*path:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs)
|
|
79
|
+
PROJ_PATH="${PROJ_PATH%/}"
|
|
80
|
+
if [ "$PROJ_PATH" = "$CWD_NORM" ] || [[ "$CWD_NORM" == "${PROJ_PATH}/"* ]]; then
|
|
81
|
+
OPEN_ISCS=$(grep -c '- \[ \] ISC-' "$ISA" 2>/dev/null || echo 0)
|
|
82
|
+
break
|
|
83
|
+
fi
|
|
84
|
+
done
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Rate limits (Pro/Max only — absent for other plans and on Cursor)
|
|
88
|
+
FIVE_H=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
|
|
89
|
+
SEVEN_D=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
|
|
90
|
+
RATE_PARTS=()
|
|
91
|
+
[ -n "$FIVE_H" ] && RATE_PARTS+=("5h: ${FIVE_H%.*}%")
|
|
92
|
+
[ -n "$SEVEN_D" ] && RATE_PARTS+=("7d: ${SEVEN_D%.*}%")
|
|
93
|
+
RATE_STR=""
|
|
94
|
+
if [ ${#RATE_PARTS[@]} -gt 0 ]; then
|
|
95
|
+
RATE_STR=" │ $(IFS=" | "; echo "${RATE_PARTS[*]}")"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# Create context progress bar (20 chars wide)
|
|
99
|
+
FILLED=$((USED / 5))
|
|
100
|
+
EMPTY=$((20 - FILLED))
|
|
101
|
+
if [ "$FILLED" -le 0 ]; then
|
|
102
|
+
BAR="░░░░░░░░░░░░░░░░░░░░"
|
|
103
|
+
elif [ "$FILLED" -ge 20 ]; then
|
|
104
|
+
BAR="████████████████████"
|
|
105
|
+
else
|
|
106
|
+
BAR=$(printf '█%.0s' $(seq 1 $FILLED))$(printf '░%.0s' $(seq 1 $EMPTY))
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# Color codes (ANSI)
|
|
110
|
+
CYAN='\033[36m'
|
|
111
|
+
GREEN='\033[32m'
|
|
112
|
+
YELLOW='\033[33m'
|
|
113
|
+
RED='\033[31m'
|
|
114
|
+
DIM='\033[90m'
|
|
115
|
+
RESET='\033[0m'
|
|
116
|
+
|
|
117
|
+
# Choose bar color based on context usage
|
|
118
|
+
if [ "$USED" -gt 80 ]; then
|
|
119
|
+
CONTEXT_COLOR=$RED
|
|
120
|
+
elif [ "$USED" -gt 60 ]; then
|
|
121
|
+
CONTEXT_COLOR=$YELLOW
|
|
122
|
+
else
|
|
123
|
+
CONTEXT_COLOR=$GREEN
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# PAL: Signal trend (reads 10-min cache written by session intelligence)
|
|
127
|
+
SIGNAL_STR=""
|
|
128
|
+
SIGNAL_CACHE="$HOME/.pal/memory/state/signal-cache.json"
|
|
129
|
+
if [ -f "$SIGNAL_CACHE" ]; then
|
|
130
|
+
SC_TODAY=$(jq -r '.today // empty' "$SIGNAL_CACHE" 2>/dev/null)
|
|
131
|
+
SC_TREND=$(jq -r '.trend // empty' "$SIGNAL_CACHE" 2>/dev/null)
|
|
132
|
+
if [ -n "$SC_TODAY" ]; then
|
|
133
|
+
case "$SC_TREND" in
|
|
134
|
+
up) ARROW="↑"; SIG_COLOR=$GREEN ;;
|
|
135
|
+
down) ARROW="↓"; SIG_COLOR=$RED ;;
|
|
136
|
+
stable) ARROW="→"; SIG_COLOR=$DIM ;;
|
|
137
|
+
*) ARROW="?"; SIG_COLOR=$DIM ;;
|
|
138
|
+
esac
|
|
139
|
+
SIG_TODAY=$(printf "%.1f" "$SC_TODAY")
|
|
140
|
+
SIGNAL_STR=" ${SIG_COLOR}sig: ${SIG_TODAY}/10 ${ARROW}${RESET}"
|
|
141
|
+
fi
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
# PAL: Update available check (reads cached result, no network call)
|
|
145
|
+
UPDATE_LINE=""
|
|
146
|
+
UPDATE_CACHE="$HOME/.pal/memory/state/update-available.json"
|
|
147
|
+
if [ -f "$UPDATE_CACHE" ]; then
|
|
148
|
+
UC_AVAIL=$(jq -r '.available // false' "$UPDATE_CACHE" 2>/dev/null)
|
|
149
|
+
if [ "$UC_AVAIL" = "true" ]; then
|
|
150
|
+
UC_CURRENT=$(jq -r '.current // ""' "$UPDATE_CACHE" 2>/dev/null)
|
|
151
|
+
UC_LATEST=$(jq -r '.latest // ""' "$UPDATE_CACHE" 2>/dev/null)
|
|
152
|
+
if [ "$UC_CURRENT" != "$UC_LATEST" ]; then
|
|
153
|
+
VERSION_STR="$UC_CURRENT → $UC_LATEST"
|
|
154
|
+
else
|
|
155
|
+
VERSION_STR="$UC_CURRENT (new commits)"
|
|
156
|
+
fi
|
|
157
|
+
UPDATE_LINE="📦 update: $VERSION_STR run: pal cli update"
|
|
158
|
+
fi
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
# Build PAL indicators
|
|
162
|
+
HOOK_STR=""
|
|
163
|
+
[ "$HOOK_ERRORS" -gt 0 ] && HOOK_STR=" ${RED}⚠️ ${HOOK_ERRORS} hook err${RESET}"
|
|
164
|
+
ISC_STR=""
|
|
165
|
+
[ "$OPEN_ISCS" -gt 0 ] && ISC_STR=" ${DIM}📋 ${OPEN_ISCS} open ISCs${RESET}"
|
|
166
|
+
|
|
167
|
+
# Line 1: Model, Directory, Git Branch, Hook Health, Open ISCs, Signal
|
|
168
|
+
echo -e "${CYAN}[${MODEL}]${RESET} 📁 ${DIR_DISPLAY}${DIM}${GIT_INDICATOR}${RESET}${HOOK_STR}${ISC_STR}${SIGNAL_STR}"
|
|
169
|
+
|
|
170
|
+
# Line 2: Context bar; cost segment only when Claude sends it
|
|
171
|
+
if [ -n "$COST_STR" ]; then
|
|
172
|
+
echo -e "${CONTEXT_COLOR}${BAR}${RESET} ${USED}% │ ${COST_STR} │ ${REMAINING}% free${DIM}${RATE_STR}${RESET}"
|
|
173
|
+
else
|
|
174
|
+
echo -e "${CONTEXT_COLOR}${BAR}${RESET} ${USED}% │ ${REMAINING}% free${DIM}${RATE_STR}${RESET}"
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# Line 3: Update available (only when cache says available AND versions differ)
|
|
178
|
+
[ -n "$UPDATE_LINE" ] && echo -e "${YELLOW}${UPDATE_LINE}${RESET}"
|
|
179
|
+
exit 0
|
|
@@ -25,6 +25,27 @@
|
|
|
25
25
|
"Bash(node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts *)"
|
|
26
26
|
]
|
|
27
27
|
},
|
|
28
|
+
"skillOverrides": {
|
|
29
|
+
"claude-api": "off",
|
|
30
|
+
"update-config": "off",
|
|
31
|
+
"deep-research": "off",
|
|
32
|
+
"code-review": "off"
|
|
33
|
+
},
|
|
34
|
+
"attribution": {
|
|
35
|
+
"commit": "",
|
|
36
|
+
"pr": ""
|
|
37
|
+
},
|
|
38
|
+
"showClearContextOnPlanAccept": true,
|
|
39
|
+
"respectGitignore": true,
|
|
40
|
+
"spinnerTipsEnabled": true,
|
|
41
|
+
"spinnerTipsOverride": {
|
|
42
|
+
"tips": [
|
|
43
|
+
"Need something reusable? Use the /create-skill command.",
|
|
44
|
+
"PAL learns from your feedback and improves over time. Don't be shy to give feedback!",
|
|
45
|
+
"For simple PDFs use the /create-pdf skill. Need a styled one? Use /consulting-report instead.",
|
|
46
|
+
"Use the /research skill to run comprehensive web research with multiple parallel agents."
|
|
47
|
+
]
|
|
48
|
+
},
|
|
28
49
|
"hooks": {
|
|
29
50
|
"SessionStart": [
|
|
30
51
|
{
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Update checker — detects if a newer version of PAL is available.
|
|
3
3
|
*
|
|
4
|
-
* Repo mode (.git exists next to package): git fetch + compare HEAD vs origin/main
|
|
5
4
|
* Package mode: fetch npm registry for latest version vs installed
|
|
5
|
+
* Repo mode: opt in with PAL_UPDATE_MODE=repo to compare HEAD vs origin/main
|
|
6
6
|
*
|
|
7
7
|
* Caches result in state/update-available.json. Checked at most once per hour.
|
|
8
8
|
*/
|
|
@@ -47,7 +47,7 @@ function writeCache(cache: UpdateCache): void {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function isRepoMode(): boolean {
|
|
50
|
-
return existsSync(resolve(palPkg(), ".git"));
|
|
50
|
+
return process.env.PAL_UPDATE_MODE === "repo" && existsSync(resolve(palPkg(), ".git"));
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function getInstalledVersion(): string {
|
package/src/hooks/lib/paths.ts
CHANGED
|
@@ -85,6 +85,8 @@ export const assets = {
|
|
|
85
85
|
copilotHooksTemplate: () => pkg("assets", "templates", "hooks.copilot.json"),
|
|
86
86
|
codexHooksTemplate: () => pkg("assets", "templates", "hooks.codex.json"),
|
|
87
87
|
codexRulesTemplate: () => pkg("assets", "templates", "rules.codex.rules"),
|
|
88
|
+
statuslineScriptBash: () => pkg("assets", "statusline.sh"),
|
|
89
|
+
statuslineScriptPs1: () => pkg("assets", "statusline.ps1"),
|
|
88
90
|
agentTools: () => pkg("src", "tools", "agent"),
|
|
89
91
|
tools: () => pkg("src", "tools"),
|
|
90
92
|
palDocs: () => pkg("assets", "templates", "PAL"),
|
|
@@ -9,9 +9,11 @@ import { resolve } from "node:path";
|
|
|
9
9
|
import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
|
|
10
10
|
import { assets, palHome, palPkg, platform } from "../../hooks/lib/paths";
|
|
11
11
|
import {
|
|
12
|
+
addStatuslineConfig,
|
|
12
13
|
copyAgents,
|
|
13
14
|
copyPalDocs,
|
|
14
15
|
copySkills,
|
|
16
|
+
copyStatusline,
|
|
15
17
|
countAgents,
|
|
16
18
|
countMd,
|
|
17
19
|
countSkills,
|
|
@@ -43,7 +45,10 @@ log.info("Backed up settings.json");
|
|
|
43
45
|
// --- Load template and merge into existing settings ---
|
|
44
46
|
const template = loadSettingsTemplate(assets.claudeSettingsTemplate(), PKG_ROOT);
|
|
45
47
|
const existing = readJson<Record<string, unknown>>(SETTINGS, {});
|
|
46
|
-
|
|
48
|
+
let merged = mergeSettings(existing, template);
|
|
49
|
+
|
|
50
|
+
// Add platform-specific statusLine config
|
|
51
|
+
merged = addStatuslineConfig(merged);
|
|
47
52
|
|
|
48
53
|
writeJson(SETTINGS, merged);
|
|
49
54
|
log.success("Merged PAL settings into settings.json");
|
|
@@ -56,6 +61,9 @@ generateSkillIndex();
|
|
|
56
61
|
// --- Copy agents ---
|
|
57
62
|
copyAgents();
|
|
58
63
|
|
|
64
|
+
// --- Copy statusline script ---
|
|
65
|
+
copyStatusline();
|
|
66
|
+
|
|
59
67
|
// --- Copy PAL system docs ---
|
|
60
68
|
const palDocsCount = copyPalDocs();
|
|
61
69
|
log.success(`Installed ${palDocsCount} PAL docs to ~/.pal/docs/`);
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
removeAgents,
|
|
14
14
|
removePalDocs,
|
|
15
15
|
removeSkills,
|
|
16
|
+
removeStatusline,
|
|
17
|
+
removeStatuslineConfig,
|
|
16
18
|
unmergeSettings,
|
|
17
19
|
writeJson,
|
|
18
20
|
} from "../lib";
|
|
@@ -33,7 +35,10 @@ log.info("Backed up settings.json");
|
|
|
33
35
|
// --- Load template and unmerge from existing settings ---
|
|
34
36
|
const template = loadSettingsTemplate(assets.claudeSettingsTemplate(), PKG_ROOT);
|
|
35
37
|
const existing = readJson<Record<string, unknown>>(SETTINGS, {});
|
|
36
|
-
|
|
38
|
+
let cleaned = unmergeSettings(existing, template);
|
|
39
|
+
|
|
40
|
+
// Remove statusLine config
|
|
41
|
+
cleaned = removeStatuslineConfig(cleaned);
|
|
37
42
|
|
|
38
43
|
writeJson(SETTINGS, cleaned);
|
|
39
44
|
log.success("Removed PAL settings from settings.json");
|
|
@@ -54,6 +59,9 @@ if (removedAgents.length > 0) {
|
|
|
54
59
|
log.info("No PAL agents found");
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
// --- Remove statusline script ---
|
|
63
|
+
removeStatusline();
|
|
64
|
+
|
|
57
65
|
// --- Remove PAL system docs ---
|
|
58
66
|
removePalDocs();
|
|
59
67
|
|
|
@@ -15,6 +15,7 @@ import { resolve } from "node:path";
|
|
|
15
15
|
import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
|
|
16
16
|
import { assets, palPkg, platform } from "../../hooks/lib/paths";
|
|
17
17
|
import {
|
|
18
|
+
addCodexStatuslineConfig,
|
|
18
19
|
copySkills,
|
|
19
20
|
countSkills,
|
|
20
21
|
generateSkillIndex,
|
|
@@ -55,6 +56,17 @@ function enableCodexHooks(configPath: string): void {
|
|
|
55
56
|
log.success("Enabled hooks = true in ~/.codex/config.toml");
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
function enableCodexStatusline(configPath: string): void {
|
|
60
|
+
const content = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "";
|
|
61
|
+
const updated = addCodexStatuslineConfig(content);
|
|
62
|
+
if (updated === content) {
|
|
63
|
+
log.info("Codex status line already configured in config.toml");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
writeFileSync(configPath, updated, "utf-8");
|
|
67
|
+
log.success("Configured Codex TUI status line in ~/.codex/config.toml");
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
const PKG_ROOT = palPkg().replaceAll("\\", "/");
|
|
59
71
|
const CODEX_DIR = platform.codexDir();
|
|
60
72
|
const HOOKS_FILE = resolve(CODEX_DIR, "hooks.json");
|
|
@@ -102,6 +114,7 @@ log.success("Ensured AGENTS.md symlink at ~/.codex/AGENTS.md");
|
|
|
102
114
|
// --- Enable hooks in config.toml ---
|
|
103
115
|
const CONFIG_FILE = resolve(CODEX_DIR, "config.toml");
|
|
104
116
|
enableCodexHooks(CONFIG_FILE);
|
|
117
|
+
enableCodexStatusline(CONFIG_FILE);
|
|
105
118
|
|
|
106
119
|
log.success("Codex installation complete");
|
|
107
120
|
console.log("");
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
loadCodexHooksTemplate,
|
|
12
12
|
log,
|
|
13
13
|
readJson,
|
|
14
|
+
removeCodexStatuslineConfig,
|
|
14
15
|
removeSkills,
|
|
15
16
|
unmergeCodexHooks,
|
|
16
17
|
unmergeCodexRules,
|
|
@@ -36,6 +37,15 @@ function disableCodexHooks(configPath: string): void {
|
|
|
36
37
|
log.success("Removed hooks from ~/.codex/config.toml");
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
function disableCodexStatusline(configPath: string): void {
|
|
41
|
+
if (!existsSync(configPath)) return;
|
|
42
|
+
const content = readFileSync(configPath, "utf-8");
|
|
43
|
+
const updated = removeCodexStatuslineConfig(content);
|
|
44
|
+
if (updated === content) return;
|
|
45
|
+
writeFileSync(configPath, updated, "utf-8");
|
|
46
|
+
log.success("Removed PAL Codex status line from ~/.codex/config.toml");
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
const PKG_ROOT = palPkg().replaceAll("\\", "/");
|
|
40
50
|
const CODEX_DIR = platform.codexDir();
|
|
41
51
|
const HOOKS_FILE = resolve(CODEX_DIR, "hooks.json");
|
|
@@ -80,5 +90,6 @@ if (removed.length > 0) {
|
|
|
80
90
|
// --- Disable hooks in config.toml ---
|
|
81
91
|
const CONFIG_FILE = resolve(CODEX_DIR, "config.toml");
|
|
82
92
|
disableCodexHooks(CONFIG_FILE);
|
|
93
|
+
disableCodexStatusline(CONFIG_FILE);
|
|
83
94
|
|
|
84
95
|
log.success("Codex uninstall complete");
|
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
* Symlinks skills. Generates AGENTS.md.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
9
|
import { writeContextDigests } from "../../hooks/handlers/context-digests";
|
|
10
10
|
import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
|
|
11
11
|
import { assets, palPkg, platform } from "../../hooks/lib/paths";
|
|
12
12
|
import {
|
|
13
|
+
addStatuslineConfig,
|
|
13
14
|
copyAgentsForCursor,
|
|
14
15
|
copyPalDocs,
|
|
15
16
|
copySkills,
|
|
17
|
+
copyStatusline,
|
|
16
18
|
countSkills,
|
|
17
19
|
generateSkillIndex,
|
|
18
20
|
loadCursorHooksTemplate,
|
|
@@ -26,6 +28,7 @@ import {
|
|
|
26
28
|
const PKG_ROOT = palPkg().replaceAll("\\", "/");
|
|
27
29
|
const CURSOR_DIR = platform.cursorDir();
|
|
28
30
|
const HOOKS_FILE = resolve(CURSOR_DIR, "hooks.json");
|
|
31
|
+
const CLI_CONFIG = resolve(CURSOR_DIR, "cli-config.json");
|
|
29
32
|
|
|
30
33
|
// --- Ensure ~/.cursor/ exists ---
|
|
31
34
|
mkdirSync(CURSOR_DIR, { recursive: true });
|
|
@@ -61,6 +64,19 @@ log.success(`Installed ${palDocsCount} PAL docs to ~/.pal/docs/`);
|
|
|
61
64
|
// --- Scaffold PAL settings ---
|
|
62
65
|
scaffoldPalSettings();
|
|
63
66
|
|
|
67
|
+
// --- Statusline script + cli-config.json statusLine ---
|
|
68
|
+
copyStatusline("cursor");
|
|
69
|
+
if (!existsSync(CLI_CONFIG)) {
|
|
70
|
+
writeFileSync(CLI_CONFIG, "{}\n", "utf-8");
|
|
71
|
+
log.info("Created new cli-config.json");
|
|
72
|
+
} else {
|
|
73
|
+
copyFileSync(CLI_CONFIG, `${CLI_CONFIG}.bak.${Date.now()}`);
|
|
74
|
+
log.info("Backed up cli-config.json");
|
|
75
|
+
}
|
|
76
|
+
const cliConfig = readJson<Record<string, unknown>>(CLI_CONFIG, {});
|
|
77
|
+
writeJson(CLI_CONFIG, addStatuslineConfig(cliConfig, "cursor"));
|
|
78
|
+
log.success("Merged statusLine into cli-config.json");
|
|
79
|
+
|
|
64
80
|
// --- Generate AGENTS.md ---
|
|
65
81
|
regenerateIfNeeded();
|
|
66
82
|
log.success("Generated AGENTS.md");
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
removeAgentsFromCursor,
|
|
16
16
|
removePalDocs,
|
|
17
17
|
removeSkills,
|
|
18
|
+
removeStatusline,
|
|
19
|
+
removeStatuslineConfig,
|
|
18
20
|
unmergeCursorHooks,
|
|
19
21
|
writeJson,
|
|
20
22
|
} from "../lib";
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
const PKG_ROOT = palPkg().replaceAll("\\", "/");
|
|
23
25
|
const CURSOR_DIR = platform.cursorDir();
|
|
24
26
|
const HOOKS_FILE = resolve(CURSOR_DIR, "hooks.json");
|
|
27
|
+
const CLI_CONFIG = resolve(CURSOR_DIR, "cli-config.json");
|
|
25
28
|
|
|
26
29
|
// --- Remove PAL hooks from hooks.json ---
|
|
27
30
|
if (existsSync(HOOKS_FILE)) {
|
|
@@ -57,6 +60,18 @@ if (removedAgents.length > 0) {
|
|
|
57
60
|
// --- Remove PAL system docs ---
|
|
58
61
|
removePalDocs();
|
|
59
62
|
|
|
63
|
+
// --- Remove statusline script and cli-config statusLine ---
|
|
64
|
+
if (existsSync(CLI_CONFIG)) {
|
|
65
|
+
copyFileSync(CLI_CONFIG, `${CLI_CONFIG}.bak.${Date.now()}`);
|
|
66
|
+
log.info("Backed up cli-config.json");
|
|
67
|
+
const cliConfig = readJson<Record<string, unknown>>(CLI_CONFIG, {});
|
|
68
|
+
writeJson(CLI_CONFIG, removeStatuslineConfig(cliConfig, "cursor"));
|
|
69
|
+
log.success("Removed PAL statusLine from cli-config.json");
|
|
70
|
+
} else {
|
|
71
|
+
log.info("No cli-config.json found, skipping statusLine removal");
|
|
72
|
+
}
|
|
73
|
+
removeStatusline("cursor");
|
|
74
|
+
|
|
60
75
|
// --- Remove ~/.cursor/rules/pal-*.mdc ---
|
|
61
76
|
for (const src of getSemiStaticSources()) {
|
|
62
77
|
try {
|
package/src/targets/lib.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
chmodSync,
|
|
6
7
|
copyFileSync,
|
|
7
8
|
existsSync,
|
|
8
9
|
lstatSync,
|
|
@@ -123,6 +124,65 @@ export function mergeSettings(existing: Settings, template: Settings): Settings
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
// Merge skillOverrides (object with skill name keys, add if not present)
|
|
128
|
+
if (template.skillOverrides && typeof template.skillOverrides === "object") {
|
|
129
|
+
result.skillOverrides ??= {};
|
|
130
|
+
const resultOverrides = result.skillOverrides as Record<string, unknown>;
|
|
131
|
+
for (const [skill, value] of Object.entries(template.skillOverrides)) {
|
|
132
|
+
if (!(skill in resultOverrides)) {
|
|
133
|
+
resultOverrides[skill] = value;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Merge attribution (object with commit/pr keys, add if not present)
|
|
139
|
+
if (template.attribution && typeof template.attribution === "object") {
|
|
140
|
+
if (!("attribution" in result)) {
|
|
141
|
+
result.attribution = template.attribution;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Merge showClearContextOnPlanAccept (boolean, add if not present)
|
|
146
|
+
if ("showClearContextOnPlanAccept" in template) {
|
|
147
|
+
if (!("showClearContextOnPlanAccept" in result)) {
|
|
148
|
+
result.showClearContextOnPlanAccept = template.showClearContextOnPlanAccept;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Merge respectGitignore (boolean, add if not present)
|
|
153
|
+
if ("respectGitignore" in template) {
|
|
154
|
+
if (!("respectGitignore" in result)) {
|
|
155
|
+
result.respectGitignore = template.respectGitignore;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Merge spinnerTipsEnabled (boolean, add if not present)
|
|
160
|
+
if ("spinnerTipsEnabled" in template) {
|
|
161
|
+
if (!("spinnerTipsEnabled" in result)) {
|
|
162
|
+
result.spinnerTipsEnabled = template.spinnerTipsEnabled;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Merge spinnerTipsOverride (merge tips array, deduplicate by content)
|
|
167
|
+
if (
|
|
168
|
+
template.spinnerTipsOverride &&
|
|
169
|
+
typeof template.spinnerTipsOverride === "object" &&
|
|
170
|
+
"tips" in template.spinnerTipsOverride &&
|
|
171
|
+
Array.isArray((template.spinnerTipsOverride as Record<string, unknown>).tips)
|
|
172
|
+
) {
|
|
173
|
+
result.spinnerTipsOverride ??= { tips: [] };
|
|
174
|
+
const resultOverride = result.spinnerTipsOverride as Record<string, unknown>;
|
|
175
|
+
resultOverride.tips ??= [];
|
|
176
|
+
const tips = resultOverride.tips as string[];
|
|
177
|
+
const templateTips = (template.spinnerTipsOverride as Record<string, unknown>)
|
|
178
|
+
.tips as string[];
|
|
179
|
+
for (const tip of templateTips) {
|
|
180
|
+
if (typeof tip === "string" && !tips.includes(tip)) {
|
|
181
|
+
tips.push(tip);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
126
186
|
return result;
|
|
127
187
|
}
|
|
128
188
|
|
|
@@ -163,6 +223,68 @@ export function unmergeSettings(existing: Settings, template: Settings): Setting
|
|
|
163
223
|
if (Object.keys(result.permissions).length === 0) delete result.permissions;
|
|
164
224
|
}
|
|
165
225
|
|
|
226
|
+
// Remove PAL skillOverrides (remove keys that appear in template)
|
|
227
|
+
if (
|
|
228
|
+
template.skillOverrides &&
|
|
229
|
+
typeof template.skillOverrides === "object" &&
|
|
230
|
+
result.skillOverrides &&
|
|
231
|
+
typeof result.skillOverrides === "object"
|
|
232
|
+
) {
|
|
233
|
+
const palSkills = new Set(Object.keys(template.skillOverrides));
|
|
234
|
+
for (const skill of palSkills) {
|
|
235
|
+
delete (result.skillOverrides as Record<string, unknown>)[skill];
|
|
236
|
+
}
|
|
237
|
+
if (Object.keys(result.skillOverrides).length === 0) delete result.skillOverrides;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Remove PAL attribution if it matches template structure
|
|
241
|
+
if (template.attribution && "attribution" in result) {
|
|
242
|
+
delete result.attribution;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Remove PAL showClearContextOnPlanAccept
|
|
246
|
+
if (
|
|
247
|
+
"showClearContextOnPlanAccept" in template &&
|
|
248
|
+
"showClearContextOnPlanAccept" in result
|
|
249
|
+
) {
|
|
250
|
+
delete result.showClearContextOnPlanAccept;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Remove PAL respectGitignore
|
|
254
|
+
if ("respectGitignore" in template && "respectGitignore" in result) {
|
|
255
|
+
delete result.respectGitignore;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Remove PAL spinnerTipsEnabled
|
|
259
|
+
if ("spinnerTipsEnabled" in template && "spinnerTipsEnabled" in result) {
|
|
260
|
+
delete result.spinnerTipsEnabled;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Remove PAL spinnerTipsOverride (remove only template tips, preserve user tips)
|
|
264
|
+
if (
|
|
265
|
+
template.spinnerTipsOverride &&
|
|
266
|
+
typeof template.spinnerTipsOverride === "object" &&
|
|
267
|
+
"tips" in template.spinnerTipsOverride &&
|
|
268
|
+
Array.isArray((template.spinnerTipsOverride as Record<string, unknown>).tips) &&
|
|
269
|
+
result.spinnerTipsOverride &&
|
|
270
|
+
typeof result.spinnerTipsOverride === "object" &&
|
|
271
|
+
"tips" in result.spinnerTipsOverride &&
|
|
272
|
+
Array.isArray((result.spinnerTipsOverride as Record<string, unknown>).tips)
|
|
273
|
+
) {
|
|
274
|
+
const templateTipsArray = (template.spinnerTipsOverride as Record<string, unknown>)
|
|
275
|
+
.tips as string[];
|
|
276
|
+
const templateTips = new Set(templateTipsArray);
|
|
277
|
+
const resultOverride = result.spinnerTipsOverride as Record<string, unknown>;
|
|
278
|
+
const tips = resultOverride.tips as string[];
|
|
279
|
+
resultOverride.tips = tips.filter((tip) => !templateTips.has(tip));
|
|
280
|
+
if (
|
|
281
|
+
(resultOverride.tips as string[]).length === 0 &&
|
|
282
|
+
Object.keys(resultOverride).length === 1
|
|
283
|
+
) {
|
|
284
|
+
delete result.spinnerTipsOverride;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
166
288
|
return result;
|
|
167
289
|
}
|
|
168
290
|
|
|
@@ -363,6 +485,69 @@ export function unmergeCodexRules(existing: string): string {
|
|
|
363
485
|
return cleaned ? `${cleaned}\n` : "";
|
|
364
486
|
}
|
|
365
487
|
|
|
488
|
+
// --- Codex TUI status line ---
|
|
489
|
+
|
|
490
|
+
const PAL_CODEX_STATUS_LINE = [
|
|
491
|
+
"model-with-reasoning",
|
|
492
|
+
"context-remaining",
|
|
493
|
+
"context-used",
|
|
494
|
+
"five-hour-limit",
|
|
495
|
+
"weekly-limit",
|
|
496
|
+
"git-branch",
|
|
497
|
+
"used-tokens",
|
|
498
|
+
"thread-id",
|
|
499
|
+
"current-dir",
|
|
500
|
+
"codex-version",
|
|
501
|
+
] as const;
|
|
502
|
+
|
|
503
|
+
function codexStatusLineToml(): string {
|
|
504
|
+
const quotedItems = PAL_CODEX_STATUS_LINE.map((item) => JSON.stringify(item)).join(
|
|
505
|
+
", "
|
|
506
|
+
);
|
|
507
|
+
return `tui.status_line = [${quotedItems}]\ntui.status_line_use_colors = true\n`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function hasCodexStatusLine(content: string): boolean {
|
|
511
|
+
return /^[ \t]*(?:tui\.)?status_line[ \t]*=/m.test(content);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Add Codex TUI status-line defaults unless the user already configured one. */
|
|
515
|
+
export function addCodexStatuslineConfig(content: string): string {
|
|
516
|
+
if (hasCodexStatusLine(content)) return content;
|
|
517
|
+
|
|
518
|
+
const block = codexStatusLineToml();
|
|
519
|
+
if (content.trim() === "") return block;
|
|
520
|
+
|
|
521
|
+
const firstTable = content.search(/^[ \t]*\[/m);
|
|
522
|
+
if (firstTable === -1) {
|
|
523
|
+
return `${content}${content.endsWith("\n") ? "" : "\n"}${block}`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const before = content.slice(0, firstTable);
|
|
527
|
+
const after = content.slice(firstTable);
|
|
528
|
+
return `${before}${before.endsWith("\n") || before === "" ? "" : "\n"}${block}\n${after}`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Remove PAL's default Codex status-line config if it still matches exactly. */
|
|
532
|
+
export function removeCodexStatuslineConfig(content: string): string {
|
|
533
|
+
const lines = content.split("\n");
|
|
534
|
+
const statusLine = codexStatusLineToml().trim().split("\n")[0];
|
|
535
|
+
const colors = codexStatusLineToml().trim().split("\n")[1];
|
|
536
|
+
const filtered: string[] = [];
|
|
537
|
+
|
|
538
|
+
for (let i = 0; i < lines.length; i++) {
|
|
539
|
+
const line = lines[i]?.trim();
|
|
540
|
+
const next = lines[i + 1]?.trim();
|
|
541
|
+
if (line === statusLine && next === colors) {
|
|
542
|
+
i++;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
filtered.push(lines[i] ?? "");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return filtered.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
549
|
+
}
|
|
550
|
+
|
|
366
551
|
// --- TELOS scaffolding ---
|
|
367
552
|
|
|
368
553
|
/** Copy template files into telos/ without overwriting existing ones */
|
|
@@ -768,6 +953,138 @@ export function loadCopilotHooksTemplate(templatePath: string, pkgRoot: string):
|
|
|
768
953
|
}
|
|
769
954
|
}
|
|
770
955
|
|
|
956
|
+
// --- Statusline ---
|
|
957
|
+
|
|
958
|
+
export type StatuslineTarget = "claude" | "cursor";
|
|
959
|
+
|
|
960
|
+
function statuslineAgentDir(target: StatuslineTarget): string {
|
|
961
|
+
return target === "claude" ? platform.claudeDir() : platform.cursorDir();
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function statuslineCommand(target: StatuslineTarget): string {
|
|
965
|
+
const isPlatformWin32 = process.platform === "win32";
|
|
966
|
+
if (target === "cursor") {
|
|
967
|
+
return isPlatformWin32
|
|
968
|
+
? "powershell -NoProfile -File ~/.cursor/statusline.ps1"
|
|
969
|
+
: "~/.cursor/statusline.sh";
|
|
970
|
+
}
|
|
971
|
+
return isPlatformWin32
|
|
972
|
+
? "powershell -NoProfile -File ~/.claude/statusline.ps1"
|
|
973
|
+
: "~/.claude/statusline.sh";
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function isPalStatuslineCommand(cmd: string, target: StatuslineTarget): boolean {
|
|
977
|
+
return target === "claude"
|
|
978
|
+
? cmd.includes(".claude/statusline")
|
|
979
|
+
: cmd.includes(".cursor/statusline");
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/** Copy statusline script to ~/.claude/ or ~/.cursor/ for the current platform */
|
|
983
|
+
export function copyStatusline(target: StatuslineTarget = "claude"): boolean {
|
|
984
|
+
const agentDir = statuslineAgentDir(target);
|
|
985
|
+
mkdirSync(agentDir, { recursive: true });
|
|
986
|
+
|
|
987
|
+
const isPlatformWin32 = process.platform === "win32";
|
|
988
|
+
const sourcePath = isPlatformWin32
|
|
989
|
+
? assets.statuslineScriptPs1()
|
|
990
|
+
: assets.statuslineScriptBash();
|
|
991
|
+
const scriptName = isPlatformWin32 ? "statusline.ps1" : "statusline.sh";
|
|
992
|
+
const destPath = resolve(agentDir, scriptName);
|
|
993
|
+
|
|
994
|
+
if (!existsSync(sourcePath)) {
|
|
995
|
+
log.warn(`Statusline script not found at ${sourcePath}`);
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
try {
|
|
1000
|
+
copyFileSync(sourcePath, destPath);
|
|
1001
|
+
|
|
1002
|
+
if (process.platform !== "win32") {
|
|
1003
|
+
chmodSync(destPath, 0o755);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
log.success(`Statusline installed to ${destPath}`);
|
|
1007
|
+
return true;
|
|
1008
|
+
} catch (e) {
|
|
1009
|
+
log.warn(`Failed to copy statusline script: ${e}`);
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/** Remove statusline script from ~/.claude/ or ~/.cursor/ */
|
|
1015
|
+
export function removeStatusline(target: StatuslineTarget = "claude"): boolean {
|
|
1016
|
+
const isPlatformWin32 = process.platform === "win32";
|
|
1017
|
+
const scriptName = isPlatformWin32 ? "statusline.ps1" : "statusline.sh";
|
|
1018
|
+
const scriptPath = resolve(statuslineAgentDir(target), scriptName);
|
|
1019
|
+
|
|
1020
|
+
if (!existsSync(scriptPath)) {
|
|
1021
|
+
log.info("Statusline script not found, nothing to remove");
|
|
1022
|
+
return true;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
try {
|
|
1026
|
+
unlinkSync(scriptPath);
|
|
1027
|
+
log.success(`Removed statusline script from ${scriptPath}`);
|
|
1028
|
+
return true;
|
|
1029
|
+
} catch (e) {
|
|
1030
|
+
log.warn(`Failed to remove statusline script: ${e}`);
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/** Add statusLine config if not already present or if using an old broken command */
|
|
1036
|
+
export function addStatuslineConfig(
|
|
1037
|
+
settings: Record<string, unknown>,
|
|
1038
|
+
target: StatuslineTarget = "claude"
|
|
1039
|
+
): Record<string, unknown> {
|
|
1040
|
+
const statusLine = settings.statusLine as Record<string, unknown> | undefined;
|
|
1041
|
+
|
|
1042
|
+
if (statusLine && typeof statusLine === "object" && statusLine.command) {
|
|
1043
|
+
const cmd = statusLine.command as string;
|
|
1044
|
+
if (target === "claude") {
|
|
1045
|
+
// Keep existing config unless it's the old broken Get-Content pattern
|
|
1046
|
+
if (!cmd.includes("Get-Content -Raw")) {
|
|
1047
|
+
return settings;
|
|
1048
|
+
}
|
|
1049
|
+
} else if (!isPalStatuslineCommand(cmd, "cursor")) {
|
|
1050
|
+
// Cursor: preserve user-defined statusLine commands
|
|
1051
|
+
return settings;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const command = statuslineCommand(target);
|
|
1056
|
+
settings.statusLine = {
|
|
1057
|
+
type: "command",
|
|
1058
|
+
command,
|
|
1059
|
+
padding: 2,
|
|
1060
|
+
...(target === "cursor" ? { updateIntervalMs: 300, timeoutMs: 2000 } : {}),
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
return settings;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/** Remove statusLine config from settings (PAL-owned only for Cursor) */
|
|
1067
|
+
export function removeStatuslineConfig(
|
|
1068
|
+
settings: Record<string, unknown>,
|
|
1069
|
+
target: StatuslineTarget = "claude"
|
|
1070
|
+
): Record<string, unknown> {
|
|
1071
|
+
const statusLine = settings.statusLine as Record<string, unknown> | undefined;
|
|
1072
|
+
if (!statusLine || typeof statusLine !== "object") {
|
|
1073
|
+
return settings;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if (target === "claude") {
|
|
1077
|
+
delete settings.statusLine;
|
|
1078
|
+
return settings;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const cmd = statusLine.command as string | undefined;
|
|
1082
|
+
if (cmd && isPalStatuslineCommand(cmd, "cursor")) {
|
|
1083
|
+
delete settings.statusLine;
|
|
1084
|
+
}
|
|
1085
|
+
return settings;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
771
1088
|
// --- Skill Index ---
|
|
772
1089
|
|
|
773
1090
|
interface SkillIndexEntry {
|