cortexhawk 3.1.1 → 3.2.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/CHANGELOG.md +21 -1
- package/CLAUDE.md +2 -2
- package/README.md +20 -5
- package/agents/git-manager.md +4 -0
- package/commands/backlog.md +1 -0
- package/commands/commit.md +24 -0
- package/commands/ship.md +1 -1
- package/commands/task.md +1 -0
- package/hooks/session-start.sh +7 -4
- package/install.sh +239 -73
- package/package.json +1 -1
- package/scripts/autodetect-profile.sh +5 -1
- package/scripts/interactive-init.sh +17 -10
- package/scripts/refresh-context.sh +51 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,12 +3,32 @@
|
|
|
3
3
|
All notable changes to CortexHawk are documented here.
|
|
4
4
|
Format: [Keep a Changelog](https://keepachangelog.com/)
|
|
5
5
|
|
|
6
|
-
## [3.
|
|
6
|
+
## [3.2.0] - 2026-02-15
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- `/commit` command: lightweight conventional commit + push without review or PR — use `/ship` for full workflow, `/commit` for quick iterations
|
|
10
|
+
- Install auto-detects existing PR/commit templates; generates CortexHawk defaults (`.github/PULL_REQUEST_TEMPLATE.md`, `.gitmessage`) if missing — agents (`git-manager`, `/ship`, `/commit`) read templates at runtime
|
|
11
|
+
- `--version` / `-v` flag: displays CortexHawk version
|
|
12
|
+
- `scripts/refresh-context.sh`: regenerates `docs/.context/_shared.md` mid-session — `/task` and `/backlog` auto-refresh after modifying backlog
|
|
7
13
|
|
|
8
14
|
### Fixed
|
|
9
15
|
- `branch-guard` / `commit-guard` hooks: JSON parsing via `jq` with regex fallback — fixes false "hook error" on commands containing quotes or HEREDOC
|
|
10
16
|
- `commit-guard`: multi-format commit message extraction (HEREDOC, single/double quotes)
|
|
11
17
|
- `file-guard`: match on basename only — `*secret*`/`*credentials*` no longer false-positive on legitimate files (e.g., `oauth_service.py`); `.env.example`/`.env.sample`/`.env.template` whitelisted; `docker-compose*.yml` unblocked
|
|
18
|
+
- **Security**: eliminate shell injection in all Python HEREDOC/inline scripts — `$hooks_json`, file paths, and user input no longer interpolated into Python source; uses `sys.argv`/`sys.stdin` instead. Hook names validated against path traversal, hook paths shell-escaped via `shlex.quote()`
|
|
19
|
+
- Portable `sed -i` via `sed_inplace()` helper — fixes `sed` failures on macOS (BSD) across snapshot, hook toggle, and init wizard
|
|
20
|
+
- Argument validation for `--target`, `--profile`, and `--restore` flags — prevents cryptic shell errors when value is missing
|
|
21
|
+
- `update_gitignore()` now ensures essential entries (`.env`, `node_modules/`, `dist/`, etc.) are present — previously only added `.claude/` to an existing `.gitignore`
|
|
22
|
+
- Warning when python3 is not found — `generate_hooks_config()` previously failed silently, now alerts user that static fallback is used
|
|
23
|
+
- `.env` parser now strips single quotes, inline comments, and trailing whitespace — previously only stripped double quotes
|
|
24
|
+
- `--init` wizard now supports "All" and "Auto-detect" targets — removed false `--target all/auto` + `--init` guards, scope step auto-selects local for multi-target
|
|
25
|
+
- Snapshot no longer reads stale `/tmp/cortexhawk-custom-*.json` from previous runs — uses `$PROFILE_FILE` from current session only
|
|
26
|
+
- `copy_skills()` now warns when a skill from the profile doesn't exist in source — previously silently skipped
|
|
27
|
+
- Removed `local` keyword used outside function in `--target auto` dispatcher — fixes portability issues
|
|
28
|
+
- `.gitignore` no longer duplicates `# CortexHawk` header with `--target all` — groups all target dirs under single header
|
|
29
|
+
- `SKILL_COUNT` no longer shows 1 when no skills detected — fixed `wc -l` on empty string in `autodetect-profile.sh`
|
|
30
|
+
- `_shared.md` now includes generation timestamp for staleness awareness
|
|
31
|
+
- `settings.json` now merges on reinstall instead of skipping — new hooks regenerated from compose.yml, new permissions added via union (user customizations preserved). `--update` also merges new permissions
|
|
12
32
|
|
|
13
33
|
## [3.1.0] - 2026-02-14
|
|
14
34
|
|
package/CLAUDE.md
CHANGED
|
@@ -6,7 +6,7 @@ Open-source development toolkit for Claude Code — optimized agents, skills, co
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
agents/ — 20 specialized AI agents
|
|
9
|
-
commands/ —
|
|
9
|
+
commands/ — 33 slash commands
|
|
10
10
|
scripts/ — Validation and post-install audit scripts
|
|
11
11
|
skills/ — 36 domain-specific knowledge modules
|
|
12
12
|
hooks/ — 9 lifecycle hooks
|
|
@@ -49,7 +49,7 @@ Custom agents in `.cortexhawk-agents/` at project root. Each `.md` file uses `ex
|
|
|
49
49
|
|
|
50
50
|
## Commands
|
|
51
51
|
|
|
52
|
-
`/plan` `/build` `/test` `/review` `/ship` `/debug` `/scan` `/check` `/refactor` `/research` `/doc` `/bootstrap` `/tdd` `/optimize` `/migrate` `/monitor` `/api-gen` `/changelog` `/journal` `/brainstorm` `/simplify` `/deploy` `/export` `/backlog` `/pulse` `/map` `/learn` `/chain` `/task` `/ci` `/context` `/upgrade`
|
|
52
|
+
`/plan` `/build` `/test` `/review` `/ship` `/commit` `/debug` `/scan` `/check` `/refactor` `/research` `/doc` `/bootstrap` `/tdd` `/optimize` `/migrate` `/monitor` `/api-gen` `/changelog` `/journal` `/brainstorm` `/simplify` `/deploy` `/export` `/backlog` `/pulse` `/map` `/learn` `/chain` `/task` `/ci` `/context` `/upgrade`
|
|
53
53
|
|
|
54
54
|
## Skills
|
|
55
55
|
|
package/README.md
CHANGED
|
@@ -2,22 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/Spechawk94/CortexHawk/stargazers)
|
|
4
4
|
[](LICENSE)
|
|
5
|
-
[](CHANGELOG.md)
|
|
6
6
|
[](https://www.npmjs.com/package/cortexhawk)
|
|
7
7
|
[](https://skillsmp.com)
|
|
8
|
-
[](#whats-inside)
|
|
9
9
|
|
|
10
10
|
An open-source, community-driven development toolkit for Claude Code.
|
|
11
11
|
|
|
12
12
|
CortexHawk provides a modular collection of optimized agents, skills, commands, hooks, and behavioral modes that transform Claude Code into a full-stack development team. Every prompt has been written for maximum efficiency — less token bloat, sharper instructions, better agent coordination.
|
|
13
13
|
|
|
14
|
-
### What's New in v3.
|
|
14
|
+
### What's New in v3.2
|
|
15
|
+
|
|
16
|
+
- **`/commit` command** — lightweight conventional commit + push without review or PR (use `/ship` for full workflow)
|
|
17
|
+
- **`--version` flag** — standard CLI version display
|
|
18
|
+
- **PR/commit templates** — auto-detected at install, generated if missing; agents read templates at runtime
|
|
19
|
+
- **Settings.json merge** — reinstall and `--update` now merge new hooks + permissions instead of skipping
|
|
20
|
+
- **Security hardening** — eliminated shell injection in all Python HEREDOC scripts, portable `sed -i`, input validation
|
|
21
|
+
- **`--init` wizard** — "Auto-detect" target option, improved multi-target support
|
|
22
|
+
- **15+ bug fixes** — see [CHANGELOG.md](CHANGELOG.md) for full details
|
|
23
|
+
|
|
24
|
+
<details>
|
|
25
|
+
<summary>v3.1 changes</summary>
|
|
15
26
|
|
|
16
27
|
- **`npm install -g cortexhawk`** — available on npm, auto-resolves source from symlinked binary
|
|
17
28
|
- **`cortexhawk` CLI wrapper** — clean subcommands (init, install, update, doctor, validate, search, snapshot, etc.) instead of `bash install.sh --flags`
|
|
18
29
|
- **`--target auto`** — auto-detects installed CLIs (claude, kimi, codex) and installs for all found
|
|
19
30
|
- **`cortexhawk validate`** — post-install diagnostic verifying skills/agents discovery per target
|
|
20
31
|
|
|
32
|
+
</details>
|
|
33
|
+
|
|
21
34
|
<details>
|
|
22
35
|
<summary>v3.0 changes</summary>
|
|
23
36
|
|
|
@@ -103,7 +116,7 @@ Specialized AI agents that coordinate together instead of working in silos.
|
|
|
103
116
|
| `fullstack-developer` | Full-stack orchestration front+back |
|
|
104
117
|
| `teacher` | Teaches concepts with 3 pedagogical levels (guided, mentor, professor) |
|
|
105
118
|
|
|
106
|
-
### Commands (
|
|
119
|
+
### Commands (33)
|
|
107
120
|
|
|
108
121
|
Slash commands for common workflows.
|
|
109
122
|
|
|
@@ -114,6 +127,7 @@ Slash commands for common workflows.
|
|
|
114
127
|
| `/test` | Generate and run tests |
|
|
115
128
|
| `/review` | Multi-agent code review |
|
|
116
129
|
| `/ship` | Commit + PR pipeline |
|
|
130
|
+
| `/commit` | Lightweight commit + push (no review, no PR) |
|
|
117
131
|
| `/debug` | Debug and fix issues |
|
|
118
132
|
| `/scan` | Full security audit |
|
|
119
133
|
| `/check` | Pre-commit quality gate (lint + test + scan + review → GO/NO-GO) |
|
|
@@ -313,7 +327,7 @@ Each target adapts components to the CLI's native format:
|
|
|
313
327
|
| Component | Claude Code | Kimi CLI | Codex CLI |
|
|
314
328
|
|---|---|---|---|
|
|
315
329
|
| Agents (20) | `.claude/agents/*.md` | Skills (`/skill:agent-*`) + `AGENTS.md` | `AGENTS.md` |
|
|
316
|
-
| Commands (
|
|
330
|
+
| Commands (33) | `.claude/commands/*.md` → `/plan` | Skills (`/skill:cmd-*`) | Skills (`$cmd-*`) |
|
|
317
331
|
| Skills (36) | `.claude/skills/` | `.kimi/skills/` (auto-discovered) | `.agents/skills/` |
|
|
318
332
|
| Hooks (9) | `settings.json` (automatic) | Skills (`/skill:hook-*`, manual) | Dispatcher (partial) |
|
|
319
333
|
| Modes (7) | `.claude/modes/` (native) | Skills (`/skill:modes/*`) | Skills (`$mode-*`) |
|
|
@@ -335,6 +349,7 @@ Each target adapts components to the CLI's native format:
|
|
|
335
349
|
./install.sh --update --dry-run # preview update delta
|
|
336
350
|
|
|
337
351
|
# Diagnostics
|
|
352
|
+
./install.sh --version # show CortexHawk version
|
|
338
353
|
./install.sh --doctor # check installation health
|
|
339
354
|
./install.sh --test-hooks # dry-run all hooks with synthetic inputs
|
|
340
355
|
./install.sh --stats # installation overview (version, counts)
|
package/agents/git-manager.md
CHANGED
|
@@ -49,6 +49,10 @@ Description: imperative mood, lowercase, no period, max 72 chars
|
|
|
49
49
|
- Checklist: tests pass, no warnings, docs updated
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
## Templates
|
|
53
|
+
- Before creating a PR, check for `.github/PULL_REQUEST_TEMPLATE.md` — if found, follow that format
|
|
54
|
+
- Before committing, check for `.gitmessage` — if found, follow that format for the commit message
|
|
55
|
+
|
|
52
56
|
## Rules
|
|
53
57
|
- Atomic commits — one logical change per commit
|
|
54
58
|
- Never force-push to shared branches
|
package/commands/backlog.md
CHANGED
|
@@ -12,6 +12,7 @@ Activate the **project-manager** agent in backlog mode.
|
|
|
12
12
|
3. Score: impact (H/M/L), effort (H/M/L), feasibility (H/M/L)
|
|
13
13
|
4. Update `docs/backlog.md` — add new items, re-prioritize existing ones
|
|
14
14
|
5. Mark items already implemented as done
|
|
15
|
+
6. Run `bash scripts/refresh-context.sh` to update shared context
|
|
15
16
|
|
|
16
17
|
Backlog format in `docs/backlog.md`:
|
|
17
18
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: commit
|
|
3
|
+
description: Conventional commit and push — lightweight alternative to /ship (no review, no PR).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /commit
|
|
7
|
+
|
|
8
|
+
Activate the **git-manager** agent. Commit: `$ARGUMENTS`
|
|
9
|
+
|
|
10
|
+
1. Read `## Git Workflow` from CLAUDE.md if present — respect branching strategy, commit convention, and auto-push settings
|
|
11
|
+
2. If `.gitmessage` exists, read it for commit message format guidance
|
|
12
|
+
3. Review staged and unstaged changes, stage relevant files (never `git add -A`)
|
|
13
|
+
4. Generate conventional commit message from changes — format: `type(scope): description`
|
|
14
|
+
5. Commit with the generated message
|
|
15
|
+
6. If auto-push is enabled, push to remote
|
|
16
|
+
7. Show commit summary (hash, message, files changed)
|
|
17
|
+
|
|
18
|
+
## Rules
|
|
19
|
+
|
|
20
|
+
- No review pass — use `/ship` for reviewed commits, `/commit` for quick iterations
|
|
21
|
+
- No PR creation — use `/ship` when a PR is needed
|
|
22
|
+
- Respect `.gitignore` — never stage `.env`, secrets, or debug artifacts
|
|
23
|
+
- If no changes to commit, report and stop
|
|
24
|
+
- If `$ARGUMENTS` is provided, use it as commit message context (not the literal message)
|
package/commands/ship.md
CHANGED
|
@@ -11,7 +11,7 @@ Activate the **git-manager** agent, then the **reviewer** agent. Ship: `$ARGUMEN
|
|
|
11
11
|
1. Stage changes and generate conventional commit message
|
|
12
12
|
2. Run quick review pass — reviewer runs Pass 1 (Correctness) and Pass 2 (Security) only, reporting Critical findings exclusively
|
|
13
13
|
3. If review passes, commit and push
|
|
14
|
-
4. Create PR
|
|
14
|
+
4. Create PR — if `.github/PULL_REQUEST_TEMPLATE.md` exists, follow that format; otherwise use: Summary, Changes, Test Plan, Checklist
|
|
15
15
|
5. If review finds critical issues, report them and stop — don't ship broken code
|
|
16
16
|
|
|
17
17
|
Format: `feat(scope): description` or `fix(scope): description`
|
package/commands/task.md
CHANGED
|
@@ -16,6 +16,7 @@ Activate the **project-manager** agent as orchestrator. Execute backlog item `$A
|
|
|
16
16
|
6. Update `CHANGELOG.md` with a one-line entry under the current version's `### Added` section
|
|
17
17
|
7. If chain completes without critical blockers, execute `/ship`
|
|
18
18
|
8. Mark item as `done` in backlog
|
|
19
|
+
9. Run `bash scripts/refresh-context.sh` to update shared context
|
|
19
20
|
|
|
20
21
|
## Save Rules
|
|
21
22
|
|
package/hooks/session-start.sh
CHANGED
|
@@ -44,14 +44,17 @@ if [ -d "docs/.context" ] && [ ! -L "docs/.context" ]; then
|
|
|
44
44
|
|
|
45
45
|
echo "# Shared Context" > "$SHARED"
|
|
46
46
|
echo "" >> "$SHARED"
|
|
47
|
-
echo "_Auto-generated
|
|
47
|
+
echo "_Auto-generated at $(date '+%Y-%m-%d %H:%M'). Snapshot from session start — may be stale._" >> "$SHARED"
|
|
48
48
|
echo "" >> "$SHARED"
|
|
49
49
|
|
|
50
50
|
# Backlog summary
|
|
51
51
|
if [ -f "docs/backlog.md" ]; then
|
|
52
|
-
ACTIVE=$(grep -c '| todo |' docs/backlog.md 2>/dev/null ||
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
ACTIVE=$(grep -c '| todo |' docs/backlog.md 2>/dev/null || true)
|
|
53
|
+
: "${ACTIVE:=0}"
|
|
54
|
+
DEFERRED=$(grep -c '| deferred |' docs/backlog.md 2>/dev/null || true)
|
|
55
|
+
: "${DEFERRED:=0}"
|
|
56
|
+
DONE=$(grep -c '| done |' docs/backlog.md 2>/dev/null || true)
|
|
57
|
+
: "${DONE:=0}"
|
|
55
58
|
echo "## Backlog" >> "$SHARED"
|
|
56
59
|
echo "- Active: $ACTIVE | Deferred: $DEFERRED | Done: $DONE" >> "$SHARED"
|
|
57
60
|
# List active items
|
package/install.sh
CHANGED
|
@@ -7,7 +7,14 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
7
7
|
if [ -f ".env" ]; then
|
|
8
8
|
while IFS='=' read -r key value; do
|
|
9
9
|
[[ -z "$key" || "$key" == \#* ]] && continue
|
|
10
|
+
# Strip double quotes
|
|
10
11
|
value="${value%\"}" && value="${value#\"}"
|
|
12
|
+
# Strip single quotes
|
|
13
|
+
value="${value%'}" && value="${value#'}"
|
|
14
|
+
# Strip inline comments (space + # marks start of comment)
|
|
15
|
+
value="$(printf '%s\n' "$value" | sed 's/[[:space:]]#.*$//')"
|
|
16
|
+
# Strip trailing whitespace
|
|
17
|
+
value="${value%"${value##*[! ]}"}"
|
|
11
18
|
export "$key=$value" 2>/dev/null || true
|
|
12
19
|
done < .env
|
|
13
20
|
fi
|
|
@@ -20,6 +27,15 @@ get_version() {
|
|
|
20
27
|
grep -m1 '## \[' "$SCRIPT_DIR/CHANGELOG.md" | sed 's/.*\[\([^]]*\)\].*/\1/'
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
# Portable sed -i (GNU vs BSD)
|
|
31
|
+
sed_inplace() {
|
|
32
|
+
if sed --version 2>/dev/null | grep -q GNU; then
|
|
33
|
+
sed -i "$@"
|
|
34
|
+
else
|
|
35
|
+
sed -i '' "$@"
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
compute_checksum() {
|
|
24
40
|
local file="$1"
|
|
25
41
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
@@ -106,6 +122,7 @@ print_usage() {
|
|
|
106
122
|
echo " --dry-run Simulate install/update without writing files"
|
|
107
123
|
echo " --test-hooks Dry-run all hooks with synthetic inputs"
|
|
108
124
|
echo " --no-scan Skip post-install security audit"
|
|
125
|
+
echo " --version, -v Show CortexHawk version"
|
|
109
126
|
echo " --help, -h Show this help"
|
|
110
127
|
}
|
|
111
128
|
|
|
@@ -149,10 +166,18 @@ MAX_SNAPSHOTS=10
|
|
|
149
166
|
while [ $# -gt 0 ]; do
|
|
150
167
|
case "$1" in
|
|
151
168
|
--target)
|
|
169
|
+
if [ -z "${2:-}" ]; then
|
|
170
|
+
echo "Error: --target requires a value (claude|kimi|codex|auto|all)"
|
|
171
|
+
exit 1
|
|
172
|
+
fi
|
|
152
173
|
TARGET_CLI="$2"
|
|
153
174
|
shift 2
|
|
154
175
|
;;
|
|
155
176
|
--profile)
|
|
177
|
+
if [ -z "${2:-}" ]; then
|
|
178
|
+
echo "Error: --profile requires a value (fullstack|api|data)"
|
|
179
|
+
exit 1
|
|
180
|
+
fi
|
|
156
181
|
PROFILE="$2"
|
|
157
182
|
shift 2
|
|
158
183
|
;;
|
|
@@ -218,6 +243,10 @@ while [ $# -gt 0 ]; do
|
|
|
218
243
|
;;
|
|
219
244
|
--restore)
|
|
220
245
|
RESTORE_MODE=true
|
|
246
|
+
if [ -z "${2:-}" ]; then
|
|
247
|
+
echo "Error: --restore requires a snapshot path or --latest"
|
|
248
|
+
exit 1
|
|
249
|
+
fi
|
|
221
250
|
SNAPSHOT_FILE="$2"
|
|
222
251
|
if [ "$SNAPSHOT_FILE" = "--latest" ]; then
|
|
223
252
|
if [ "$GLOBAL" = true ]; then
|
|
@@ -323,6 +352,10 @@ while [ $# -gt 0 ]; do
|
|
|
323
352
|
fi
|
|
324
353
|
shift 2
|
|
325
354
|
;;
|
|
355
|
+
--version|-v)
|
|
356
|
+
echo "CortexHawk $(get_version)"
|
|
357
|
+
exit 0
|
|
358
|
+
;;
|
|
326
359
|
--help|-h)
|
|
327
360
|
print_usage
|
|
328
361
|
exit 0
|
|
@@ -368,14 +401,6 @@ if [ "$RESTORE_MODE" = true ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" =
|
|
|
368
401
|
echo "Error: --restore cannot be combined with --init or --update"
|
|
369
402
|
exit 1
|
|
370
403
|
fi
|
|
371
|
-
if [ "$TARGET_CLI" = "all" ] && [ "$INIT_MODE" = true ]; then
|
|
372
|
-
echo "Error: --target all cannot be combined with --init (run --init per target instead)"
|
|
373
|
-
exit 1
|
|
374
|
-
fi
|
|
375
|
-
if [ "$TARGET_CLI" = "auto" ] && [ "$INIT_MODE" = true ]; then
|
|
376
|
-
echo "Error: --target auto cannot be combined with --init (run --init per target instead)"
|
|
377
|
-
exit 1
|
|
378
|
-
fi
|
|
379
404
|
if [ -n "$ADD_SKILL_URL" ] && { [ "$INIT_MODE" = true ] || [ "$UPDATE_MODE" = true ] || [ "$SNAPSHOT_MODE" = true ] || [ "$RESTORE_MODE" = true ]; }; then
|
|
380
405
|
echo "Error: --add-skill cannot be combined with --init, --update, --snapshot, or --restore"
|
|
381
406
|
exit 1
|
|
@@ -486,6 +511,10 @@ copy_skills() {
|
|
|
486
511
|
grep '"[a-z]' "$PROFILE_FILE" | sed 's/.*"\([a-z][^"]*\)".*/\1/' | grep '/' | while read -r skill; do
|
|
487
512
|
local src="$SCRIPT_DIR/skills/$skill"
|
|
488
513
|
local dest="$target_dir/skills/$skill"
|
|
514
|
+
if [ ! -d "$src" ]; then
|
|
515
|
+
yellow " Warning: skill '$skill' not found in source — skipping"
|
|
516
|
+
continue
|
|
517
|
+
fi
|
|
489
518
|
mkdir -p "$(dirname "$dest")"
|
|
490
519
|
cp -r "$src" "$dest" 2>/dev/null || true
|
|
491
520
|
done
|
|
@@ -594,11 +623,34 @@ update_gitignore() {
|
|
|
594
623
|
touch "$gitignore"
|
|
595
624
|
fi
|
|
596
625
|
|
|
597
|
-
#
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
626
|
+
# Ensure essential entries are present (disable glob to keep *.log literal)
|
|
627
|
+
local essentials="node_modules/ __pycache__/ .env .env.local *.log dist/ build/ .DS_Store"
|
|
628
|
+
local added_count=0
|
|
629
|
+
set -f
|
|
630
|
+
for entry in $essentials; do
|
|
631
|
+
if ! grep -qxF "$entry" "$gitignore" 2>/dev/null; then
|
|
632
|
+
echo "$entry" >> "$gitignore"
|
|
633
|
+
added_count=$((added_count + 1))
|
|
634
|
+
fi
|
|
635
|
+
done
|
|
636
|
+
set +f
|
|
637
|
+
if [ "$added_count" -gt 0 ]; then
|
|
638
|
+
green " Added $added_count essential entries to .gitignore (.env, node_modules/, etc.)"
|
|
639
|
+
fi
|
|
640
|
+
|
|
641
|
+
# Auto-add target directory (always), group under single header
|
|
642
|
+
if ! grep -qxF "$target_dir/" "$gitignore" 2>/dev/null; then
|
|
643
|
+
if grep -q "# CortexHawk" "$gitignore" 2>/dev/null; then
|
|
644
|
+
# Append under the first existing header only
|
|
645
|
+
local header_line
|
|
646
|
+
header_line=$(grep -n "# CortexHawk" "$gitignore" | head -1 | cut -d: -f1)
|
|
647
|
+
sed_inplace "${header_line}a\\
|
|
648
|
+
$target_dir/" "$gitignore"
|
|
649
|
+
else
|
|
650
|
+
echo "" >> "$gitignore"
|
|
651
|
+
echo "# CortexHawk" >> "$gitignore"
|
|
652
|
+
echo "$target_dir/" >> "$gitignore"
|
|
653
|
+
fi
|
|
602
654
|
green " Added $target_dir/ to .gitignore"
|
|
603
655
|
fi
|
|
604
656
|
|
|
@@ -619,6 +671,52 @@ update_gitignore() {
|
|
|
619
671
|
fi
|
|
620
672
|
}
|
|
621
673
|
|
|
674
|
+
# --- setup_templates() ---
|
|
675
|
+
# Detects existing PR/commit templates; generates CortexHawk defaults if missing
|
|
676
|
+
# Args: $1=project_root
|
|
677
|
+
setup_templates() {
|
|
678
|
+
local project_root="$1"
|
|
679
|
+
|
|
680
|
+
[ "$GLOBAL" = true ] && return
|
|
681
|
+
|
|
682
|
+
# PR template
|
|
683
|
+
local pr_template=""
|
|
684
|
+
for path in ".github/PULL_REQUEST_TEMPLATE.md" ".github/pull_request_template.md" "docs/pull_request_template.md"; do
|
|
685
|
+
if [ -f "$project_root/$path" ]; then
|
|
686
|
+
pr_template="$path"
|
|
687
|
+
break
|
|
688
|
+
fi
|
|
689
|
+
done
|
|
690
|
+
# Also check template directory
|
|
691
|
+
if [ -z "$pr_template" ] && [ -d "$project_root/.github/PULL_REQUEST_TEMPLATE" ]; then
|
|
692
|
+
pr_template=".github/PULL_REQUEST_TEMPLATE/"
|
|
693
|
+
fi
|
|
694
|
+
|
|
695
|
+
if [ -n "$pr_template" ]; then
|
|
696
|
+
green " PR template found: $pr_template"
|
|
697
|
+
else
|
|
698
|
+
mkdir -p "$project_root/.github"
|
|
699
|
+
cp "$SCRIPT_DIR/templates/github/PULL_REQUEST_TEMPLATE.md" "$project_root/.github/PULL_REQUEST_TEMPLATE.md"
|
|
700
|
+
green " PR template created: .github/PULL_REQUEST_TEMPLATE.md"
|
|
701
|
+
fi
|
|
702
|
+
|
|
703
|
+
# Commit template
|
|
704
|
+
local commit_template=""
|
|
705
|
+
for path in ".gitmessage" ".github/gitmessage" ".github/commit-template"; do
|
|
706
|
+
if [ -f "$project_root/$path" ]; then
|
|
707
|
+
commit_template="$path"
|
|
708
|
+
break
|
|
709
|
+
fi
|
|
710
|
+
done
|
|
711
|
+
|
|
712
|
+
if [ -n "$commit_template" ]; then
|
|
713
|
+
green " Commit template found: $commit_template"
|
|
714
|
+
else
|
|
715
|
+
cp "$SCRIPT_DIR/templates/github/gitmessage" "$project_root/.gitmessage"
|
|
716
|
+
green " Commit template created: .gitmessage"
|
|
717
|
+
fi
|
|
718
|
+
}
|
|
719
|
+
|
|
622
720
|
# --- run_audit() ---
|
|
623
721
|
run_audit() {
|
|
624
722
|
local project_root="$1"
|
|
@@ -641,11 +739,12 @@ generate_hooks_config() {
|
|
|
641
739
|
fi
|
|
642
740
|
|
|
643
741
|
if ! command -v python3 >/dev/null 2>&1; then
|
|
742
|
+
yellow " Warning: python3 not found — hooks config will use static fallback" >&2
|
|
644
743
|
return 1
|
|
645
744
|
fi
|
|
646
745
|
|
|
647
|
-
python3 << PYEOF
|
|
648
|
-
import re, json, sys
|
|
746
|
+
python3 - "$compose_file" "$hooks_dir" << 'PYEOF'
|
|
747
|
+
import re, json, sys, shlex
|
|
649
748
|
|
|
650
749
|
def parse_compose(path, hooks_dir):
|
|
651
750
|
"""Parse compose.yml and generate Claude Code hooks JSON."""
|
|
@@ -679,10 +778,13 @@ def parse_compose(path, hooks_dir):
|
|
|
679
778
|
# Hook item
|
|
680
779
|
elif line.strip().startswith('- '):
|
|
681
780
|
hook_name = line.strip()[2:].strip()
|
|
781
|
+
if not re.match(r'^[a-zA-Z0-9_-]+$', hook_name):
|
|
782
|
+
print(f"Warning: skipping invalid hook name: {hook_name}", file=sys.stderr)
|
|
783
|
+
continue
|
|
682
784
|
hook_path = f"{hooks_dir}/{hook_name}.sh"
|
|
683
785
|
|
|
684
786
|
# Build command — hooks read stdin JSON (Claude Code protocol)
|
|
685
|
-
cmd = f'bash {hook_path}'
|
|
787
|
+
cmd = f'bash {shlex.quote(hook_path)}'
|
|
686
788
|
|
|
687
789
|
hook_entry = {"type": "command", "command": cmd}
|
|
688
790
|
|
|
@@ -708,7 +810,7 @@ def parse_compose(path, hooks_dir):
|
|
|
708
810
|
return result
|
|
709
811
|
|
|
710
812
|
try:
|
|
711
|
-
result = parse_compose(
|
|
813
|
+
result = parse_compose(sys.argv[1], sys.argv[2])
|
|
712
814
|
print(json.dumps(result, indent=2))
|
|
713
815
|
except Exception as e:
|
|
714
816
|
print(f"Error: {e}", file=sys.stderr)
|
|
@@ -1176,30 +1278,40 @@ do_update() {
|
|
|
1176
1278
|
# Make hooks executable
|
|
1177
1279
|
chmod +x "$TARGET/hooks/"*.sh 2>/dev/null || true
|
|
1178
1280
|
|
|
1179
|
-
# 7b. Regenerate settings.json hooks
|
|
1281
|
+
# 7b. Regenerate settings.json hooks + merge new permissions from compose.yml
|
|
1180
1282
|
if [ -f "$SCRIPT_DIR/hooks/compose.yml" ]; then
|
|
1181
1283
|
local hooks_json
|
|
1182
1284
|
hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
|
|
1183
|
-
if [ -n "$hooks_json" ]; then
|
|
1184
|
-
python3
|
|
1285
|
+
if [ -n "$hooks_json" ] && [ "$hooks_json" != "{}" ]; then
|
|
1286
|
+
echo "$hooks_json" | python3 -c "
|
|
1185
1287
|
import json, sys
|
|
1186
|
-
|
|
1187
|
-
# Read current settings.json to preserve permissions
|
|
1288
|
+
hooks = json.load(sys.stdin)
|
|
1188
1289
|
current = {}
|
|
1189
1290
|
try:
|
|
1190
|
-
with open(
|
|
1291
|
+
with open(sys.argv[1]) as f:
|
|
1191
1292
|
current = json.load(f)
|
|
1192
1293
|
except:
|
|
1193
1294
|
pass
|
|
1194
|
-
|
|
1195
|
-
# Merge: keep existing permissions, replace hooks
|
|
1196
|
-
hooks = json.loads('''$hooks_json''')
|
|
1197
1295
|
current['hooks'] = hooks
|
|
1198
|
-
|
|
1199
|
-
|
|
1296
|
+
# Merge new permissions from source
|
|
1297
|
+
try:
|
|
1298
|
+
with open(sys.argv[2]) as f:
|
|
1299
|
+
src_perms = json.load(f).get('permissions', {})
|
|
1300
|
+
cur_perms = current.get('permissions', {})
|
|
1301
|
+
for key in ('allow', 'deny'):
|
|
1302
|
+
src_list = src_perms.get(key, [])
|
|
1303
|
+
cur_list = cur_perms.get(key, [])
|
|
1304
|
+
added = [p for p in src_list if p not in cur_list]
|
|
1305
|
+
if added:
|
|
1306
|
+
cur_list.extend(added)
|
|
1307
|
+
cur_perms[key] = cur_list
|
|
1308
|
+
current['permissions'] = cur_perms
|
|
1309
|
+
except:
|
|
1310
|
+
pass
|
|
1311
|
+
with open(sys.argv[1], 'w') as f:
|
|
1200
1312
|
json.dump(current, f, indent=2)
|
|
1201
1313
|
f.write('\n')
|
|
1202
|
-
|
|
1314
|
+
" "$TARGET/settings.json" "$SCRIPT_DIR/settings.json"
|
|
1203
1315
|
echo " Regenerated settings.json hooks from compose.yml"
|
|
1204
1316
|
fi
|
|
1205
1317
|
fi
|
|
@@ -1214,6 +1326,7 @@ PYEOF
|
|
|
1214
1326
|
local target_dir_name=".${TARGET_CLI:-claude}"
|
|
1215
1327
|
update_gitignore "$(dirname "$TARGET")" "$target_dir_name"
|
|
1216
1328
|
[ "$TARGET_CLI" = "codex" ] && update_gitignore "$(dirname "$TARGET")" ".agents"
|
|
1329
|
+
setup_templates "$(dirname "$TARGET")"
|
|
1217
1330
|
|
|
1218
1331
|
if [ ! -f "$TARGET/git-workflow.conf" ]; then
|
|
1219
1332
|
GIT_BRANCHING="direct-main"
|
|
@@ -1313,12 +1426,10 @@ do_snapshot() {
|
|
|
1313
1426
|
git_push=$(grep '^AUTO_PUSH=' "$TARGET/git-workflow.conf" | cut -d= -f2)
|
|
1314
1427
|
fi
|
|
1315
1428
|
|
|
1316
|
-
# Read custom profile if applicable
|
|
1429
|
+
# Read custom profile if applicable (use PROFILE_FILE from current run, never glob /tmp/)
|
|
1317
1430
|
local profile_def="null"
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
if [ -n "$custom_profile" ] && [ -f "$custom_profile" ]; then
|
|
1321
|
-
profile_def=$(cat "$custom_profile")
|
|
1431
|
+
if [ -n "$PROFILE_FILE" ] && [ -f "$PROFILE_FILE" ]; then
|
|
1432
|
+
profile_def=$(cat "$PROFILE_FILE")
|
|
1322
1433
|
fi
|
|
1323
1434
|
|
|
1324
1435
|
# Build files checksums from manifest
|
|
@@ -1369,7 +1480,7 @@ do_snapshot() {
|
|
|
1369
1480
|
[ -n "$git_workflow_content" ] && printf ' "git-workflow.conf": "%s",\n' "$git_workflow_content" >> "$snap_file"
|
|
1370
1481
|
[ -n "$claude_md_content" ] && printf ' "CLAUDE.md": "%s",\n' "$claude_md_content" >> "$snap_file"
|
|
1371
1482
|
# Remove trailing comma from last entry
|
|
1372
|
-
|
|
1483
|
+
sed_inplace '$ s/,$//' "$snap_file"
|
|
1373
1484
|
printf ' }\n' >> "$snap_file"
|
|
1374
1485
|
printf '}\n' >> "$snap_file"
|
|
1375
1486
|
|
|
@@ -1875,15 +1986,15 @@ do_restore() {
|
|
|
1875
1986
|
if command -v python3 >/dev/null 2>&1; then
|
|
1876
1987
|
python3 -c "
|
|
1877
1988
|
import json, sys
|
|
1878
|
-
with open(
|
|
1989
|
+
with open(sys.argv[1]) as f:
|
|
1879
1990
|
snap = json.load(f)
|
|
1880
1991
|
settings = snap.get('settings')
|
|
1881
1992
|
if settings is not None:
|
|
1882
|
-
with open(
|
|
1993
|
+
with open(sys.argv[2], 'w') as f:
|
|
1883
1994
|
json.dump(settings, f, indent=2)
|
|
1884
1995
|
f.write('\n')
|
|
1885
1996
|
print(' Restored settings.json')
|
|
1886
|
-
"
|
|
1997
|
+
" "$snap_file" "$TARGET/settings.json"
|
|
1887
1998
|
else
|
|
1888
1999
|
echo " Warning: python3 not found — settings.json not restored from snapshot"
|
|
1889
2000
|
fi
|
|
@@ -1909,23 +2020,24 @@ if settings is not None:
|
|
|
1909
2020
|
if grep -q '"file_contents"' "$snap_file" && command -v python3 >/dev/null 2>&1; then
|
|
1910
2021
|
python3 -c "
|
|
1911
2022
|
import json, base64, sys, os
|
|
1912
|
-
|
|
2023
|
+
snap_file, target_dir = sys.argv[1], sys.argv[2]
|
|
2024
|
+
with open(snap_file) as f:
|
|
1913
2025
|
snap = json.load(f)
|
|
1914
2026
|
contents = snap.get('file_contents', {})
|
|
1915
2027
|
for filename, b64data in contents.items():
|
|
1916
2028
|
try:
|
|
1917
2029
|
data = base64.b64decode(b64data).decode('utf-8')
|
|
1918
2030
|
if filename == 'CLAUDE.md':
|
|
1919
|
-
target_path = os.path.dirname(
|
|
2031
|
+
target_path = os.path.dirname(target_dir) + '/CLAUDE.md'
|
|
1920
2032
|
else:
|
|
1921
|
-
target_path = '
|
|
2033
|
+
target_path = target_dir + '/' + filename
|
|
1922
2034
|
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
1923
2035
|
with open(target_path, 'w') as f:
|
|
1924
2036
|
f.write(data)
|
|
1925
2037
|
print(f' Restored {filename} (from file_contents)')
|
|
1926
2038
|
except Exception as e:
|
|
1927
2039
|
print(f' Warning: could not restore {filename}: {e}', file=sys.stderr)
|
|
1928
|
-
"
|
|
2040
|
+
" "$snap_file" "$TARGET"
|
|
1929
2041
|
fi
|
|
1930
2042
|
|
|
1931
2043
|
# Write new manifest
|
|
@@ -2312,14 +2424,14 @@ do_list_hooks() {
|
|
|
2312
2424
|
printf " %-20s %-14s %-8s %s\n" "Name" "Event" "Status" "Description"
|
|
2313
2425
|
printf " %-20s %-14s %-8s %s\n" "----" "-----" "------" "-----------"
|
|
2314
2426
|
# Parse hooks.json with python3 for reliable JSON handling
|
|
2315
|
-
python3 << PYEOF
|
|
2316
|
-
import json
|
|
2317
|
-
with open(
|
|
2427
|
+
python3 - "$hooks_json" "$compose_file" << 'PYEOF'
|
|
2428
|
+
import json, sys
|
|
2429
|
+
with open(sys.argv[1]) as f:
|
|
2318
2430
|
data = json.load(f)
|
|
2319
2431
|
# Read compose.yml to detect disabled hooks (commented out)
|
|
2320
2432
|
disabled = set()
|
|
2321
2433
|
try:
|
|
2322
|
-
with open(
|
|
2434
|
+
with open(sys.argv[2]) as f:
|
|
2323
2435
|
for line in f:
|
|
2324
2436
|
stripped = line.strip()
|
|
2325
2437
|
if stripped.startswith("# - "):
|
|
@@ -2368,14 +2480,14 @@ do_toggle_hook() {
|
|
|
2368
2480
|
echo "Hook '$hook_name' is already disabled"
|
|
2369
2481
|
return 0
|
|
2370
2482
|
fi
|
|
2371
|
-
|
|
2483
|
+
sed_inplace "s/^ - ${hook_name}$/ # - ${hook_name}/" "$compose_file"
|
|
2372
2484
|
echo "Disabled hook: $hook_name"
|
|
2373
2485
|
else
|
|
2374
2486
|
if grep -q "^ - ${hook_name}$" "$compose_file"; then
|
|
2375
2487
|
echo "Hook '$hook_name' is already enabled"
|
|
2376
2488
|
return 0
|
|
2377
2489
|
fi
|
|
2378
|
-
|
|
2490
|
+
sed_inplace "s/^ # - ${hook_name}$/ - ${hook_name}/" "$compose_file"
|
|
2379
2491
|
echo "Enabled hook: $hook_name"
|
|
2380
2492
|
fi
|
|
2381
2493
|
# Regenerate settings.json if target exists
|
|
@@ -2383,17 +2495,18 @@ do_toggle_hook() {
|
|
|
2383
2495
|
if [ -d "$target" ] && [ -f "$target/settings.json" ]; then
|
|
2384
2496
|
local hooks_json
|
|
2385
2497
|
hooks_json=$(generate_hooks_config "$compose_file" ".claude/hooks")
|
|
2386
|
-
if [ -n "$hooks_json" ]; then
|
|
2387
|
-
python3
|
|
2388
|
-
import json
|
|
2389
|
-
|
|
2498
|
+
if [ -n "$hooks_json" ] && [ "$hooks_json" != "{}" ]; then
|
|
2499
|
+
echo "$hooks_json" | python3 -c "
|
|
2500
|
+
import json, sys
|
|
2501
|
+
hooks = json.load(sys.stdin)
|
|
2502
|
+
with open(sys.argv[1]) as f:
|
|
2390
2503
|
settings = json.load(f)
|
|
2391
|
-
settings['hooks'] =
|
|
2392
|
-
with open(
|
|
2504
|
+
settings['hooks'] = hooks
|
|
2505
|
+
with open(sys.argv[1], 'w') as f:
|
|
2393
2506
|
json.dump(settings, f, indent=2)
|
|
2394
2507
|
f.write('\n')
|
|
2395
2508
|
print(' Regenerated settings.json')
|
|
2396
|
-
|
|
2509
|
+
" "$target/settings.json"
|
|
2397
2510
|
fi
|
|
2398
2511
|
fi
|
|
2399
2512
|
}
|
|
@@ -2441,17 +2554,18 @@ _search_skillsmp() {
|
|
|
2441
2554
|
fi
|
|
2442
2555
|
|
|
2443
2556
|
# Parse JSON response with python3
|
|
2444
|
-
python3 << PYEOF
|
|
2557
|
+
python3 - "$tmp_response" "$keyword" << 'PYEOF'
|
|
2445
2558
|
import json, sys
|
|
2446
2559
|
try:
|
|
2447
|
-
|
|
2560
|
+
response_file, keyword = sys.argv[1], sys.argv[2]
|
|
2561
|
+
with open(response_file) as f:
|
|
2448
2562
|
data = json.load(f)
|
|
2449
2563
|
container = data.get("data", {})
|
|
2450
2564
|
skills = container.get("skills", [])
|
|
2451
2565
|
pagination = container.get("pagination", {})
|
|
2452
2566
|
total = pagination.get("total", len(skills))
|
|
2453
2567
|
if not skills:
|
|
2454
|
-
print(" No skills found matching '
|
|
2568
|
+
print(f" No skills found matching '{keyword}'")
|
|
2455
2569
|
print("")
|
|
2456
2570
|
print(" Try broader terms or check https://skillsmp.com")
|
|
2457
2571
|
sys.exit(0)
|
|
@@ -2474,7 +2588,7 @@ try:
|
|
|
2474
2588
|
if len(parts) >= 2:
|
|
2475
2589
|
print(f" Install: ./install.sh --add-skill {parts[0]}/{parts[1]}")
|
|
2476
2590
|
break
|
|
2477
|
-
print(f" Browse all: https://skillsmp.com/?q
|
|
2591
|
+
print(f" Browse all: https://skillsmp.com/?q={keyword}")
|
|
2478
2592
|
except Exception as e:
|
|
2479
2593
|
print(f" Error parsing response: {e}")
|
|
2480
2594
|
print(" Falling back to local registry")
|
|
@@ -2841,26 +2955,78 @@ install_claude() {
|
|
|
2841
2955
|
cp -r "$SCRIPT_DIR/modes/"* "$TARGET/modes/" 2>/dev/null || true
|
|
2842
2956
|
cp -r "$SCRIPT_DIR/mcp/"* "$TARGET/mcp/" 2>/dev/null || true
|
|
2843
2957
|
|
|
2958
|
+
local hooks_json
|
|
2959
|
+
hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
|
|
2844
2960
|
if [ ! -f "$TARGET/settings.json" ]; then
|
|
2845
|
-
#
|
|
2846
|
-
local hooks_json
|
|
2847
|
-
hooks_json=$(generate_hooks_config "$SCRIPT_DIR/hooks/compose.yml" ".claude/hooks")
|
|
2961
|
+
# Fresh install: generate settings.json from scratch
|
|
2848
2962
|
if [ -n "$hooks_json" ] && [ "$hooks_json" != "{}" ]; then
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
with open(
|
|
2963
|
+
echo "$hooks_json" | python3 -c "
|
|
2964
|
+
import json, sys
|
|
2965
|
+
hooks = json.load(sys.stdin)
|
|
2966
|
+
with open(sys.argv[1]) as f:
|
|
2967
|
+
permissions = json.load(f).get('permissions', {})
|
|
2968
|
+
with open(sys.argv[2], 'w') as f:
|
|
2855
2969
|
json.dump({'permissions': permissions, 'hooks': hooks}, f, indent=2)
|
|
2856
2970
|
f.write('\n')
|
|
2857
|
-
"
|
|
2971
|
+
" "$SCRIPT_DIR/settings.json" "$TARGET/settings.json"
|
|
2858
2972
|
echo " Generated settings.json from hooks/compose.yml"
|
|
2859
2973
|
else
|
|
2860
2974
|
cp "$SCRIPT_DIR/settings.json" "$TARGET/settings.json"
|
|
2861
2975
|
fi
|
|
2862
2976
|
else
|
|
2863
|
-
|
|
2977
|
+
# Merge: preserve user customizations, add new hooks + permissions
|
|
2978
|
+
python3 -c "
|
|
2979
|
+
import json, sys, shutil, os
|
|
2980
|
+
|
|
2981
|
+
raw = sys.stdin.read().strip()
|
|
2982
|
+
hooks = json.loads(raw) if raw else {}
|
|
2983
|
+
|
|
2984
|
+
# Load current settings (tolerate corrupted JSON)
|
|
2985
|
+
try:
|
|
2986
|
+
with open(sys.argv[1]) as f:
|
|
2987
|
+
current = json.load(f)
|
|
2988
|
+
except Exception:
|
|
2989
|
+
backup = sys.argv[1] + '.bak'
|
|
2990
|
+
if os.path.isfile(sys.argv[1]):
|
|
2991
|
+
shutil.copy2(sys.argv[1], backup)
|
|
2992
|
+
print(f' Warning: settings.json corrupted — backed up to {os.path.basename(backup)}', file=sys.stderr)
|
|
2993
|
+
current = {}
|
|
2994
|
+
|
|
2995
|
+
try:
|
|
2996
|
+
with open(sys.argv[2]) as f:
|
|
2997
|
+
source = json.load(f)
|
|
2998
|
+
except Exception:
|
|
2999
|
+
source = {}
|
|
3000
|
+
|
|
3001
|
+
changes = []
|
|
3002
|
+
|
|
3003
|
+
# Merge hooks (regenerate from compose.yml)
|
|
3004
|
+
if hooks and hooks != {}:
|
|
3005
|
+
current['hooks'] = hooks
|
|
3006
|
+
changes.append('hooks regenerated')
|
|
3007
|
+
|
|
3008
|
+
# Merge permissions (union: keep user additions + add new from source)
|
|
3009
|
+
src_perms = source.get('permissions', {})
|
|
3010
|
+
cur_perms = current.get('permissions', {})
|
|
3011
|
+
for key in ('allow', 'deny'):
|
|
3012
|
+
src_list = src_perms.get(key, [])
|
|
3013
|
+
cur_list = cur_perms.get(key, [])
|
|
3014
|
+
added = [p for p in src_list if p not in cur_list]
|
|
3015
|
+
if added:
|
|
3016
|
+
cur_list.extend(added)
|
|
3017
|
+
changes.append(f'{len(added)} new {key} permission(s)')
|
|
3018
|
+
cur_perms[key] = cur_list
|
|
3019
|
+
current['permissions'] = cur_perms
|
|
3020
|
+
|
|
3021
|
+
with open(sys.argv[1], 'w') as f:
|
|
3022
|
+
json.dump(current, f, indent=2)
|
|
3023
|
+
f.write('\n')
|
|
3024
|
+
|
|
3025
|
+
if changes:
|
|
3026
|
+
print(' Merged settings.json: ' + ', '.join(changes))
|
|
3027
|
+
else:
|
|
3028
|
+
print(' settings.json up to date — no merge needed')
|
|
3029
|
+
" "$TARGET/settings.json" "$SCRIPT_DIR/settings.json" <<< "${hooks_json:-}"
|
|
2864
3030
|
fi
|
|
2865
3031
|
|
|
2866
3032
|
PROJECT_ROOT="$(dirname "$TARGET")"
|
|
@@ -2897,11 +3063,12 @@ with open('$TARGET/settings.json', 'w') as f:
|
|
|
2897
3063
|
|
|
2898
3064
|
run_audit "$(dirname "$TARGET")"
|
|
2899
3065
|
update_gitignore "$(dirname "$TARGET")" ".claude"
|
|
3066
|
+
setup_templates "$(dirname "$TARGET")"
|
|
2900
3067
|
|
|
2901
3068
|
echo ""
|
|
2902
3069
|
echo "CortexHawk installed successfully for Claude Code!"
|
|
2903
3070
|
echo ""
|
|
2904
|
-
echo "
|
|
3071
|
+
echo " 33 commands | 20 agents | 36 skills | 9 hooks | 7 modes"
|
|
2905
3072
|
echo ""
|
|
2906
3073
|
do_quickstart
|
|
2907
3074
|
echo ""
|
|
@@ -3749,7 +3916,6 @@ else
|
|
|
3749
3916
|
install_codex
|
|
3750
3917
|
;;
|
|
3751
3918
|
auto)
|
|
3752
|
-
local detected
|
|
3753
3919
|
detected=$(detect_installed_clis)
|
|
3754
3920
|
if [ -z "$detected" ]; then
|
|
3755
3921
|
echo "Error: no supported CLI found (claude, kimi, codex)"
|
|
@@ -3758,7 +3924,7 @@ else
|
|
|
3758
3924
|
fi
|
|
3759
3925
|
echo "Auto-detected CLIs: $detected"
|
|
3760
3926
|
echo ""
|
|
3761
|
-
|
|
3927
|
+
auto_count=0
|
|
3762
3928
|
for cli in $detected; do
|
|
3763
3929
|
[ $auto_count -gt 0 ] && echo "" && echo "---" && echo ""
|
|
3764
3930
|
case "$cli" in
|
package/package.json
CHANGED
|
@@ -57,7 +57,11 @@ $SKILL_JSON
|
|
|
57
57
|
]
|
|
58
58
|
}
|
|
59
59
|
EOF
|
|
60
|
-
|
|
60
|
+
if [ -z "$SKILLS" ]; then
|
|
61
|
+
SKILL_COUNT=0
|
|
62
|
+
else
|
|
63
|
+
SKILL_COUNT=$(echo "$SKILLS" | tr ',' '\n' | wc -l | tr -d ' ')
|
|
64
|
+
fi
|
|
61
65
|
|
|
62
66
|
# --- Stack snapshot ---
|
|
63
67
|
if [ -d "docs/.context" ]; then
|
|
@@ -20,11 +20,13 @@ echo " 1) Claude Code (default) — fully supported"
|
|
|
20
20
|
echo " 2) Kimi CLI (experimental)"
|
|
21
21
|
echo " 3) Codex CLI (experimental)"
|
|
22
22
|
echo " 4) All (install for all CLIs simultaneously)"
|
|
23
|
-
|
|
23
|
+
echo " 5) Auto-detect (install for all CLIs found on system)"
|
|
24
|
+
read -r -p "Choice [1-5] (default: 1): " target_choice
|
|
24
25
|
case "$target_choice" in
|
|
25
26
|
2) TARGET_CLI="kimi" ;;
|
|
26
27
|
3) TARGET_CLI="codex" ;;
|
|
27
28
|
4) TARGET_CLI="all" ;;
|
|
29
|
+
5) TARGET_CLI="auto" ;;
|
|
28
30
|
*) TARGET_CLI="claude" ;;
|
|
29
31
|
esac
|
|
30
32
|
green " → $TARGET_CLI"
|
|
@@ -32,14 +34,19 @@ echo ""
|
|
|
32
34
|
|
|
33
35
|
# --- Step 2: Scope ---
|
|
34
36
|
bold "2. Install Scope"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
if [ "$TARGET_CLI" = "all" ] || [ "$TARGET_CLI" = "auto" ]; then
|
|
38
|
+
GLOBAL=false
|
|
39
|
+
green " → local (always local for multi-target install)"
|
|
40
|
+
else
|
|
41
|
+
echo " 1) Local project — install in current directory (default)"
|
|
42
|
+
echo " 2) Global — install in ~/.${TARGET_CLI}/ (shared across all projects)"
|
|
43
|
+
read -r -p "Choice [1-2] (default: 1): " scope_choice
|
|
44
|
+
case "$scope_choice" in
|
|
45
|
+
2) GLOBAL=true ;;
|
|
46
|
+
*) GLOBAL=false ;;
|
|
47
|
+
esac
|
|
48
|
+
green " → $([ "$GLOBAL" = true ] && echo "global" || echo "local")"
|
|
49
|
+
fi
|
|
43
50
|
echo ""
|
|
44
51
|
|
|
45
52
|
# --- Step 3: Skills ---
|
|
@@ -122,7 +129,7 @@ echo ""
|
|
|
122
129
|
read -r -p " API key (press Enter to skip): " SKILLSMP_KEY
|
|
123
130
|
if [ -n "$SKILLSMP_KEY" ]; then
|
|
124
131
|
if [ -f ".env" ] && grep -q '^SKILLSMP_API_KEY=' .env 2>/dev/null; then
|
|
125
|
-
|
|
132
|
+
sed_inplace "s/^SKILLSMP_API_KEY=.*/SKILLSMP_API_KEY=$SKILLSMP_KEY/" .env
|
|
126
133
|
else
|
|
127
134
|
echo "SKILLSMP_API_KEY=$SKILLSMP_KEY" >> .env
|
|
128
135
|
fi
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# refresh-context.sh — Regenerate docs/.context/_shared.md mid-session
|
|
3
|
+
# Called by agents after modifying backlog, completing tasks, or committing code
|
|
4
|
+
|
|
5
|
+
[ -d "docs/.context" ] && [ ! -L "docs/.context" ] || exit 0
|
|
6
|
+
|
|
7
|
+
SHARED="docs/.context/_shared.md"
|
|
8
|
+
|
|
9
|
+
echo "# Shared Context" > "$SHARED"
|
|
10
|
+
echo "" >> "$SHARED"
|
|
11
|
+
echo "_Auto-generated at $(date '+%Y-%m-%d %H:%M'). Last refresh: $(date '+%H:%M:%S')._" >> "$SHARED"
|
|
12
|
+
echo "" >> "$SHARED"
|
|
13
|
+
|
|
14
|
+
# Backlog summary
|
|
15
|
+
if [ -f "docs/backlog.md" ]; then
|
|
16
|
+
ACTIVE=$(grep -c '| todo |' docs/backlog.md 2>/dev/null || true)
|
|
17
|
+
: "${ACTIVE:=0}"
|
|
18
|
+
DEFERRED=$(grep -c '| deferred |' docs/backlog.md 2>/dev/null || true)
|
|
19
|
+
: "${DEFERRED:=0}"
|
|
20
|
+
DONE=$(grep -c '| done |' docs/backlog.md 2>/dev/null || true)
|
|
21
|
+
: "${DONE:=0}"
|
|
22
|
+
echo "## Backlog" >> "$SHARED"
|
|
23
|
+
echo "- Active: $ACTIVE | Deferred: $DEFERRED | Done: $DONE" >> "$SHARED"
|
|
24
|
+
grep '| todo |' docs/backlog.md >> "$SHARED" 2>/dev/null || true
|
|
25
|
+
echo "" >> "$SHARED"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Recent commits
|
|
29
|
+
if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
|
|
30
|
+
echo "## Recent Commits" >> "$SHARED"
|
|
31
|
+
git log --oneline -5 2>/dev/null >> "$SHARED" || true
|
|
32
|
+
echo "" >> "$SHARED"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# User context
|
|
36
|
+
if [ -f "docs/.context/_user.md" ]; then
|
|
37
|
+
cat "docs/.context/_user.md" >> "$SHARED"
|
|
38
|
+
echo "" >> "$SHARED"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Active warnings
|
|
42
|
+
echo "## Warnings" >> "$SHARED"
|
|
43
|
+
WARNINGS=0
|
|
44
|
+
if [ -f ".env.example" ] && [ ! -f ".env" ]; then
|
|
45
|
+
echo "- .env missing — copy from .env.example" >> "$SHARED"
|
|
46
|
+
WARNINGS=$((WARNINGS + 1))
|
|
47
|
+
fi
|
|
48
|
+
if [ "$WARNINGS" -eq 0 ]; then
|
|
49
|
+
echo "- None" >> "$SHARED"
|
|
50
|
+
fi
|
|
51
|
+
echo "" >> "$SHARED"
|