@kurokeita/add-skill 1.21.0 → 2.0.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.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: statusline-setup
3
- description: 'Set up the Claude Code statusline with cwd, git branch, model, context %, session token totals, and 5h/7d quota usage with ETA. Use when asked to "set up my statusline", "install statusline", "configure Claude Code statusline", or to add a colored status line showing rate limits and token usage. Detects OS and installs the appropriate variant (bash + jq on Linux/macOS, PowerShell on Windows).'
3
+ description: 'Set up the Claude Code or Gemini/Antigravity CLI statusline with cwd, git branch, model, context %, session token totals, and 5h/7d quota usage with ETA. Use when asked to "set up my statusline", "install statusline", "configure statusline", or to add a colored status line showing rate limits and token usage. Detects OS and installs the appropriate variant (bash + jq on Linux/macOS, PowerShell on Windows).'
4
4
  ---
5
5
 
6
6
  # Statusline Setup
7
7
 
8
- Install a Claude Code statusline that renders, left to right, pipe-separated:
8
+ Install a Claude Code or Gemini/Antigravity statusline that renders, left to right, pipe-separated:
9
9
 
10
10
  1. **cwd** — 24-bit ANSI `#5EFFFF` (`38;2;94;255;255`), with `$HOME` / `%USERPROFILE%` shortened to `~`.
11
11
  2. **git branch** in parentheses — 24-bit ANSI `#C24870` (`38;2;194;72;112`), only when inside a repo.
@@ -18,18 +18,25 @@ Segments are joined with ANSI `0;37` pipes (` | `).
18
18
 
19
19
  ## When to Use This Skill
20
20
 
21
- - "Set up my Claude Code statusline"
21
+ - "Set up my Claude Code statusline" or "Set up my Antigravity statusline"
22
22
  - "Install the statusline"
23
23
  - "Configure my statusline with token counts and quota"
24
24
  - Any request to add a colored status line showing context %, rate limits, or session tokens.
25
25
 
26
- ## OS Detection
26
+ ## OS & Platform Detection
27
27
 
28
- Detect the platform first and install only the matching variant:
28
+ Detect the operating system and targeted platform first, then write and wire the corresponding configuration:
29
+
30
+ ### Claude Code
29
31
 
30
32
  - **Linux / macOS** → write `~/.claude/statusline-command.sh` (pure bash + `jq`), wire into `~/.claude/settings.json`.
31
33
  - **Windows** → write `%USERPROFILE%\.claude\statusline-command.ps1`, wire into `%USERPROFILE%\.claude\settings.json`.
32
34
 
35
+ ### Antigravity / Gemini CLI
36
+
37
+ - **Linux / macOS** → write `~/.gemini/antigravity-cli/statusline-command.sh` (pure bash + `jq`), wire into `~/.gemini/antigravity-cli/settings.json`.
38
+ - **Windows** → write `%USERPROFILE%\.gemini\antigravity-cli\statusline-command.ps1`, wire into `%USERPROFILE%\.gemini\antigravity-cli\settings.json`.
39
+
33
40
  ## Token Formatting
34
41
 
35
42
  - `< 1000` → raw integer (e.g. `↑850 ↓120`)
@@ -39,12 +46,14 @@ Detect the platform first and install only the matching variant:
39
46
 
40
47
  ## Token Source
41
48
 
42
- Claude Code does **not** include token totals in the statusline JSON. Read `transcript_path` (JSONL, one message per line) and sum across **assistant** messages:
49
+ Tokens are calculated dynamically:
43
50
 
