cortexhawk 3.1.0 → 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 +30 -0
- package/CLAUDE.md +2 -2
- package/README.md +31 -6
- package/agents/git-manager.md +4 -0
- package/commands/backlog.md +1 -0
- package/commands/chain.md +10 -1
- package/commands/commit.md +24 -0
- package/commands/pulse.md +11 -1
- package/commands/ship.md +1 -1
- package/commands/task.md +1 -0
- package/cortexhawk +90 -0
- package/hooks/branch-guard.sh +6 -1
- package/hooks/commit-guard.sh +16 -3
- package/hooks/file-guard.sh +40 -7
- 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,6 +3,33 @@
|
|
|
3
3
|
All notable changes to CortexHawk are documented here.
|
|
4
4
|
Format: [Keep a Changelog](https://keepachangelog.com/)
|
|
5
5
|
|
|
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
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- `branch-guard` / `commit-guard` hooks: JSON parsing via `jq` with regex fallback — fixes false "hook error" on commands containing quotes or HEREDOC
|
|
16
|
+
- `commit-guard`: multi-format commit message extraction (HEREDOC, single/double quotes)
|
|
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
|
|
32
|
+
|
|
6
33
|
## [3.1.0] - 2026-02-14
|
|
7
34
|
|
|
8
35
|
### Added
|
|
@@ -11,6 +38,9 @@ Format: [Keep a Changelog](https://keepachangelog.com/)
|
|
|
11
38
|
- `--target auto`: auto-detects installed CLIs (claude, kimi, codex) and installs for all found — no need to specify which CLIs are available
|
|
12
39
|
- `cortexhawk validate [path]`: post-install diagnostic that verifies skills/agents/commands/hooks discovery per target — checks manifest, file counts, settings.json validity, .gitignore coverage
|
|
13
40
|
- npm distribution: `npm install -g cortexhawk` installs the CLI globally — auto-resolves `CORTEXHAWK_HOME` from symlinked binary, `self-update` detects npm install and suggests `npm update -g`
|
|
41
|
+
- `/pulse --unused`: detects installed skills that don't match the project's tech stack — scans for technology markers, maps framework skills to stacks, reports potentially unused skills
|
|
42
|
+
- `/chain --browse`: lists all available chain presets (built-in + custom from `.cortexhawk-chains.yml`) and recent chain runs from `docs/chains/`
|
|
43
|
+
- `cortexhawk sync [path] [from]`: syncs modified skills across installed CLI targets — compares checksums, copies changed skills, skips generated content (cmd-*, hook-*, agent-*)
|
|
14
44
|
|
|
15
45
|
## [3.0.0] - 2026-02-14
|
|
16
46
|
|
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,27 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/Spechawk94/CortexHawk/stargazers)
|
|
4
4
|
[](LICENSE)
|
|
5
|
-
[](CHANGELOG.md)
|
|
6
|
+
[](https://www.npmjs.com/package/cortexhawk)
|
|
6
7
|
[](https://skillsmp.com)
|
|
7
|
-
[](#whats-inside)
|
|
8
9
|
|
|
9
10
|
An open-source, community-driven development toolkit for Claude Code.
|
|
10
11
|
|
|
11
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.
|
|
12
13
|
|
|
13
|
-
### 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>
|
|
26
|
+
|
|
27
|
+
- **`npm install -g cortexhawk`** — available on npm, auto-resolves source from symlinked binary
|
|
28
|
+
- **`cortexhawk` CLI wrapper** — clean subcommands (init, install, update, doctor, validate, search, snapshot, etc.) instead of `bash install.sh --flags`
|
|
29
|
+
- **`--target auto`** — auto-detects installed CLIs (claude, kimi, codex) and installs for all found
|
|
30
|
+
- **`cortexhawk validate`** — post-install diagnostic verifying skills/agents discovery per target
|
|
31
|
+
|
|
32
|
+
</details>
|
|
33
|
+
|
|
34
|
+
<details>
|
|
35
|
+
<summary>v3.0 changes</summary>
|
|
14
36
|
|
|
15
37
|
- **`--init` wizard overhaul** — reordered flow with git workflow config, auto `git init` + `.gitignore` creation, branching strategies (direct-main, dev-branch, git-flow)
|
|
16
38
|
- **`/bootstrap --smart`** — researcher agent scans roadmap, compares stacks, recommends, then scaffolds
|
|
17
39
|
- **Post-install gitignore** — auto-adds `.claude/`, `.kimi/`, `.codex/` to `.gitignore`, asks about `docs/`
|
|
18
40
|
- **`--update` checksum detection** — compares file checksums instead of just version numbers to detect changes
|
|
19
41
|
- **Kimi CLI overhaul** — agents, commands, hooks all converted to skills; AGENTS.md at project root; MCP optional; auto `.envrc` for local install
|
|
20
|
-
- **Codex CLI fixes** — config.toml format fixes (flat model key, save-all persistence)
|
|
21
42
|
- **`--demo` flag** — sandbox project in `/tmp/` with intentional bugs for testing
|
|
22
43
|
- **`/upgrade` command** — check for updates, show changelog diff, propose `--update`
|
|
23
44
|
- **`--publish-skill`** — publish local skills to GitHub with auto-generated README
|
|
24
45
|
- **Cursor CLI removed** — too unstable for maintained support
|
|
25
46
|
|
|
47
|
+
</details>
|
|
48
|
+
|
|
26
49
|
## Quick Start
|
|
27
50
|
|
|
28
51
|
```bash
|
|
@@ -93,7 +116,7 @@ Specialized AI agents that coordinate together instead of working in silos.
|
|
|
93
116
|
| `fullstack-developer` | Full-stack orchestration front+back |
|
|
94
117
|
| `teacher` | Teaches concepts with 3 pedagogical levels (guided, mentor, professor) |
|
|
95
118
|
|
|
96
|
-
### Commands (
|
|
119
|
+
### Commands (33)
|
|
97
120
|
|
|
98
121
|
Slash commands for common workflows.
|
|
99
122
|
|
|
@@ -104,6 +127,7 @@ Slash commands for common workflows.
|
|
|
104
127
|
| `/test` | Generate and run tests |
|
|
105
128
|
| `/review` | Multi-agent code review |
|
|
106
129
|
| `/ship` | Commit + PR pipeline |
|
|
130
|
+
| `/commit` | Lightweight commit + push (no review, no PR) |
|
|
107
131
|
| `/debug` | Debug and fix issues |
|
|
108
132
|
| `/scan` | Full security audit |
|
|
109
133
|
| `/check` | Pre-commit quality gate (lint + test + scan + review → GO/NO-GO) |
|
|
@@ -303,7 +327,7 @@ Each target adapts components to the CLI's native format:
|
|
|
303
327
|
| Component | Claude Code | Kimi CLI | Codex CLI |
|
|
304
328
|
|---|---|---|---|
|
|
305
329
|
| Agents (20) | `.claude/agents/*.md` | Skills (`/skill:agent-*`) + `AGENTS.md` | `AGENTS.md` |
|
|
306
|
-
| Commands (
|
|
330
|
+
| Commands (33) | `.claude/commands/*.md` → `/plan` | Skills (`/skill:cmd-*`) | Skills (`$cmd-*`) |
|
|
307
331
|
| Skills (36) | `.claude/skills/` | `.kimi/skills/` (auto-discovered) | `.agents/skills/` |
|
|
308
332
|
| Hooks (9) | `settings.json` (automatic) | Skills (`/skill:hook-*`, manual) | Dispatcher (partial) |
|
|
309
333
|
| Modes (7) | `.claude/modes/` (native) | Skills (`/skill:modes/*`) | Skills (`$mode-*`) |
|
|
@@ -325,6 +349,7 @@ Each target adapts components to the CLI's native format:
|
|
|
325
349
|
./install.sh --update --dry-run # preview update delta
|
|
326
350
|
|
|
327
351
|
# Diagnostics
|
|
352
|
+
./install.sh --version # show CortexHawk version
|
|
328
353
|
./install.sh --doctor # check installation health
|
|
329
354
|
./install.sh --test-hooks # dry-run all hooks with synthetic inputs
|
|
330
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
|
|
package/commands/chain.md
CHANGED
|
@@ -11,7 +11,7 @@ Execute a declared sequence of agents with automatic context passing. Topic: `$A
|
|
|
11
11
|
|
|
12
12
|
**Custom presets**: Define in `.cortexhawk-chains.yml` at project root — custom presets override built-in presets with the same name
|
|
13
13
|
|
|
14
|
-
**Flags**: `--gate` = pause between steps | `--copy` = physical copy to plans/ | `--replay <slug>` = re-run a previous chain
|
|
14
|
+
**Flags**: `--gate` = pause between steps | `--copy` = physical copy to plans/ | `--replay <slug>` = re-run a previous chain | `--browse` = list all available presets + recent chains
|
|
15
15
|
|
|
16
16
|
**Mapping**: plan=planner, build=implementer, test=tester, review=reviewer, scan=security-auditor, debug=debugger, doc=docs-manager, ship=git-manager, refactor=code-simplifier, research=researcher
|
|
17
17
|
|
|
@@ -38,6 +38,15 @@ When detected in a step's output:
|
|
|
38
38
|
2. Save as `{N}a-{delegated-agent}.md` (sub-step)
|
|
39
39
|
3. Feed original output + delegation result to the next step
|
|
40
40
|
|
|
41
|
+
## Browse mode (`--browse`)
|
|
42
|
+
|
|
43
|
+
List all available chains from 3 sources:
|
|
44
|
+
1. **Built-in presets** — default, security, ship (table: name, sequence, description)
|
|
45
|
+
2. **Custom presets** — read `.cortexhawk-chains.yml` if present (table: name, sequence, description, gate)
|
|
46
|
+
3. **Recent chains** — scan `docs/chains/*/SUMMARY.md`, extract date/slug/sequence/result (table: date, slug, sequence, status — last 10)
|
|
47
|
+
|
|
48
|
+
Output all 3 tables. End with usage hint: `/chain <preset> <topic>`
|
|
49
|
+
|
|
41
50
|
## Rules
|
|
42
51
|
|
|
43
52
|
- Max 8 agents per chain (delegations count toward this limit)
|
|
@@ -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/pulse.md
CHANGED
|
@@ -7,7 +7,7 @@ description: Project health dashboard — code quality metrics at a glance.
|
|
|
7
7
|
|
|
8
8
|
Activate the **project-manager** agent in health-check mode. Scope: `$ARGUMENTS`
|
|
9
9
|
|
|
10
|
-
**Flags**: `--history Nd` = show metrics trends over N days
|
|
10
|
+
**Flags**: `--history Nd` = show metrics trends over N days | `--unused` = detect potentially unused skills
|
|
11
11
|
|
|
12
12
|
## Standard mode (no flag)
|
|
13
13
|
|
|
@@ -44,3 +44,13 @@ Avg: [tokens/day] tokens/day, [duration] min/session
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
If scope is specified, limit checks to those files/directories.
|
|
47
|
+
|
|
48
|
+
## Unused skills mode (`--unused`)
|
|
49
|
+
|
|
50
|
+
Detect installed skills that don't match the project's tech stack.
|
|
51
|
+
|
|
52
|
+
1. **List installed skills** from `.claude/skills/` (or target equivalent)
|
|
53
|
+
2. **Detect stack** — scan for: `package.json`/`*.ts` → js/ts, `requirements.txt`/`*.py` → python, `go.mod` → go, `Cargo.toml` → rust, `Dockerfile` → docker, `*.svelte` → svelte, `next.config.*` → nextjs, `tailwind.config.*` → tailwind
|
|
54
|
+
3. **Map framework skills** — `frameworks/react` → js, `frameworks/nextjs` → nextjs, `frameworks/sveltekit` → svelte, `frameworks/fastapi` → python, `frameworks/python` → python, `frameworks/typescript` → ts, `frameworks/tailwindcss` → tailwind, `devops/docker` → docker
|
|
55
|
+
4. **Universal skills** (security/\*, quality/\*, testing/\*, meta/\*, databases/\*, workflow/\*, optimization/\*) → always relevant, skip
|
|
56
|
+
5. **Output** — table with skill, required stack, OK/UNUSED status. Count unused. Suggest `cortexhawk install --profile autodetect`
|
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/cortexhawk
CHANGED
|
@@ -250,6 +250,91 @@ do_validate() {
|
|
|
250
250
|
fi
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
# --- sync command ---
|
|
254
|
+
do_sync() {
|
|
255
|
+
local project_root="${1:-.}"
|
|
256
|
+
local from_target="${2:-}"
|
|
257
|
+
|
|
258
|
+
# Detect installed targets
|
|
259
|
+
local targets=()
|
|
260
|
+
[ -f "$project_root/.claude/.cortexhawk-manifest" ] && targets+=("claude")
|
|
261
|
+
[ -f "$project_root/.kimi/.cortexhawk-manifest" ] && targets+=("kimi")
|
|
262
|
+
[ -f "$project_root/.codex/.cortexhawk-manifest" ] && targets+=("codex")
|
|
263
|
+
|
|
264
|
+
if [ "${#targets[@]}" -lt 2 ]; then
|
|
265
|
+
yellow "Sync requires at least 2 targets installed. Found: ${targets[*]:-none}"
|
|
266
|
+
exit 1
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
# Determine source target
|
|
270
|
+
if [ -z "$from_target" ]; then
|
|
271
|
+
from_target="${targets[0]}"
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
# Map target to skills directory
|
|
275
|
+
target_skills_dir() {
|
|
276
|
+
case "$1" in
|
|
277
|
+
claude) echo "$project_root/.claude/skills" ;;
|
|
278
|
+
kimi) echo "$project_root/.kimi/skills" ;;
|
|
279
|
+
codex) echo "$project_root/.agents/skills" ;;
|
|
280
|
+
esac
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
local src_dir
|
|
284
|
+
src_dir=$(target_skills_dir "$from_target")
|
|
285
|
+
if [ ! -d "$src_dir" ]; then
|
|
286
|
+
red "Source skills dir not found: $src_dir"
|
|
287
|
+
exit 1
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
echo "CortexHawk Sync"
|
|
291
|
+
echo "==============="
|
|
292
|
+
echo " Source: $from_target ($src_dir)"
|
|
293
|
+
echo " Targets: ${targets[*]}"
|
|
294
|
+
echo ""
|
|
295
|
+
|
|
296
|
+
local synced=0
|
|
297
|
+
for target in "${targets[@]}"; do
|
|
298
|
+
[ "$target" = "$from_target" ] && continue
|
|
299
|
+
local dst_dir
|
|
300
|
+
dst_dir=$(target_skills_dir "$target")
|
|
301
|
+
[ ! -d "$dst_dir" ] && continue
|
|
302
|
+
|
|
303
|
+
# Sync each skill category/name
|
|
304
|
+
for skill_file in "$src_dir"/*/SKILL.md "$src_dir"/*/*/SKILL.md; do
|
|
305
|
+
[ ! -f "$skill_file" ] && continue
|
|
306
|
+
local rel_path="${skill_file#"$src_dir/"}"
|
|
307
|
+
local dst_file="$dst_dir/$rel_path"
|
|
308
|
+
|
|
309
|
+
# Skip generated skills (cmd-*, hook-*, agent-*, modes/)
|
|
310
|
+
case "$rel_path" in
|
|
311
|
+
cmd-*|hook-*|agent-*|modes/*) continue ;;
|
|
312
|
+
esac
|
|
313
|
+
|
|
314
|
+
# Compare checksums
|
|
315
|
+
if [ -f "$dst_file" ]; then
|
|
316
|
+
local src_md5 dst_md5
|
|
317
|
+
src_md5=$(md5sum "$skill_file" 2>/dev/null | cut -d' ' -f1)
|
|
318
|
+
dst_md5=$(md5sum "$dst_file" 2>/dev/null | cut -d' ' -f1)
|
|
319
|
+
[ "$src_md5" = "$dst_md5" ] && continue
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
# Copy
|
|
323
|
+
mkdir -p "$(dirname "$dst_file")"
|
|
324
|
+
cp "$skill_file" "$dst_file"
|
|
325
|
+
echo " [SYNC] $rel_path → $target"
|
|
326
|
+
synced=$((synced + 1))
|
|
327
|
+
done
|
|
328
|
+
done
|
|
329
|
+
|
|
330
|
+
echo ""
|
|
331
|
+
if [ "$synced" -eq 0 ]; then
|
|
332
|
+
green "All targets already in sync."
|
|
333
|
+
else
|
|
334
|
+
green "Synced $synced file(s)."
|
|
335
|
+
fi
|
|
336
|
+
}
|
|
337
|
+
|
|
253
338
|
# Verify CortexHawk source exists
|
|
254
339
|
check_home() {
|
|
255
340
|
if [ ! -f "$INSTALL_SH" ]; then
|
|
@@ -276,6 +361,7 @@ show_help() {
|
|
|
276
361
|
echo ""
|
|
277
362
|
echo "Diagnostics:"
|
|
278
363
|
echo " validate [path] Verify skills/agents discovery per target"
|
|
364
|
+
echo " sync [path] [from] Sync skills across installed CLI targets"
|
|
279
365
|
echo " doctor Check installation health"
|
|
280
366
|
echo " stats Show installation overview"
|
|
281
367
|
echo " quickstart Getting-started guide"
|
|
@@ -336,6 +422,10 @@ case "$cmd" in
|
|
|
336
422
|
shift
|
|
337
423
|
do_validate "$@"
|
|
338
424
|
;;
|
|
425
|
+
sync)
|
|
426
|
+
shift
|
|
427
|
+
do_sync "$@"
|
|
428
|
+
;;
|
|
339
429
|
doctor)
|
|
340
430
|
check_home
|
|
341
431
|
shift
|
package/hooks/branch-guard.sh
CHANGED
|
@@ -7,7 +7,12 @@ if [ -n "$CORTEXHAWK_COMMAND" ]; then
|
|
|
7
7
|
CMD="$CORTEXHAWK_COMMAND"
|
|
8
8
|
else
|
|
9
9
|
INPUT=$(cat)
|
|
10
|
-
|
|
10
|
+
if command -v jq &>/dev/null; then
|
|
11
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
12
|
+
fi
|
|
13
|
+
if [[ -z "$CMD" ]]; then
|
|
14
|
+
CMD=$(printf '%s' "$INPUT" | grep -o '"command" *: *"[^"]*"' | head -1 | sed 's/.*: *"//;s/"$//')
|
|
15
|
+
fi
|
|
11
16
|
fi
|
|
12
17
|
|
|
13
18
|
if [[ -z "$CMD" ]]; then
|
package/hooks/commit-guard.sh
CHANGED
|
@@ -7,7 +7,12 @@ if [ -n "$CORTEXHAWK_COMMAND" ]; then
|
|
|
7
7
|
CMD="$CORTEXHAWK_COMMAND"
|
|
8
8
|
else
|
|
9
9
|
INPUT=$(cat)
|
|
10
|
-
|
|
10
|
+
if command -v jq &>/dev/null; then
|
|
11
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
12
|
+
fi
|
|
13
|
+
if [[ -z "$CMD" ]]; then
|
|
14
|
+
CMD=$(printf '%s' "$INPUT" | grep -o '"command" *: *"[^"]*"' | head -1 | sed 's/.*: *"//;s/"$//')
|
|
15
|
+
fi
|
|
11
16
|
fi
|
|
12
17
|
|
|
13
18
|
if [[ -z "$CMD" ]]; then
|
|
@@ -20,9 +25,17 @@ if ! echo "$CMD" | grep -qE 'git\s+commit'; then
|
|
|
20
25
|
fi
|
|
21
26
|
|
|
22
27
|
# Extract commit message from -m flag
|
|
23
|
-
|
|
28
|
+
# Handle: -m "msg", -m 'msg', -m "$(cat <<'EOF'\nmsg\nEOF\n)"
|
|
29
|
+
COMMIT_MSG=$(echo "$CMD" | sed -n 's/.*-m[[:space:]]*"\$(cat <<.*//p' 2>/dev/null)
|
|
30
|
+
if [[ -n "$COMMIT_MSG" ]]; then
|
|
31
|
+
# HEREDOC style — extract first non-empty line after the heredoc opener
|
|
32
|
+
COMMIT_MSG=$(echo "$CMD" | sed -n '/cat <</{n;p;}' | sed 's/^[[:space:]]*//' 2>/dev/null)
|
|
33
|
+
fi
|
|
34
|
+
if [[ -z "$COMMIT_MSG" ]]; then
|
|
35
|
+
COMMIT_MSG=$(echo "$CMD" | grep -oP "(?<=-m\s[\"'])(.+?)(?=[\"'](\s|$))" 2>/dev/null | head -1)
|
|
36
|
+
fi
|
|
24
37
|
if [[ -z "$COMMIT_MSG" ]]; then
|
|
25
|
-
COMMIT_MSG=$(echo "$CMD" | grep -oP '(?<=-m\s)
|
|
38
|
+
COMMIT_MSG=$(echo "$CMD" | grep -oP '(?<=-m\s)\S+' 2>/dev/null | head -1)
|
|
26
39
|
fi
|
|
27
40
|
|
|
28
41
|
# Load git workflow config — skip conventional check if freeform
|
package/hooks/file-guard.sh
CHANGED
|
@@ -2,20 +2,28 @@
|
|
|
2
2
|
# file-guard — Blocks Claude from accessing sensitive files
|
|
3
3
|
# Hook type: PreToolUse (Read|Edit|Write)
|
|
4
4
|
|
|
5
|
+
# Patterns matched against BASENAME only
|
|
5
6
|
BLOCKED_PATTERNS=(
|
|
6
7
|
".env"
|
|
7
|
-
".env.*"
|
|
8
8
|
"*.pem"
|
|
9
9
|
"*.key"
|
|
10
|
-
"*credentials*"
|
|
11
|
-
"*secret*"
|
|
12
10
|
"id_rsa"
|
|
13
11
|
"id_ed25519"
|
|
14
|
-
".ssh/*"
|
|
15
12
|
"*.p12"
|
|
16
13
|
"*.pfx"
|
|
17
14
|
"*.keystore"
|
|
18
|
-
"
|
|
15
|
+
"credentials.json"
|
|
16
|
+
"credentials.yml"
|
|
17
|
+
"credentials.yaml"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Basename patterns that are .env.* but NOT .env.example/.env.sample/.env.template
|
|
21
|
+
BLOCKED_ENV_GLOB=".env.*"
|
|
22
|
+
ALLOWED_ENV_NAMES=(".env.example" ".env.sample" ".env.template")
|
|
23
|
+
|
|
24
|
+
# Path patterns — only match directory components
|
|
25
|
+
BLOCKED_PATH_PATTERNS=(
|
|
26
|
+
".ssh/"
|
|
19
27
|
)
|
|
20
28
|
|
|
21
29
|
# Read file_path from stdin JSON (Claude Code hook protocol)
|
|
@@ -42,10 +50,35 @@ BASENAME=$(basename "$RESOLVED")
|
|
|
42
50
|
# Also check against the full resolved path
|
|
43
51
|
RELPATH="$RESOLVED"
|
|
44
52
|
|
|
53
|
+
# Check basename against exact patterns
|
|
45
54
|
for pattern in "${BLOCKED_PATTERNS[@]}"; do
|
|
46
|
-
if [[ "$BASENAME" == $pattern ]]
|
|
55
|
+
if [[ "$BASENAME" == $pattern ]]; then
|
|
56
|
+
echo "BLOCKED: Access to '$FILE_ARG' denied by file-guard" >&2
|
|
57
|
+
echo "Matched pattern: '$pattern'" >&2
|
|
58
|
+
exit 2
|
|
59
|
+
fi
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
# Check .env.* files (allow .env.example/.env.sample/.env.template)
|
|
63
|
+
if [[ "$BASENAME" == $BLOCKED_ENV_GLOB ]]; then
|
|
64
|
+
ALLOWED=false
|
|
65
|
+
for name in "${ALLOWED_ENV_NAMES[@]}"; do
|
|
66
|
+
if [[ "$BASENAME" == "$name" ]]; then
|
|
67
|
+
ALLOWED=true
|
|
68
|
+
break
|
|
69
|
+
fi
|
|
70
|
+
done
|
|
71
|
+
if [[ "$ALLOWED" == false ]]; then
|
|
72
|
+
echo "BLOCKED: Access to '$FILE_ARG' denied by file-guard" >&2
|
|
73
|
+
echo "Matched pattern: '$BLOCKED_ENV_GLOB'" >&2
|
|
74
|
+
exit 2
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# Check path patterns (directory components)
|
|
79
|
+
for pattern in "${BLOCKED_PATH_PATTERNS[@]}"; do
|
|
80
|
+
if [[ "$RELPATH" == *"$pattern"* ]]; then
|
|
47
81
|
echo "BLOCKED: Access to '$FILE_ARG' denied by file-guard" >&2
|
|
48
|
-
echo "Resolved path: $RESOLVED" >&2
|
|
49
82
|
echo "Matched pattern: '$pattern'" >&2
|
|
50
83
|
exit 2
|
|
51
84
|
fi
|
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"
|