@event4u/agent-config 1.38.0 → 1.40.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/.agent-src/commands/onboard.md +131 -50
- package/.agent-src/commands/orchestrate.md +123 -0
- package/.agent-src/commands/sync-gitignore/fix.md +135 -0
- package/.agent-src/commands/sync-gitignore.md +31 -5
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +30 -2
- package/.agent-src/skills/subagent-orchestration/SKILL.md +9 -0
- package/.agent-src/skills/using-git-worktrees/SKILL.md +25 -0
- package/.agent-src/templates/agent-settings.md +9 -0
- package/.agent-src/templates/agents/agent-project-settings.example.yml +9 -2
- package/.agent-src/templates/scripts/work_engine/_lib/__init__.py +7 -0
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +168 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +18 -19
- package/.agent-src/templates/scripts/work_engine/orchestration.py +168 -0
- package/.claude-plugin/marketplace.json +3 -1
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +76 -0
- package/README.md +17 -6
- package/bin/install.php +13 -6
- package/config/agent-settings.template.yml +21 -0
- package/docs/DISTRIBUTION_CHECKLIST.md +169 -0
- package/docs/architecture.md +1 -1
- package/docs/catalog.md +3 -2
- package/docs/contracts/audit-log-v1.md +142 -0
- package/docs/contracts/command-clusters.md +2 -0
- package/docs/contracts/file-ownership-matrix.json +20 -0
- package/docs/contracts/orchestration-dsl-v1.md +152 -0
- package/docs/customization.md +45 -0
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/agent-infra/layered-settings.md +54 -17
- package/docs/installation.md +132 -0
- package/docs/setup/mcp-client-config.md +152 -0
- package/docs/setup/mcp-cloud-endpoints.md +16 -0
- package/docs/setup/per-ide/aider.md +48 -0
- package/docs/setup/per-ide/claude-code.md +108 -0
- package/docs/setup/per-ide/claude-desktop.md +148 -0
- package/docs/setup/per-ide/cline.md +43 -0
- package/docs/setup/per-ide/codex.md +46 -0
- package/docs/setup/per-ide/copilot.md +80 -0
- package/docs/setup/per-ide/cursor.md +125 -0
- package/docs/setup/per-ide/gemini-cli.md +45 -0
- package/docs/setup/per-ide/windsurf.md +120 -0
- package/package.json +1 -1
- package/scripts/_lib/agent_settings.py +168 -0
- package/scripts/compress.py +153 -1
- package/scripts/extract_audit_patterns.py +202 -0
- package/scripts/install +156 -1
- package/scripts/install.py +270 -10
- package/scripts/install.sh +52 -7
- package/scripts/lint_orchestration_dsl.py +214 -0
- package/scripts/skill_linter.py +9 -0
- package/scripts/sync_gitignore.py +56 -1
- package/templates/claude_desktop_config.json.template +21 -0
- package/templates/cursor-rule.mdc.j2 +7 -0
- package/templates/global-install-manifest.yml +91 -0
- package/templates/marketing-copy.yml +64 -0
- package/templates/windsurf-rule.md.j2 +7 -0
package/scripts/install
CHANGED
|
@@ -15,13 +15,30 @@
|
|
|
15
15
|
# --source <dir> Package source directory (default: auto-detect)
|
|
16
16
|
# --target <dir> Target project root (default: cwd)
|
|
17
17
|
# --profile <name> Cost profile for bridges (minimal|balanced|full)
|
|
18
|
+
# --tools <list> Comma-separated tool IDs to install (default: all).
|
|
19
|
+
# Valid: claude-code,claude-desktop,cursor,windsurf,
|
|
20
|
+
# cline,gemini-cli,copilot,augment,aider,codex,all
|
|
21
|
+
# --list-tools Print supported tool IDs with descriptions, then exit
|
|
22
|
+
# --yes, -y Non-interactive mode: do not prompt (default for non-TTY)
|
|
18
23
|
# --force Overwrite existing bridge files
|
|
19
24
|
# --dry-run Show what payload sync would do (does not run bridges)
|
|
20
25
|
# --verbose Detailed payload sync output
|
|
21
26
|
# --quiet Suppress non-error output
|
|
22
27
|
# --skip-sync Skip payload sync (install.sh)
|
|
23
28
|
# --skip-bridges Skip bridge files (install.py)
|
|
29
|
+
# --global Phase-3: ship kernel rules + curated skills to user-scope
|
|
30
|
+
# dirs (~/.claude/, ~/.cursor/, ~/.codeium/windsurf/,
|
|
31
|
+
# ~/.config/agent-config/) so the agent has them in every
|
|
32
|
+
# project. Pair with --tools to scope surfaces; default = all.
|
|
33
|
+
# --uninstall With --global: remove the event4u/ namespace dir from each
|
|
34
|
+
# enabled surface (no effect on user-added files).
|
|
24
35
|
# --help, -h Show this help
|
|
36
|
+
#
|
|
37
|
+
# Examples:
|
|
38
|
+
# bash scripts/install # everything (default)
|
|
39
|
+
# bash scripts/install --tools=claude-code,cursor # only those two
|
|
40
|
+
# bash scripts/install --tools=cursor --yes # CI-friendly
|
|
41
|
+
# bash scripts/install --list-tools # show catalog
|
|
25
42
|
|
|
26
43
|
set -uo pipefail
|
|
27
44
|
|
|
@@ -32,19 +49,67 @@ INSTALL_PY="$SCRIPT_DIR/install.py"
|
|
|
32
49
|
SOURCE_DIR=""
|
|
33
50
|
TARGET_DIR=""
|
|
34
51
|
PROFILE=""
|
|
52
|
+
TOOLS=""
|
|
53
|
+
TOOLS_EXPLICIT=false
|
|
54
|
+
YES=false
|
|
35
55
|
FORCE=false
|
|
36
56
|
DRY_RUN=false
|
|
37
57
|
VERBOSE=false
|
|
38
58
|
QUIET=false
|
|
39
59
|
SKIP_SYNC=false
|
|
40
60
|
SKIP_BRIDGES=false
|
|
61
|
+
LIST_TOOLS=false
|
|
62
|
+
GLOBAL_INSTALL=false
|
|
63
|
+
UNINSTALL=false
|
|
64
|
+
|
|
65
|
+
# Single source of truth for valid tool IDs (also referenced by install.sh / install.py).
|
|
66
|
+
VALID_TOOLS="claude-code claude-desktop cursor windsurf cline gemini-cli copilot augment aider codex all"
|
|
41
67
|
|
|
42
68
|
show_help() {
|
|
43
|
-
sed -n '3,
|
|
69
|
+
sed -n '3,35p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
list_tools() {
|
|
73
|
+
cat <<'EOF'
|
|
74
|
+
Supported --tools IDs (default: all):
|
|
75
|
+
|
|
76
|
+
claude-code .claude/rules, .claude/skills, .claude/commands, .claude/settings.json
|
|
77
|
+
claude-desktop ~/Library/Application Support/Claude/ (global, see Phase 4 docs)
|
|
78
|
+
cursor .cursor/rules, .cursor/commands (legacy .cursorrules also written)
|
|
79
|
+
windsurf .windsurf/rules, .windsurf/workflows (legacy .windsurfrules also written)
|
|
80
|
+
cline .clinerules/ symlinks
|
|
81
|
+
gemini-cli GEMINI.md, .gemini/settings.json
|
|
82
|
+
copilot .github/copilot-instructions.md, .vscode/settings.json
|
|
83
|
+
augment .augment/ payload + settings (substrate — recommended for every install)
|
|
84
|
+
aider AGENTS.md (Linux Foundation cross-tool contract; always written)
|
|
85
|
+
codex AGENTS.md (same as aider — no extra action)
|
|
86
|
+
all every ID above (the default; backward-compatible)
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
--tools=claude-code,cursor project-local install for those two surfaces
|
|
90
|
+
--tools=all equivalent to omitting the flag
|
|
91
|
+
EOF
|
|
44
92
|
}
|
|
45
93
|
|
|
46
94
|
err() { echo " ❌ $*" >&2; }
|
|
47
95
|
|
|
96
|
+
# Validate a comma-separated tool list against $VALID_TOOLS. Empty input is
|
|
97
|
+
# rejected so a stray --tools= does not silently behave like --tools=all.
|
|
98
|
+
validate_tools() {
|
|
99
|
+
local raw="$1"
|
|
100
|
+
[[ -z "$raw" ]] && { err "--tools requires a non-empty value (use --list-tools to see options)"; return 1; }
|
|
101
|
+
local IFS=','
|
|
102
|
+
local item
|
|
103
|
+
for item in $raw; do
|
|
104
|
+
[[ -z "$item" ]] && { err "--tools contains an empty entry"; return 1; }
|
|
105
|
+
if [[ " $VALID_TOOLS " != *" $item "* ]]; then
|
|
106
|
+
err "Unknown tool ID: $item (run --list-tools for the catalog)"
|
|
107
|
+
return 1
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
return 0
|
|
111
|
+
}
|
|
112
|
+
|
|
48
113
|
while [[ $# -gt 0 ]]; do
|
|
49
114
|
case "$1" in
|
|
50
115
|
--source) SOURCE_DIR="$2"; shift 2 ;;
|
|
@@ -53,17 +118,84 @@ while [[ $# -gt 0 ]]; do
|
|
|
53
118
|
--target=*) TARGET_DIR="${1#*=}"; shift ;;
|
|
54
119
|
--profile) PROFILE="$2"; shift 2 ;;
|
|
55
120
|
--profile=*) PROFILE="${1#*=}"; shift ;;
|
|
121
|
+
--tools) TOOLS="$2"; TOOLS_EXPLICIT=true; shift 2 ;;
|
|
122
|
+
--tools=*) TOOLS="${1#*=}"; TOOLS_EXPLICIT=true; shift ;;
|
|
123
|
+
--list-tools) LIST_TOOLS=true; shift ;;
|
|
124
|
+
--yes|-y) YES=true; shift ;;
|
|
56
125
|
--force) FORCE=true; shift ;;
|
|
57
126
|
--dry-run) DRY_RUN=true; shift ;;
|
|
58
127
|
--verbose) VERBOSE=true; shift ;;
|
|
59
128
|
--quiet) QUIET=true; shift ;;
|
|
60
129
|
--skip-sync) SKIP_SYNC=true; shift ;;
|
|
61
130
|
--skip-bridges) SKIP_BRIDGES=true; shift ;;
|
|
131
|
+
--global) GLOBAL_INSTALL=true; shift ;;
|
|
132
|
+
--uninstall) UNINSTALL=true; shift ;;
|
|
62
133
|
--help|-h) show_help; exit 0 ;;
|
|
63
134
|
*) err "Unknown argument: $1"; show_help >&2; exit 1 ;;
|
|
64
135
|
esac
|
|
65
136
|
done
|
|
66
137
|
|
|
138
|
+
if $LIST_TOOLS; then
|
|
139
|
+
list_tools
|
|
140
|
+
exit 0
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# Interactive --tools picker (S9). Fires only when:
|
|
144
|
+
# - --tools was not explicitly passed
|
|
145
|
+
# - --yes / -y was not passed (CI / non-interactive opt-out)
|
|
146
|
+
# - stdin AND stdout are both TTYs (so we're not in a pipe / curl|bash flow)
|
|
147
|
+
# - --dry-run, --quiet, --skip-sync, --skip-bridges did not opt out of UX
|
|
148
|
+
# Otherwise we fall through to the backward-compatible "all" default.
|
|
149
|
+
prompt_tools() {
|
|
150
|
+
local choice picked tool i=0
|
|
151
|
+
local -a menu=(claude-code claude-desktop cursor windsurf cline gemini-cli copilot augment aider codex)
|
|
152
|
+
echo ""
|
|
153
|
+
echo " 📦 Pick the tools to install (comma-separated numbers, blank = all):"
|
|
154
|
+
for tool in "${menu[@]}"; do
|
|
155
|
+
i=$((i+1))
|
|
156
|
+
printf " %2d) %s\n" "$i" "$tool"
|
|
157
|
+
done
|
|
158
|
+
echo " a) all (default)"
|
|
159
|
+
echo ""
|
|
160
|
+
printf " Selection: "
|
|
161
|
+
IFS= read -r choice || choice=""
|
|
162
|
+
choice="${choice// /}"
|
|
163
|
+
if [[ -z "$choice" || "$choice" == "a" || "$choice" == "all" ]]; then
|
|
164
|
+
TOOLS="all"; return
|
|
165
|
+
fi
|
|
166
|
+
picked=""
|
|
167
|
+
local IFS=','
|
|
168
|
+
for n in $choice; do
|
|
169
|
+
if ! [[ "$n" =~ ^[0-9]+$ ]] || (( n < 1 || n > ${#menu[@]} )); then
|
|
170
|
+
err "Invalid selection: $n (expected 1-${#menu[@]} or 'a')"; exit 1
|
|
171
|
+
fi
|
|
172
|
+
picked+="${menu[$((n-1))]},"
|
|
173
|
+
done
|
|
174
|
+
TOOLS="${picked%,}"
|
|
175
|
+
echo " ✅ Selected: $TOOLS"
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if ! $TOOLS_EXPLICIT && ! $YES && ! $QUIET && ! $LIST_TOOLS && ! $GLOBAL_INSTALL && [[ -t 0 && -t 1 ]]; then
|
|
179
|
+
prompt_tools
|
|
180
|
+
TOOLS_EXPLICIT=true
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
if $UNINSTALL && ! $GLOBAL_INSTALL; then
|
|
184
|
+
err "--uninstall is only valid combined with --global"
|
|
185
|
+
exit 1
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
# Default = "all": backward compatible with pre-Phase-1 invocations. An
|
|
189
|
+
# explicit --tools= (empty value) is rejected by validate_tools — only an
|
|
190
|
+
# absent flag falls through to "all".
|
|
191
|
+
if ! $TOOLS_EXPLICIT && [[ -z "$TOOLS" ]]; then
|
|
192
|
+
TOOLS="all"
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
if ! validate_tools "$TOOLS"; then
|
|
196
|
+
exit 1
|
|
197
|
+
fi
|
|
198
|
+
|
|
67
199
|
# Auto-detect source: directory above this script
|
|
68
200
|
if [[ -z "$SOURCE_DIR" ]]; then
|
|
69
201
|
SOURCE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
@@ -103,6 +235,7 @@ run_sync() {
|
|
|
103
235
|
$DRY_RUN && args+=(--dry-run)
|
|
104
236
|
$VERBOSE && args+=(--verbose)
|
|
105
237
|
$QUIET && args+=(--quiet)
|
|
238
|
+
args+=(--tools="$TOOLS")
|
|
106
239
|
bash "$INSTALL_SH" "${args[@]}"
|
|
107
240
|
}
|
|
108
241
|
|
|
@@ -123,10 +256,32 @@ run_bridges() {
|
|
|
123
256
|
[[ -n "$PROFILE" ]] && args+=(--profile="$PROFILE")
|
|
124
257
|
$FORCE && args+=(--force)
|
|
125
258
|
$QUIET && args+=(--quiet)
|
|
259
|
+
args+=(--tools="$TOOLS")
|
|
126
260
|
"$python_bin" "$INSTALL_PY" "${args[@]}"
|
|
127
261
|
}
|
|
128
262
|
|
|
129
263
|
RC=0
|
|
264
|
+
|
|
265
|
+
# --global: dedicated user-scope path. Skips the project-bridge sync entirely
|
|
266
|
+
# and forwards to install.py --global (Phase 3 / S13). --uninstall pairs with
|
|
267
|
+
# --global to wipe the event4u/ namespace dir under each surface.
|
|
268
|
+
if $GLOBAL_INSTALL; then
|
|
269
|
+
if ! python_bin="$(find_python)"; then
|
|
270
|
+
err "Python 3 not found — required for --global install"
|
|
271
|
+
exit 1
|
|
272
|
+
fi
|
|
273
|
+
if [[ ! -f "$INSTALL_PY" ]]; then
|
|
274
|
+
err "Missing $INSTALL_PY"
|
|
275
|
+
exit 1
|
|
276
|
+
fi
|
|
277
|
+
args=(--package "$SOURCE_DIR" --global --tools="$TOOLS")
|
|
278
|
+
$FORCE && args+=(--force)
|
|
279
|
+
$QUIET && args+=(--quiet)
|
|
280
|
+
$UNINSTALL && args+=(--uninstall)
|
|
281
|
+
"$python_bin" "$INSTALL_PY" "${args[@]}"
|
|
282
|
+
exit $?
|
|
283
|
+
fi
|
|
284
|
+
|
|
130
285
|
if ! $SKIP_SYNC; then
|
|
131
286
|
if [[ ! -f "$INSTALL_SH" ]]; then
|
|
132
287
|
err "Missing $INSTALL_SH"
|
package/scripts/install.py
CHANGED
|
@@ -1217,6 +1217,189 @@ def _smoke_test_hooks(project_root: Path, package_root: Path) -> int:
|
|
|
1217
1217
|
return 1 if failed else 0
|
|
1218
1218
|
|
|
1219
1219
|
|
|
1220
|
+
# --- Global user-level install (Phase 3 — road-to-simplicity-and-everywhere) ---
|
|
1221
|
+
#
|
|
1222
|
+
# `scripts/install --global` ships kernel rules + a curated top-N of skills
|
|
1223
|
+
# into per-tool user-scope directories so the agent has them in every
|
|
1224
|
+
# project on the machine. Curation lives in templates/global-install-manifest.yml.
|
|
1225
|
+
#
|
|
1226
|
+
# Files are written under an `event4u/` namespace so `--global --uninstall`
|
|
1227
|
+
# can wipe the namespace dir without touching user-added files.
|
|
1228
|
+
|
|
1229
|
+
GLOBAL_NAMESPACE = "event4u"
|
|
1230
|
+
GLOBAL_MANIFEST_REL = Path("templates") / "global-install-manifest.yml"
|
|
1231
|
+
|
|
1232
|
+
# Per-tool global directories. Each surface gets a `rules/` and `skills/`
|
|
1233
|
+
# bucket under the event4u/ namespace. claude-desktop reuses claude-code's
|
|
1234
|
+
# ~/.claude/ (the two surfaces share the dir per Anthropic's docs).
|
|
1235
|
+
GLOBAL_TOOL_DIRS: dict[str, dict[str, Path]] = {
|
|
1236
|
+
"claude-code": {
|
|
1237
|
+
"rules": Path.home() / ".claude" / "rules" / GLOBAL_NAMESPACE,
|
|
1238
|
+
"skills": Path.home() / ".claude" / "skills" / GLOBAL_NAMESPACE,
|
|
1239
|
+
},
|
|
1240
|
+
"cursor": {
|
|
1241
|
+
"rules": Path.home() / ".cursor" / "rules" / "imported" / GLOBAL_NAMESPACE / "rules",
|
|
1242
|
+
"skills": Path.home() / ".cursor" / "rules" / "imported" / GLOBAL_NAMESPACE / "skills",
|
|
1243
|
+
},
|
|
1244
|
+
"windsurf": {
|
|
1245
|
+
"rules": Path.home() / ".codeium" / "windsurf" / "global_workflows" / GLOBAL_NAMESPACE / "rules",
|
|
1246
|
+
"skills": Path.home() / ".codeium" / "windsurf" / "global_workflows" / GLOBAL_NAMESPACE / "skills",
|
|
1247
|
+
},
|
|
1248
|
+
"fallback": {
|
|
1249
|
+
"rules": Path.home() / ".config" / "agent-config" / "rules" / GLOBAL_NAMESPACE,
|
|
1250
|
+
"skills": Path.home() / ".config" / "agent-config" / "skills" / GLOBAL_NAMESPACE,
|
|
1251
|
+
},
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def _load_global_manifest(package_root: Path) -> dict:
|
|
1256
|
+
"""Parse templates/global-install-manifest.yml without a YAML dep.
|
|
1257
|
+
|
|
1258
|
+
Tiny hand-rolled parser — only handles the manifest's flat shape:
|
|
1259
|
+
`key: value`, `- item`, and `- key: value` indented under a parent
|
|
1260
|
+
list. We avoid pulling in PyYAML so install.py stays zero-dep.
|
|
1261
|
+
"""
|
|
1262
|
+
path = package_root / GLOBAL_MANIFEST_REL
|
|
1263
|
+
if not path.is_file():
|
|
1264
|
+
fail(f"global manifest missing: {path}")
|
|
1265
|
+
out: dict = {"kernel_rules": [], "top_skills": []}
|
|
1266
|
+
section: str | None = None
|
|
1267
|
+
pending: dict | None = None
|
|
1268
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
1269
|
+
line = raw.split("#", 1)[0].rstrip()
|
|
1270
|
+
if not line.strip():
|
|
1271
|
+
continue
|
|
1272
|
+
if line.startswith("kernel_rules:"):
|
|
1273
|
+
section = "kernel_rules"; pending = None; continue
|
|
1274
|
+
if line.startswith("top_skills:"):
|
|
1275
|
+
section = "top_skills"; pending = None; continue
|
|
1276
|
+
stripped = line.lstrip()
|
|
1277
|
+
indent = len(line) - len(stripped)
|
|
1278
|
+
if section == "kernel_rules" and stripped.startswith("- "):
|
|
1279
|
+
out["kernel_rules"].append(stripped[2:].strip())
|
|
1280
|
+
elif section == "top_skills":
|
|
1281
|
+
if stripped.startswith("- id:"):
|
|
1282
|
+
if pending is not None:
|
|
1283
|
+
out["top_skills"].append(pending)
|
|
1284
|
+
pending = {"id": stripped.split(":", 1)[1].strip(), "surfaces": []}
|
|
1285
|
+
elif pending is not None and stripped.startswith("surfaces:"):
|
|
1286
|
+
raw_list = stripped.split(":", 1)[1].strip()
|
|
1287
|
+
if raw_list.startswith("[") and raw_list.endswith("]"):
|
|
1288
|
+
pending["surfaces"] = [s.strip() for s in raw_list[1:-1].split(",") if s.strip()]
|
|
1289
|
+
elif indent == 0 and not stripped.startswith("-"):
|
|
1290
|
+
section = None
|
|
1291
|
+
if pending is not None:
|
|
1292
|
+
out["top_skills"].append(pending)
|
|
1293
|
+
return out
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
def _resolve_skill_source(package_root: Path, skill_id: str) -> Path | None:
|
|
1297
|
+
"""Locate a skill's SKILL.md in the package. Prefers .claude/skills/."""
|
|
1298
|
+
for base in (package_root / ".claude" / "skills",
|
|
1299
|
+
package_root / ".agent-src" / "skills"):
|
|
1300
|
+
candidate = base / skill_id / "SKILL.md"
|
|
1301
|
+
if candidate.is_file():
|
|
1302
|
+
return candidate.parent
|
|
1303
|
+
return None
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
def _resolve_rule_source(package_root: Path, rule_id: str) -> Path | None:
|
|
1307
|
+
"""Locate a rule's .md in the package. Prefers .agent-src/rules/."""
|
|
1308
|
+
for base in (package_root / ".agent-src" / "rules",
|
|
1309
|
+
package_root / ".augment" / "rules"):
|
|
1310
|
+
candidate = base / f"{rule_id}.md"
|
|
1311
|
+
if candidate.is_file():
|
|
1312
|
+
return candidate
|
|
1313
|
+
return None
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
def _global_targets(tools: set[str]) -> dict[str, dict[str, Path]]:
|
|
1317
|
+
"""Return the subset of GLOBAL_TOOL_DIRS to write to. Fallback always on."""
|
|
1318
|
+
targets: dict[str, dict[str, Path]] = {"fallback": GLOBAL_TOOL_DIRS["fallback"]}
|
|
1319
|
+
for tool_id, dirs in GLOBAL_TOOL_DIRS.items():
|
|
1320
|
+
if tool_id == "fallback":
|
|
1321
|
+
continue
|
|
1322
|
+
# claude-desktop shares claude-code's dir — both flip the same target.
|
|
1323
|
+
if tool_id == "claude-code" and ("claude-code" in tools or "claude-desktop" in tools):
|
|
1324
|
+
targets[tool_id] = dirs
|
|
1325
|
+
elif tool_id in tools:
|
|
1326
|
+
targets[tool_id] = dirs
|
|
1327
|
+
return targets
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
def install_global(package_root: Path, tools: set[str], force: bool) -> int:
|
|
1331
|
+
"""S13/S14: ship kernel rules + curated skills to user-scope dirs."""
|
|
1332
|
+
import shutil
|
|
1333
|
+
manifest = _load_global_manifest(package_root)
|
|
1334
|
+
targets = _global_targets(tools)
|
|
1335
|
+
|
|
1336
|
+
if not QUIET:
|
|
1337
|
+
info(f"Global install — surfaces: {', '.join(sorted(targets))}")
|
|
1338
|
+
info(f" rules: {len(manifest['kernel_rules'])}")
|
|
1339
|
+
info(f" skills: {len(manifest['top_skills'])}")
|
|
1340
|
+
|
|
1341
|
+
for surface, dirs in targets.items():
|
|
1342
|
+
for kind in ("rules", "skills"):
|
|
1343
|
+
dirs[kind].mkdir(parents=True, exist_ok=True)
|
|
1344
|
+
# Rules: copy each kernel rule (file).
|
|
1345
|
+
for rule_id in manifest["kernel_rules"]:
|
|
1346
|
+
src = _resolve_rule_source(package_root, rule_id)
|
|
1347
|
+
if src is None:
|
|
1348
|
+
warn(f"global: rule '{rule_id}' missing in package — skipped")
|
|
1349
|
+
continue
|
|
1350
|
+
dst = dirs["rules"] / f"{rule_id}.md"
|
|
1351
|
+
if dst.exists() and not force and dst.read_bytes() == src.read_bytes():
|
|
1352
|
+
continue
|
|
1353
|
+
dst.write_bytes(src.read_bytes())
|
|
1354
|
+
# Skills: copy each curated skill (directory).
|
|
1355
|
+
for entry in manifest["top_skills"]:
|
|
1356
|
+
if surface != "fallback" and surface not in entry.get("surfaces", []):
|
|
1357
|
+
continue
|
|
1358
|
+
src_dir = _resolve_skill_source(package_root, entry["id"])
|
|
1359
|
+
if src_dir is None:
|
|
1360
|
+
warn(f"global: skill '{entry['id']}' missing in package — skipped")
|
|
1361
|
+
continue
|
|
1362
|
+
dst_dir = dirs["skills"] / entry["id"]
|
|
1363
|
+
if dst_dir.exists() and not force:
|
|
1364
|
+
shutil.rmtree(dst_dir)
|
|
1365
|
+
shutil.copytree(src_dir, dst_dir)
|
|
1366
|
+
if not QUIET:
|
|
1367
|
+
success(f" {surface}: {dirs['rules']}, {dirs['skills']}")
|
|
1368
|
+
return 0
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
def uninstall_global(tools: set[str]) -> int:
|
|
1372
|
+
"""S15: remove the event4u/ namespace dir from each enabled surface."""
|
|
1373
|
+
import shutil
|
|
1374
|
+
targets = _global_targets(tools)
|
|
1375
|
+
removed: list[str] = []
|
|
1376
|
+
# Collect ancestor dirs named GLOBAL_NAMESPACE so we can drop the
|
|
1377
|
+
# shared parent (e.g. cursor/windsurf put rules + skills as siblings
|
|
1378
|
+
# under one event4u/ dir; removing both leaves a stranded namespace
|
|
1379
|
+
# dir). We never delete anything not literally named event4u/.
|
|
1380
|
+
namespace_parents: set[Path] = set()
|
|
1381
|
+
for surface, dirs in targets.items():
|
|
1382
|
+
for kind in ("rules", "skills"):
|
|
1383
|
+
d = dirs[kind]
|
|
1384
|
+
if d.exists():
|
|
1385
|
+
shutil.rmtree(d)
|
|
1386
|
+
removed.append(str(d))
|
|
1387
|
+
for ancestor in d.parents:
|
|
1388
|
+
if ancestor.name == GLOBAL_NAMESPACE:
|
|
1389
|
+
namespace_parents.add(ancestor)
|
|
1390
|
+
for parent in namespace_parents:
|
|
1391
|
+
if parent.is_dir() and not any(parent.iterdir()):
|
|
1392
|
+
parent.rmdir()
|
|
1393
|
+
removed.append(str(parent))
|
|
1394
|
+
if not QUIET:
|
|
1395
|
+
if removed:
|
|
1396
|
+
for r in removed:
|
|
1397
|
+
success(f"removed {r}")
|
|
1398
|
+
else:
|
|
1399
|
+
skip("global uninstall: nothing to remove")
|
|
1400
|
+
return 0
|
|
1401
|
+
|
|
1402
|
+
|
|
1220
1403
|
# --- Argument parsing ---
|
|
1221
1404
|
|
|
1222
1405
|
def parse_options(argv: list[str]) -> argparse.Namespace:
|
|
@@ -1260,14 +1443,70 @@ def parse_options(argv: list[str]) -> argparse.Namespace:
|
|
|
1260
1443
|
parser.add_argument("--project", default=None, help="project root (default: cwd or PROJECT_ROOT env)")
|
|
1261
1444
|
parser.add_argument("--package", default=None, help="package root (default: auto-detect under project)")
|
|
1262
1445
|
parser.add_argument("--quiet", action="store_true", help="suppress info/success output (warnings/errors still shown)")
|
|
1446
|
+
parser.add_argument(
|
|
1447
|
+
"--tools",
|
|
1448
|
+
default="all",
|
|
1449
|
+
help=(
|
|
1450
|
+
"comma-separated tool IDs to install bridges for "
|
|
1451
|
+
"(claude-code,cursor,windsurf,cline,gemini-cli,copilot,augment,aider,codex,all). "
|
|
1452
|
+
"Default: all (backward-compatible)."
|
|
1453
|
+
),
|
|
1454
|
+
)
|
|
1263
1455
|
parser.add_argument(
|
|
1264
1456
|
"--no-smoke",
|
|
1265
1457
|
action="store_true",
|
|
1266
1458
|
help="skip the post-install hook smoke test (default: dry-fire dispatch:hook against every installed bridge)",
|
|
1267
1459
|
)
|
|
1460
|
+
parser.add_argument(
|
|
1461
|
+
"--global",
|
|
1462
|
+
dest="global_install",
|
|
1463
|
+
action="store_true",
|
|
1464
|
+
help=(
|
|
1465
|
+
"Phase-3 mode: ship kernel rules + curated top-N skills to user-scope "
|
|
1466
|
+
"dirs (~/.claude/, ~/.cursor/, ~/.codeium/windsurf/, ~/.config/agent-config/) "
|
|
1467
|
+
"so the agent has them in every project. Curation: templates/global-install-manifest.yml."
|
|
1468
|
+
),
|
|
1469
|
+
)
|
|
1470
|
+
parser.add_argument(
|
|
1471
|
+
"--uninstall",
|
|
1472
|
+
action="store_true",
|
|
1473
|
+
help="With --global: remove the event4u/ namespace dir from each enabled surface.",
|
|
1474
|
+
)
|
|
1268
1475
|
return parser.parse_args(argv)
|
|
1269
1476
|
|
|
1270
1477
|
|
|
1478
|
+
# Mapping of --tools IDs accepted by install.py. Mirrors VALID_TOOLS in
|
|
1479
|
+
# scripts/install. Bridges keyed off these IDs are gated; substrate
|
|
1480
|
+
# bridges (vscode, augment) always run.
|
|
1481
|
+
_VALID_TOOLS = {
|
|
1482
|
+
"claude-code", "claude-desktop", "cursor", "windsurf", "cline",
|
|
1483
|
+
"gemini-cli", "copilot", "augment", "aider", "codex", "all",
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
|
|
1487
|
+
def _parse_tools(raw: str) -> set[str]:
|
|
1488
|
+
"""Parse and validate --tools value. Returns set of normalized IDs.
|
|
1489
|
+
|
|
1490
|
+
"all" expands to every concrete ID. Empty input is rejected.
|
|
1491
|
+
Unknown IDs raise SystemExit so the caller's message reaches stderr.
|
|
1492
|
+
"""
|
|
1493
|
+
if not raw or not raw.strip():
|
|
1494
|
+
fail("--tools requires a non-empty value")
|
|
1495
|
+
items = [s.strip() for s in raw.split(",") if s.strip()]
|
|
1496
|
+
if not items:
|
|
1497
|
+
fail("--tools requires at least one ID")
|
|
1498
|
+
unknown = [s for s in items if s not in _VALID_TOOLS]
|
|
1499
|
+
if unknown:
|
|
1500
|
+
fail(f"--tools: unknown ID(s): {', '.join(unknown)} (valid: {', '.join(sorted(_VALID_TOOLS))})")
|
|
1501
|
+
if "all" in items:
|
|
1502
|
+
return {t for t in _VALID_TOOLS if t != "all"}
|
|
1503
|
+
return set(items)
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
def _is_tool_enabled(tools: set[str], tool_id: str) -> bool:
|
|
1507
|
+
return tool_id in tools
|
|
1508
|
+
|
|
1509
|
+
|
|
1271
1510
|
# --- Main ---
|
|
1272
1511
|
|
|
1273
1512
|
def main(argv: list[str]) -> int:
|
|
@@ -1300,31 +1539,52 @@ def main(argv: list[str]) -> int:
|
|
|
1300
1539
|
info(f"Profile: {opts.profile}")
|
|
1301
1540
|
print()
|
|
1302
1541
|
|
|
1542
|
+
# --global: short-circuits the project-bridge path. Ships kernel rules
|
|
1543
|
+
# + curated skills to user-scope dirs and exits. --uninstall pairs
|
|
1544
|
+
# with --global to remove the namespace dir.
|
|
1545
|
+
if opts.global_install:
|
|
1546
|
+
tools = _parse_tools(opts.tools)
|
|
1547
|
+
if opts.uninstall:
|
|
1548
|
+
return uninstall_global(tools)
|
|
1549
|
+
return install_global(package_root, tools, opts.force)
|
|
1550
|
+
if opts.uninstall:
|
|
1551
|
+
fail("--uninstall is only valid combined with --global")
|
|
1552
|
+
|
|
1303
1553
|
ensure_agent_settings(project_root, package_root, opts.profile, opts.force)
|
|
1304
1554
|
|
|
1555
|
+
tools = _parse_tools(opts.tools)
|
|
1556
|
+
|
|
1305
1557
|
if not opts.skip_bridges:
|
|
1558
|
+
# Substrate bridges (always written; other tools symlink/depend on them).
|
|
1306
1559
|
ensure_vscode_bridge(project_root, package_type, opts.force)
|
|
1307
1560
|
ensure_augment_bridge(project_root, opts.force)
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1561
|
+
# Tool-specific bridges (gated by --tools selection).
|
|
1562
|
+
if _is_tool_enabled(tools, "claude-code"):
|
|
1563
|
+
ensure_claude_bridge(project_root, opts.force)
|
|
1564
|
+
if _is_tool_enabled(tools, "cursor"):
|
|
1565
|
+
ensure_cursor_bridge(project_root, opts.force)
|
|
1566
|
+
if _is_tool_enabled(tools, "cline"):
|
|
1567
|
+
ensure_cline_bridge(project_root, opts.force)
|
|
1568
|
+
if _is_tool_enabled(tools, "windsurf"):
|
|
1569
|
+
ensure_windsurf_bridge(project_root, opts.force)
|
|
1570
|
+
if _is_tool_enabled(tools, "gemini-cli"):
|
|
1571
|
+
ensure_gemini_bridge(project_root, opts.force)
|
|
1572
|
+
if _is_tool_enabled(tools, "copilot"):
|
|
1573
|
+
ensure_copilot_bridge(project_root, opts.force)
|
|
1314
1574
|
|
|
1315
1575
|
if opts.augment_user_hooks:
|
|
1316
1576
|
ensure_augment_user_hooks(package_root, opts.force)
|
|
1317
1577
|
|
|
1318
|
-
if opts.cursor_user_hooks:
|
|
1578
|
+
if opts.cursor_user_hooks and _is_tool_enabled(tools, "cursor"):
|
|
1319
1579
|
ensure_cursor_user_hooks(package_root, opts.force)
|
|
1320
1580
|
|
|
1321
|
-
if opts.cline_user_hooks:
|
|
1581
|
+
if opts.cline_user_hooks and _is_tool_enabled(tools, "cline"):
|
|
1322
1582
|
ensure_cline_user_hooks(package_root, opts.force)
|
|
1323
1583
|
|
|
1324
|
-
if opts.windsurf_user_hooks:
|
|
1584
|
+
if opts.windsurf_user_hooks and _is_tool_enabled(tools, "windsurf"):
|
|
1325
1585
|
ensure_windsurf_user_hooks(package_root, opts.force)
|
|
1326
1586
|
|
|
1327
|
-
if opts.gemini_user_hooks:
|
|
1587
|
+
if opts.gemini_user_hooks and _is_tool_enabled(tools, "gemini-cli"):
|
|
1328
1588
|
ensure_gemini_user_hooks(package_root, opts.force)
|
|
1329
1589
|
|
|
1330
1590
|
if not opts.skip_bridges and not opts.no_smoke:
|