44
- - `input` = `message.usage.input_tokens` + `message.usage.cache_creation_input_tokens` + `message.usage.cache_read_input_tokens`
45
- - `output` = `message.usage.output_tokens`
46
-
47
- Skip lines that fail JSON parsing or lack `message.usage`. If `transcript_path` is missing or the file does not exist, omit the tokens segment.
51
+ 1. **Transcript Parsing (Claude Code / general)**: Sum across assistant messages in the JSONL `transcript_path` file:
52
+ - `input` = `message.usage.input_tokens` + `message.usage.cache_creation_input_tokens` + `message.usage.cache_read_input_tokens`
53
+ - `output` = `message.usage.output_tokens`
54
+ 2. **Payload Fallback (Antigravity / Gemini CLI)**: If transcript parsing yields `0` tokens (or the transcript doesn't store step token metrics), fallback to:
55
+ - `input` = `context_window.total_input_tokens`
56
+ - `output` = `context_window.total_output_tokens`
48
57
 
49
58
  ## Quota Colors (by REMAINING quota = 100 - used%)
50
59
 
@@ -57,7 +66,7 @@ Skip lines that fail JSON parsing or lack `message.usage`. If `transcript_path`
57
66
 
58
67
  The 5h and 7d windows must be colored **independently**.
59
68
 
60
- ## ETA Format (from epoch seconds in `resets_at`)
69
+ ## ETA Format (from epoch seconds or reset duration in seconds)
61
70
 
62
71
  - `> 1 day` → `Xd Yh`
63
72
  - `> 1 hour` → `Xh Ym`
@@ -66,34 +75,57 @@ The 5h and 7d windows must be colored **independently**.
66
75
 
67
76
  ## Input Schema (stdin JSON)
68
77
 
69
- Any field may be missing:
78
+ The input schema depends on the platform:
79
+
80
+ ### Claude Code Schema
70
81
 
82
+ ```json
83
+ {
84
+ "cwd": "/path/to/cwd",
85
+ "transcript_path": "/path/to/transcript.jsonl",
86
+ "model": { "display_name": "Opus 4.7" },
87
+ "effort": { "level": "medium" },
88
+ "context_window": { "used_percentage": 42 },
89
+ "rate_limits": {
90
+ "five_hour": { "used_percentage": 80, "resets_at": 1719213265 },
91
+ "seven_day": { "used_percentage": 50, "resets_at": 1719213265 }
92
+ }
93
+ }
71
94
  ```
72
- cwd
73
- transcript_path
74
- model.display_name
75
- effort.level
76
- context_window.used_percentage
77
- rate_limits.five_hour.used_percentage
78
- rate_limits.five_hour.resets_at
79
- rate_limits.seven_day.used_percentage
80
- rate_limits.seven_day.resets_at
95
+
96
+ ### Antigravity Schema
97
+
98
+ ```json
99
+ {
100
+ "cwd": "/path/to/cwd",
101
+ "transcript_path": "/path/to/transcript.jsonl",
102
+ "model": { "display_name": "Gemini 3.5 Flash" },
103
+ "context_window": {
104
+ "used_percentage": 6.7,
105
+ "total_input_tokens": 70419,
106
+ "total_output_tokens": 16667
107
+ },
108
+ "quota": {
109
+ "gemini-5h": { "remaining_fraction": 0.87, "reset_in_seconds": 2515 },
110
+ "gemini-weekly": { "remaining_fraction": 0.97, "reset_in_seconds": 589315 }
111
+ }
112
+ }
81
113
  ```
82
114
 
83
115
  ## Linux / macOS Implementation
84
116
 
85
- Write `~/.claude/statusline-command.sh` (chmod +x). Pure bash + `jq` — no Python. `jq` must be installed (`apt install jq`, `brew install jq`, etc.); if missing, the script prints a one-line hint and exits 0 so the statusline area stays clean.
117
+ Write `statusline-command.sh` (chmod +x) to the targeted platform directory. Pure bash + `jq` — no Python. `jq` must be installed; if missing, the script prints a one-line hint and exits 0.
86
118
 
87
119
  JSON parsing rules:
88
120
 
89
121
  - Read the full stdin payload **once** with `cat`, parse all fields with a single `jq` call.
90
- - For the JSONL transcript, invoke `jq` **once over the whole file** (do not call `jq` per line) — use a streaming filter that reduces over `inputs`.
122
+ - Use `fromjson?` to process JSONL files line-by-line resiliently to ignore malformed/plain-text entries.
91
123
 
92
- `~/.claude/statusline-command.sh`:
124
+ `statusline-command.sh`:
93
125
 
94
126
  ```bash
95
127
  #!/usr/bin/env bash
96
- # Claude Code statusline — pure bash + jq.
128
+ # Statusline — pure bash + jq. Supports Claude Code & Antigravity payload formats.
97
129
 
98
130
  set -u
99
131
 
@@ -127,10 +159,14 @@ quota_color() {
127
159
 
128
160
  fmt_eta() {
129
161
  local resets="$1"
130
- [[ -z "$resets" || "$resets" == "null" ]] && { printf ''; return; }
162
+ [[ -z "$resets" || "$resets" == "null" || "$resets" == "" ]] && { printf ''; return; }
131
163
  local now delta days hours mins
132
- now=$(date +%s)
133
- delta=$(( resets - now ))
164
+ if (( resets < 10000000 )); then
165
+ delta=$resets
166
+ else
167
+ now=$(date +%s)
168
+ delta=$(( resets - now ))
169
+ fi
134
170
  if (( delta <= 0 )); then printf 'now'
135
171
  elif (( delta >= 86400 )); then
136
172
  days=$(( delta / 86400 )); hours=$(( (delta % 86400) / 3600 ))
@@ -173,15 +209,41 @@ read_fields() {
173
209
  .model.display_name // "",
174
210
  .effort.level // "",
175
211
  (.context_window.used_percentage // "" | tostring),
176
- (.rate_limits.five_hour.used_percentage // "" | tostring),
177
- (.rate_limits.five_hour.resets_at // "" | tostring),
178
- (.rate_limits.seven_day.used_percentage // "" | tostring),
179
- (.rate_limits.seven_day.resets_at // "" | tostring)
212
+ (
213
+ .rate_limits.five_hour.used_percentage //
214
+ (if .quota."gemini-5h" != null then ((1.0 - .quota."gemini-5h".remaining_fraction) * 100)
215
+ elif .quota."3p-5h" != null then ((1.0 - .quota."3p-5h".remaining_fraction) * 100)
216
+ else "" end)
217
+ | tostring
218
+ ),
219
+ (
220
+ .rate_limits.five_hour.resets_at //
221
+ .quota."gemini-5h".reset_in_seconds //
222
+ .quota."3p-5h".reset_in_seconds //
223
+ ""
224
+ | tostring
225
+ ),
226
+ (
227
+ .rate_limits.seven_day.used_percentage //
228
+ (if .quota."gemini-weekly" != null then ((1.0 - .quota."gemini-weekly".remaining_fraction) * 100)
229
+ elif .quota."3p-weekly" != null then ((1.0 - .quota."3p-weekly".remaining_fraction) * 100)
230
+ else "" end)
231
+ | tostring
232
+ ),
233
+ (
234
+ .rate_limits.seven_day.resets_at //
235
+ .quota."gemini-weekly".reset_in_seconds //
236
+ .quota."3p-weekly".reset_in_seconds //
237
+ ""
238
+ | tostring
239
+ ),
240
+ (.context_window.total_input_tokens // "" | tostring),
241
+ (.context_window.total_output_tokens // "" | tostring)
180
242
  ] | join("\u001f")
181
243
  ' 2>/dev/null <<<"$payload"
182
244
  }
183
245
 
184
- IFS=$'\x1f' read -r cwd transcript model effort ctx five_used five_reset seven_used seven_reset < <(read_fields)
246
+ IFS=$'\x1f' read -r cwd transcript model effort ctx five_used five_reset seven_used seven_reset payload_in payload_out < <(read_fields)
185
247
 
186
248
  segments=()
187
249
 
@@ -211,10 +273,23 @@ if [[ -n "$ctx" ]]; then
211
273
  segments+=("${C_CTX}ctx ${ctx_int}%${RESET}")
212
274
  fi
213
275
 
214
- # --- tokens: one jq pass over the whole JSONL transcript ---
276
+ ti=0
277
+ to=0
278
+
279
+ # --- tokens: sum transcripts, fallback to context_window if needed ---
280
+ if [[ -n "$transcript" ]]; then
281
+ # Resolve path mismatch (if it has .gemini/antigravity/ but it actually is at .gemini/antigravity-cli/)
282
+ if [[ ! -f "$transcript" && "$transcript" == *"/antigravity/"* ]]; then
283
+ corrected_transcript="${transcript//\/antigravity\//\/antigravity-cli\/}"
284
+ if [[ -f "$corrected_transcript" ]]; then
285
+ transcript="$corrected_transcript"
286
+ fi
287
+ fi
288
+ fi
289
+
215
290
  if [[ -n "$transcript" && -f "$transcript" ]]; then
216
- tok=$(jq -rs '
217
- reduce .[] as $m ({i:0,o:0};
291
+ tok=$(jq -Rn '
292
+ reduce (inputs | fromjson?) as $m ({i:0,o:0};
218
293
  ($m.message.usage // null) as $u
219
294
  | if $u == null then .
220
295
  else
@@ -226,12 +301,21 @@ if [[ -n "$transcript" && -f "$transcript" ]]; then
226
301
  ' "$transcript" 2>/dev/null) || tok=""
227
302
  if [[ -n "$tok" ]]; then
228
303
  IFS=$'\t' read -r ti to <<<"$tok"
229
- if (( ti > 0 || to > 0 )); then
230
- segments+=("${C_TOK}↑$(fmt_tokens "$ti") ↓$(fmt_tokens "$to")${RESET}")
231
- fi
232
304
  fi
233
305
  fi
234
306
 
307
+ # Fallback to payload context_window tokens if transcript sum is 0
308
+ if (( ti == 0 && to == 0 )); then
309
+ if [[ -n "$payload_in" && -n "$payload_out" ]]; then
310
+ ti=$payload_in
311
+ to=$payload_out
312
+ fi
313
+ fi
314
+
315
+ if (( ti > 0 || to > 0 )); then
316
+ segments+=("${C_TOK}↑$(fmt_tokens "$ti") ↓$(fmt_tokens "$to")${RESET}")
317
+ fi
318
+
235
319
  if [[ -n "$five_used" ]]; then
236
320
  fu=$(awk -v n="$five_used" 'BEGIN{printf "%d", (n+0.5)}')
237
321
  eta=$(fmt_eta "$five_reset")
@@ -253,22 +337,11 @@ done
253
337
  printf '%s' "$out"
254
338
  ```
255
339
 
256
- Wire into `~/.claude/settings.json` (merge into existing JSON, don't clobber other keys):
257
-
258
- ```json
259
- {
260
- "statusLine": {
261
- "type": "command",
262
- "command": "bash ~/.claude/statusline-command.sh"
263
- }
264
- }
265
- ```
266
-
267
340
  ## Windows Implementation
268
341
 
269
- Write `%USERPROFILE%\.claude\statusline-command.ps1`. Use `ConvertFrom-Json` (no Python dep). Read the JSONL transcript with `Get-Content -ReadCount 0` and `ConvertFrom-Json` per line inside try/catch. Use `[char]27` for ANSI escapes (works on PS 5.1 and 7+). Read stdin with `[Console]::In.ReadToEnd()`.
342
+ Write `statusline-command.ps1` to the targeted platform directory. Use `ConvertFrom-Json` (no Python dep). Read the JSONL transcript with `Get-Content -ReadCount 0` and `ConvertFrom-Json` per line inside try/catch. Use `[char]27` for ANSI escapes.
270
343
 
271
- `%USERPROFILE%\.claude\statusline-command.ps1`:
344
+ `statusline-command.ps1`:
272
345
 
273
346
  ```powershell
274
347
  $ErrorActionPreference = 'SilentlyContinue'
@@ -298,8 +371,13 @@ function QuotaColor([double]$used) {
298
371
  }
299
372
 
300
373
  function FmtEta($resetsAt) {
301
- if ($null -eq $resetsAt) { return "" }
302
- $delta = [int64]$resetsAt - [int64](Get-Date -UFormat %s)
374
+ if ($null -eq $resetsAt -or $resetsAt -eq "") { return "" }
375
+ $val = [int64]$resetsAt
376
+ if ($val -lt 10000000) {
377
+ $delta = $val
378
+ } else {
379
+ $delta = $val - [int64](Get-Date -UFormat %s)
380
+ }
303
381
  if ($delta -le 0) { return "now" }
304
382
  $days = [math]::Floor($delta / 86400); $delta = $delta % 86400
305
383
  $hours = [math]::Floor($delta / 3600); $delta = $delta % 3600
@@ -334,6 +412,13 @@ function GetBranch($cwd) {
334
412
 
335
413
  function SumTokens($path) {
336
414
  if (-not $path) { return $null }
415
+ # Resolve path mismatch (if it has .gemini/antigravity/ but it actually is at .gemini/antigravity-cli/)
416
+ if (-not (Test-Path -LiteralPath $path) -and $path.Contains("/.gemini/antigravity/")) {
417
+ $corrected = $path.Replace(".gemini/antigravity/", ".gemini/antigravity-cli/").Replace(".gemini\antigravity\", ".gemini\antigravity-cli\")
418
+ if (Test-Path -LiteralPath $corrected) {
419
+ $path = $corrected
420
+ }
421
+ }
337
422
  if (-not (Test-Path -LiteralPath $path)) { return $null }
338
423
  $sumIn = 0; $sumOut = 0; $found = $false
339
424
  $lines = Get-Content -LiteralPath $path -ReadCount 0 -ErrorAction SilentlyContinue
@@ -377,39 +462,99 @@ $ctx = $data.context_window.used_percentage
377
462
  if ($null -ne $ctx) { $segments += "$C_CTX" + "ctx " + [int][math]::Round([double]$ctx) + "%$RESET" }
378
463
 
379
464
  $tokens = SumTokens $data.transcript_path
465
+ $ti = 0
466
+ $to = 0
380
467
  if ($null -ne $tokens) {
381
- $segments += "$C_TOK↑$(FmtTokens $tokens[0]) ↓$(FmtTokens $tokens[1])$RESET"
468
+ $ti = $tokens[0]
469
+ $to = $tokens[1]
382
470
  }
383
471
 
384
- $five = $data.rate_limits.five_hour
385
- $seven = $data.rate_limits.seven_day
386
- if ($null -ne $five.used_percentage) {
387
- $u = [double]$five.used_percentage
388
- $segments += "$(QuotaColor $u)5h:$([int][math]::Round($u))%($(FmtEta $five.resets_at))$RESET"
472
+ # Fallback to payload context_window tokens if transcript sum is 0
473
+ if ($ti -eq 0 -and $to -eq 0) {
474
+ if ($null -ne $data.context_window.total_input_tokens -and $null -ne $data.context_window.total_output_tokens) {
475
+ $ti = [int64]$data.context_window.total_input_tokens
476
+ $to = [int64]$data.context_window.total_output_tokens
477
+ }
389
478
  }
390
- if ($null -ne $seven.used_percentage) {
391
- $u = [double]$seven.used_percentage
392
- $segments += "$(QuotaColor $u)7d:$([int][math]::Round($u))%($(FmtEta $seven.resets_at))$RESET"
479
+
480
+ if ($ti -gt 0 -or $to -gt 0) {
481
+ $segments += "$C_TOK↑$(FmtTokens $ti) ↓$(FmtTokens $to)$RESET"
482
+ }
483
+
484
+ $five_used = $null
485
+ $five_reset = $null
486
+ $seven_used = $null
487
+ $seven_reset = $null
488
+
489
+ if ($null -ne $data.rate_limits.five_hour.used_percentage) {
490
+ $five_used = $data.rate_limits.five_hour.used_percentage
491
+ $five_reset = $data.rate_limits.five_hour.resets_at
492
+ } elseif ($null -ne $data.quota) {
493
+ $q5 = if ($null -ne $data.quota."gemini-5h") { $data.quota."gemini-5h" } else { $data.quota."3p-5h" }
494
+ if ($null -ne $q5) {
495
+ $five_used = (1.0 - $q5.remaining_fraction) * 100
496
+ $five_reset = $q5.reset_in_seconds
497
+ }
498
+ }
499
+
500
+ if ($null -ne $data.rate_limits.seven_day.used_percentage) {
501
+ $seven_used = $data.rate_limits.seven_day.used_percentage
502
+ $seven_reset = $data.rate_limits.seven_day.resets_at
503
+ } elseif ($null -ne $data.quota) {
504
+ $qw = if ($null -ne $data.quota."gemini-weekly") { $data.quota."gemini-weekly" } else { $data.quota."3p-weekly" }
505
+ if ($null -ne $qw) {
506
+ $seven_used = (1.0 - $qw.remaining_fraction) * 100
507
+ $seven_reset = $qw.reset_in_seconds
508
+ }
509
+ }
510
+
511
+ if ($null -ne $five_used) {
512
+ $segments += "$(QuotaColor $five_used)5h:$([int][math]::Round($five_used))%($(FmtEta $five_reset))$RESET"
513
+ }
514
+ if ($null -ne $seven_used) {
515
+ $segments += "$(QuotaColor $seven_used)7d:$([int][math]::Round($seven_used))%($(FmtEta $seven_reset))$RESET"
393
516
  }
394
517
 
395
518
  $sep = " $C_PIPE|$RESET "
396
519
  [Console]::Out.Write([string]::Join($sep, $segments))
397
520
  ```
398
521
 
399
- Wire into `%USERPROFILE%\.claude\settings.json`:
522
+ ## Settings Wiring
523
+
524
+ ### Claude Code
525
+
526
+ Wire into `~/.claude/settings.json` (or `%USERPROFILE%\.claude\settings.json` on Windows):
527
+
528
+ ```json
529
+ {
530
+ "statusLine": {
531
+ "type": "command",
532
+ "command": "bash ~/.claude/statusline-command.sh"
533
+ }
534
+ }
535
+ ```
536
+
537
+ (Or the `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\.claude\statusline-command.ps1"` variant on Windows).
538
+
539
+ ### Antigravity CLI
540
+
541
+ Wire into `~/.gemini/antigravity-cli/settings.json` (or `%USERPROFILE%\.gemini\antigravity-cli\settings.json` on Windows):
400
542
 
401
543
  ```json
402
544
  {
403
545
  "statusLine": {
404
546
  "type": "command",
405
- "command": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"%USERPROFILE%\\.claude\\statusline-command.ps1\""
547
+ "command": "bash ~/.gemini/antigravity-cli/statusline-command.sh",
548
+ "enabled": true
406
549
  }
407
550
  }
408
551
  ```
409
552
 
553
+ (Or the `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\.gemini\antigravity-cli\statusline-command.ps1"` variant on Windows).
554
+
410
555
  ## Smoke Test
411
556
 
412
- After installing, generate a fake JSON payload with 5h at 80% used, 7d at 50% used, both with future `resets_at`, plus a `transcript_path` pointing at a small fixture JSONL containing two assistant messages with `usage` blocks. Pipe it into the script and confirm:
557
+ After installing, generate a fake JSON payload with 5h at 80% used, 7d at 50% used, both with future `resets_at`/`reset_in_seconds`, plus a `transcript_path` pointing at a small fixture JSONL containing two assistant messages with `usage` blocks. Pipe it into the script and confirm:
413
558
 
414
559
  - 5h renders **orange**, 7d renders **amber**.
415
560
  - cwd, model, and context % are present.
@@ -0,0 +1,245 @@
1
+ ---
2
+ name: universalize-agents
3
+ description: Use when the user wants to consolidate or "universalize" their AI agent setup across platforms (Claude Code, Codex, Windsurf, GitHub Copilot, Gemini CLI, Antigravity). Discovers each platform's installed skills/agents/commands and master instructions, copies them into a single `.agents/` directory, migrates master instructions into a canonical `AGENTS.md`, and installs a per-platform session-start hook that symlinks `.agents/` back into each platform's directories.
4
+ version: 0.1.0
5
+ ---
6
+
7
+ # Universalize Agents
8
+
9
+ Consolidate a multi-platform AI agent setup into one source of truth
10
+ (`.agents/` + `AGENTS.md`) and wire every platform to re-materialize it via
11
+ symlinks on each session start.
12
+
13
+ This skill is the first step of a larger "universalize agent setup" feature.
14
+ It performs discovery, consolidation, and hook installation. It does **not**
15
+ modify this repository's `src/` or CLI.
16
+
17
+ ## Canonical references
18
+
19
+ Read these before acting — they are the source of truth and must not drift
20
+ from the codebase:
21
+
22
+ - `reference/mapping.md` — reverse map of every platform's
23
+ skills/agents/commands directories (derived from `src/utils/paths.ts`),
24
+ the per-platform hook-config file locations, and the master-instruction
25
+ file names.
26
+ - `scripts/agent-setup.sh` (macOS/Linux) and `scripts/agent-setup.ps1`
27
+ (Windows) — the idempotent symlink templates that get installed.
28
+ - `reference/hook-templates/` — per-platform session-start hook snippets.
29
+
30
+ ## When to Use
31
+
32
+ Invoke when the user asks to "universalize", "consolidate", "unify", or
33
+ "centralize" their agent setup, skills, commands, or instructions across more
34
+ than one AI coding platform.
35
+
36
+ ## Workflow
37
+
38
+ Follow these phases in order. Stop and ask whenever a decision is the user's
39
+ to make.
40
+
41
+ ### Phase 1 — Choose scope
42
+
43
+ Ask the user: **universalize the setup for the current project, or globally?**
44
+
45
+ - `project` → `.agents/` lives at the project root; platform targets are the
46
+ project-relative directories in `reference/mapping.md`.
47
+ - `global` → `.agents/` lives at `$HOME/.agents`; platform targets are the
48
+ `~/...` directories in `reference/mapping.md`.
49
+
50
+ Record the chosen scope and the resolved base directory (`BASE`). Everything
51
+ downstream uses `BASE`.
52
+
53
+ ### Phase 2 — Discover existing setups
54
+
55
+ For each of the six platforms, check whether its directories exist under
56
+ `BASE` (per `reference/mapping.md`). Treat a platform as "in use" if any of
57
+ its skills/agents/commands dirs or its master-instruction file exists.
58
+
59
+ For each in-use platform, enumerate items in its `skills`, `agents`,
60
+ `workflows`, and `rules` source dirs. Some platforms collapse multiple types into one
61
+ directory (Codex puts skills + agents + workflows in `.codex/skills`; Claude
62
+ Code puts workflows in `.claude/skills`). Disambiguate an item's real type by
63
+ reading its frontmatter / the `x-ai-agents-type` metadata this toolchain
64
+ writes when wrapping items — never by directory alone.
65
+
66
+ Report a discovery summary (platforms in use, item counts per type) before
67
+ continuing.
68
+
69
+ ### Phase 3 — Build `.agents/`
70
+
71
+ Create the unified layout under `BASE`:
72
+
73
+ ```text
74
+ .agents/
75
+ skills/ agents/ commands/ rules/ hooks/
76
+ ```
77
+
78
+ Copy discovered items into `.agents/{skills,agents,commands,rules}`. When the
79
+ same-named item is found on more than one platform, copy it once: prefer the
80
+ first platform discovered and warn about the collision rather than silently
81
+ overwriting. Never wipe an existing `.agents/` — merge into it.
82
+
83
+ #### Format normalization
84
+
85
+ The unified store is markdown-only. Normalize as you copy:
86
+
87
+ - **Rules.** Collect standard markdown rule files from the platforms that
88
+ support them — Claude, Gemini, and Antigravity use `<agent-dir>/rules`;
89
+ GitHub Copilot uses `.github/instructions` (see `reference/mapping.md`) —
90
+ into `.agents/rules/`. Codex and Windsurf have none. These are per-platform
91
+ rule files, distinct from the master instruction files consolidated in
92
+ Phase 4. Rules are **not** symlinked back (not in `__LINK_MAP__`); Phase 4
93
+ wires the whole dir into `AGENTS.md`.
94
+ - **Workflows → commands.** Treat every platform's "workflow" items as the
95
+ universal `commands` type and place them in `.agents/commands/`. If a prior
96
+ run left a `.agents/workflows/` directory, move its contents into
97
+ `.agents/commands/` and remove the now-empty `workflows/` dir.
98
+ - **Gemini commands → skills.** Gemini stores commands as TOML
99
+ (`.gemini/commands/*.toml`), which cannot be symlinked back as markdown.
100
+ Convert each Gemini `*.toml` into a skill: parse its `description` and
101
+ `prompt = """..."""` fields, then write `.agents/skills/<name>/SKILL.md`
102
+ with the prompt as the body and this frontmatter:
103
+
104
+ ```markdown
105
+ ---
106
+ name: <toml file name without extension>
107
+ description: <value of the TOML `description` field>
108
+ ---
109
+
110
+ <contents of the TOML `prompt` field>
111
+ ```
112
+
113
+ This is the reverse of `convertToGeminiCommandTOML` in
114
+ `src/utils/toml.ts`. Gemini workflows therefore land in `.agents/skills/`,
115
+ not `.agents/commands/`.
116
+
117
+ ### Phase 4 — Migrate master instructions
118
+
119
+ Detect every master-instruction file present in scope:
120
+
121
+ | Platform | File |
122
+ |---|---|
123
+ | Claude Code | `CLAUDE.md` |
124
+ | Codex / Antigravity / Windsurf | `AGENTS.md` |
125
+ | Gemini CLI | `GEMINI.md` |
126
+ | GitHub Copilot | `.github/copilot-instructions.md` |
127
+
128
+ The canonical target is root `AGENTS.md` (already the native master file for
129
+ Codex, Antigravity, and Windsurf).
130
+
131
+ - **None found** → create a minimal `AGENTS.md`.
132
+ - **Exactly one found** → use it as `AGENTS.md` directly, no prompt.
133
+ - **Two or more distinct files found** → STOP and ask the user how to
134
+ consolidate:
135
+ 1. **Keep one** — show each detected file with a short preview / line
136
+ count; the user picks which becomes `AGENTS.md`.
137
+ 2. **Merge all + trim** — concatenate every source, then de-duplicate
138
+ overlapping instructions into one coherent `AGENTS.md` (semantic
139
+ de-duplication, preserving every unique directive). Surface any
140
+ conflicting directives to the user instead of dropping them silently.
141
+
142
+ After producing `AGENTS.md` (at `BASE`), **replace the entire contents** of
143
+ each non-`AGENTS.md` master file with a single import line — its original
144
+ content now lives in `AGENTS.md`:
145
+
146
+ ```text
147
+ @AGENTS.md
148
+ ```
149
+
150
+ Always create `AGENTS.md` (with the migrated content) before overwriting any
151
+ master file, so nothing is lost. Platforms that natively read `AGENTS.md`
152
+ (Codex, Antigravity, Windsurf) need no such file.
153
+
154
+ **Wiring rules into masters.** Rules are not symlinked into any platform dir.
155
+ Instead, import the whole `.agents/rules/` directory into `AGENTS.md` with a
156
+ single directory import, so every platform that reads `AGENTS.md` (directly, or
157
+ via its `@AGENTS.md` master import) picks them up:
158
+
159
+ ```text
160
+ @.agents/rules/
161
+ ```
162
+
163
+ Add this import only when `.agents/rules/` is non-empty.
164
+
165
+ ### Phase 5 — Install and run the setup script
166
+
167
+ Pick the script for the host OS — `scripts/agent-setup.sh` on macOS/Linux,
168
+ `scripts/agent-setup.ps1` on Windows — copy it into `BASE/.agents/hooks/`
169
+ (keeping its extension), make it executable, and fill its two placeholders:
170
+
171
+ - `__AGENTS_DIR__` → path to `BASE/.agents`: **relative** (`.agents`) for
172
+ project scope, **absolute** for global scope.
173
+ - `__LINK_MAP__` → one `SRC_SUBDIR|DEST_DIR` line per (in-use platform × type),
174
+ using the scope-correct destination dirs from `reference/mapping.md`.
175
+ `SRC_SUBDIR` is `skills`, `agents`, or `commands`. Use **relative** dest
176
+ dirs (e.g. `.claude/skills`) for project scope and **absolute** dirs for
177
+ global scope. The script detects the mode from `__AGENTS_DIR__` and emits
178
+ relative symlinks for project scope, absolute for global.
179
+
180
+ The script symlinks each item individually (not whole directories) so that
181
+ collapsed targets like `.codex/skills` can receive skills + agents + commands
182
+ merged together without conflict. It is idempotent and refuses to clobber a
183
+ real (non-symlink) path.
184
+
185
+ In **project scope** the script keeps the generated symlinks out of git by
186
+ writing a single `.gitignore` per platform at its base dir (the parent of the
187
+ target dirs, e.g. `.claude/.gitignore`) that lists each symlink subdir
188
+ (`skills/`, `commands/`, …). Only the canonical `.agents/` content (which you
189
+ commit) is version-controlled. Global scope skips this (it is not inside a
190
+ repo).
191
+
192
+ Once `.agents/` holds every item and the script is installed, **delete the
193
+ original item files/directories at each platform source dir**. The canonical
194
+ copy now lives in `.agents/`, so the originals are redundant — and because the
195
+ symlink step refuses to overwrite a real path, it cannot link until the
196
+ original is gone. Then run the setup script once to create the symlinks; every
197
+ later session-start run re-applies them idempotently.
198
+
199
+ ### Phase 6 — Install session-start hooks
200
+
201
+ For each in-use platform, register a session-start hook that runs the
202
+ installed setup script in `BASE/.agents/hooks/` (`agent-setup.sh` on
203
+ macOS/Linux, `agent-setup.ps1` on Windows), writing to the config file in
204
+ `reference/mapping.md`:
205
+
206
+ | Platform | Project file | Global file | Format |
207
+ |---|---|---|---|
208
+ | Claude Code | `.claude/settings.json` | `~/.claude/settings.json` | JSON |
209
+ | Gemini / Antigravity | `.gemini/settings.json` | `~/.gemini/settings.json` | JSON |
210
+ | Codex | `.codex/config.toml` | `~/.codex/config.toml` | TOML |
211
+ | Windsurf | — | — | no session-start hook (skip) |
212
+ | GitHub Copilot | `.github/hooks/agent-setup.json` | `~/.copilot/hooks/agent-setup.json` | JSON |
213
+
214
+ Rules for every write:
215
+
216
+ - **Append, never overwrite.** Parse the existing file, inject the hook entry
217
+ only if an equivalent one is absent, then write back. Preserve all other
218
+ keys and formatting as much as the format allows.
219
+ - **Path style follows scope.** The hook command points at the setup script:
220
+ use a **relative** path (`.agents/hooks/agent-setup.sh`) for project scope
221
+ and an **absolute** path for global scope — matching how `__AGENTS_DIR__`
222
+ was filled.
223
+ - Back up the file (`<file>.bak`) before the first modification.
224
+ - Use the verified template for each platform in `reference/hook-templates/`
225
+ (`claude-code.json`, `gemini.json`, `codex.toml`, `copilot.json`).
226
+ **Windsurf has no session-start hook**, so it gets no recurring hook. Keep
227
+ its dirs in the Phase 5 `__LINK_MAP__` so the one-time setup run symlinks
228
+ them, but tell the user the shared `.agents/` will not auto-refresh on
229
+ Windsurf — they re-run the setup script manually to pick up later changes.
230
+ Codex prompts for a trust review of project hooks on first use.
231
+
232
+ ### Phase 7 — Report
233
+
234
+ Summarize: scope, platforms processed, items consolidated per type, how
235
+ master instructions were resolved, which hooks were installed (and which were
236
+ skipped/unverified), and any collisions or backups created.
237
+
238
+ ## Constraints
239
+
240
+ - Append-only for existing config (settings/hook) files; back up first.
241
+ Master instruction files are the exception: replace their contents with the
242
+ `@AGENTS.md` import once the content is migrated into `AGENTS.md`.
243
+ - Per-item symlinks, never whole-directory, never clobber real paths.
244
+ - Do not fabricate hook schemas; verify unverified platforms at runtime.
245
+ - Do not modify this repo's `src/`, CLI, or `paths.ts`.