@trac3er/oh-my-god 1.0.2

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.
Files changed (229) hide show
  1. package/.claude-plugin/marketplace.json +36 -0
  2. package/.claude-plugin/plugin.json +23 -0
  3. package/.claude-plugin/scripts/install.sh +49 -0
  4. package/.claude-plugin/scripts/uninstall.sh +80 -0
  5. package/.claude-plugin/scripts/update.sh +84 -0
  6. package/.mcp.json +20 -0
  7. package/LICENSE +21 -0
  8. package/OMG-setup.sh +1093 -0
  9. package/README.md +335 -0
  10. package/THIRD_PARTY_NOTICES.md +24 -0
  11. package/UPSTREAM_DIFF.md +20 -0
  12. package/agents/__init__.py +1 -0
  13. package/agents/_model_roles.yaml +26 -0
  14. package/agents/designer.md +67 -0
  15. package/agents/explore.md +60 -0
  16. package/agents/model_roles.py +196 -0
  17. package/agents/omg-api-builder.md +23 -0
  18. package/agents/omg-architect-mode.md +43 -0
  19. package/agents/omg-architect.md +13 -0
  20. package/agents/omg-backend-engineer.md +43 -0
  21. package/agents/omg-critic.md +16 -0
  22. package/agents/omg-database-engineer.md +43 -0
  23. package/agents/omg-escalation-router.md +17 -0
  24. package/agents/omg-executor.md +12 -0
  25. package/agents/omg-frontend-designer.md +42 -0
  26. package/agents/omg-implement-mode.md +50 -0
  27. package/agents/omg-infra-engineer.md +43 -0
  28. package/agents/omg-qa-tester.md +16 -0
  29. package/agents/omg-research-mode.md +43 -0
  30. package/agents/omg-security-auditor.md +43 -0
  31. package/agents/omg-testing-engineer.md +43 -0
  32. package/agents/plan.md +80 -0
  33. package/agents/quick_task.md +64 -0
  34. package/agents/reviewer.md +83 -0
  35. package/agents/task.md +71 -0
  36. package/commands/OMG:ccg.md +22 -0
  37. package/commands/OMG:compat.md +57 -0
  38. package/commands/OMG:crazy.md +125 -0
  39. package/commands/OMG:domain-init.md +11 -0
  40. package/commands/OMG:escalate.md +52 -0
  41. package/commands/OMG:health-check.md +45 -0
  42. package/commands/OMG:init.md +134 -0
  43. package/commands/OMG:mode.md +44 -0
  44. package/commands/OMG:project-init.md +11 -0
  45. package/commands/OMG:ralph-start.md +43 -0
  46. package/commands/OMG:ralph-stop.md +23 -0
  47. package/commands/OMG:teams.md +39 -0
  48. package/commands/ai-commit.md +113 -0
  49. package/commands/ccg.md +9 -0
  50. package/commands/create-agent.md +183 -0
  51. package/commands/omc-teams.md +9 -0
  52. package/commands/session-branch.md +85 -0
  53. package/commands/session-fork.md +53 -0
  54. package/commands/session-merge.md +134 -0
  55. package/commands/theme.md +44 -0
  56. package/config/lsp_languages.yaml +324 -0
  57. package/config/themes/catppuccin-frappe.yaml +14 -0
  58. package/config/themes/catppuccin-latte.yaml +14 -0
  59. package/config/themes/catppuccin-macchiato.yaml +14 -0
  60. package/config/themes/catppuccin-mocha.yaml +14 -0
  61. package/config/themes/dracula.yaml +14 -0
  62. package/config/themes/gruvbox-dark.yaml +14 -0
  63. package/config/themes/nord.yaml +14 -0
  64. package/config/themes/one-dark.yaml +14 -0
  65. package/config/themes/solarized-dark.yaml +14 -0
  66. package/config/themes/tokyo-night.yaml +14 -0
  67. package/control_plane/__init__.py +2 -0
  68. package/control_plane/openapi.yaml +109 -0
  69. package/control_plane/server.py +107 -0
  70. package/control_plane/service.py +148 -0
  71. package/crates/omg-natives/Cargo.toml +17 -0
  72. package/crates/omg-natives/src/clipboard.rs +5 -0
  73. package/crates/omg-natives/src/glob.rs +15 -0
  74. package/crates/omg-natives/src/grep.rs +15 -0
  75. package/crates/omg-natives/src/highlight.rs +15 -0
  76. package/crates/omg-natives/src/html.rs +14 -0
  77. package/crates/omg-natives/src/image.rs +5 -0
  78. package/crates/omg-natives/src/keys.rs +5 -0
  79. package/crates/omg-natives/src/lib.rs +36 -0
  80. package/crates/omg-natives/src/prof.rs +5 -0
  81. package/crates/omg-natives/src/ps.rs +5 -0
  82. package/crates/omg-natives/src/shell.rs +5 -0
  83. package/crates/omg-natives/src/task.rs +5 -0
  84. package/crates/omg-natives/src/text.rs +14 -0
  85. package/hooks/_agent_registry.py +421 -0
  86. package/hooks/_budget.py +31 -0
  87. package/hooks/_common.py +476 -0
  88. package/hooks/_learnings.py +126 -0
  89. package/hooks/_memory.py +103 -0
  90. package/hooks/circuit-breaker.py +270 -0
  91. package/hooks/config-guard.py +163 -0
  92. package/hooks/context_pressure.py +53 -0
  93. package/hooks/credential_store.py +801 -0
  94. package/hooks/fetch-rate-limits.py +212 -0
  95. package/hooks/firewall.py +48 -0
  96. package/hooks/hashline-formatter-bridge.py +224 -0
  97. package/hooks/hashline-injector.py +273 -0
  98. package/hooks/hashline-validator.py +216 -0
  99. package/hooks/idle-detector.py +95 -0
  100. package/hooks/intentgate-keyword-detector.py +188 -0
  101. package/hooks/magic-keyword-router.py +195 -0
  102. package/hooks/policy_engine.py +310 -0
  103. package/hooks/post-tool-failure.py +19 -0
  104. package/hooks/post-write.py +199 -0
  105. package/hooks/pre-compact.py +204 -0
  106. package/hooks/pre-tool-inject.py +98 -0
  107. package/hooks/prompt-enhancer.py +672 -0
  108. package/hooks/quality-runner.py +191 -0
  109. package/hooks/secret-guard.py +47 -0
  110. package/hooks/session-end-capture.py +137 -0
  111. package/hooks/session-start.py +275 -0
  112. package/hooks/shadow_manager.py +297 -0
  113. package/hooks/state_migration.py +209 -0
  114. package/hooks/stop-gate.py +7 -0
  115. package/hooks/stop_dispatcher.py +929 -0
  116. package/hooks/test-validator.py +138 -0
  117. package/hooks/todo-state-tracker.py +114 -0
  118. package/hooks/tool-ledger.py +126 -0
  119. package/hooks/trust_review.py +524 -0
  120. package/install.sh +9 -0
  121. package/omg_natives/__init__.py +186 -0
  122. package/omg_natives/_bindings.py +165 -0
  123. package/omg_natives/clipboard.py +36 -0
  124. package/omg_natives/glob.py +42 -0
  125. package/omg_natives/grep.py +61 -0
  126. package/omg_natives/highlight.py +54 -0
  127. package/omg_natives/html.py +157 -0
  128. package/omg_natives/image.py +51 -0
  129. package/omg_natives/keys.py +46 -0
  130. package/omg_natives/prof.py +39 -0
  131. package/omg_natives/ps.py +93 -0
  132. package/omg_natives/shell.py +58 -0
  133. package/omg_natives/task.py +41 -0
  134. package/omg_natives/text.py +50 -0
  135. package/package.json +26 -0
  136. package/plugins/README.md +82 -0
  137. package/plugins/advanced/commands/OMG:code-review.md +114 -0
  138. package/plugins/advanced/commands/OMG:deep-plan.md +221 -0
  139. package/plugins/advanced/commands/OMG:handoff.md +115 -0
  140. package/plugins/advanced/commands/OMG:learn.md +110 -0
  141. package/plugins/advanced/commands/OMG:maintainer.md +31 -0
  142. package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
  143. package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
  144. package/plugins/advanced/commands/OMG:security-review.md +119 -0
  145. package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
  146. package/plugins/advanced/commands/OMG:ship.md +46 -0
  147. package/plugins/advanced/plugin.json +96 -0
  148. package/plugins/core/plugin.json +82 -0
  149. package/pytest.ini +5 -0
  150. package/registry/__init__.py +1 -0
  151. package/registry/verify_artifact.py +90 -0
  152. package/rules/contextual/architect-mode.md +9 -0
  153. package/rules/contextual/big-picture.md +20 -0
  154. package/rules/contextual/code-hygiene.md +26 -0
  155. package/rules/contextual/context-management.md +19 -0
  156. package/rules/contextual/context-minimization.md +32 -0
  157. package/rules/contextual/ddd-sdd.md +28 -0
  158. package/rules/contextual/dependency-safety.md +16 -0
  159. package/rules/contextual/doc-check.md +13 -0
  160. package/rules/contextual/implement-mode.md +9 -0
  161. package/rules/contextual/infra-safety.md +14 -0
  162. package/rules/contextual/outside-in.md +13 -0
  163. package/rules/contextual/persistent-mode.md +24 -0
  164. package/rules/contextual/research-mode.md +9 -0
  165. package/rules/contextual/security-domains.md +25 -0
  166. package/rules/contextual/vision-detection.md +27 -0
  167. package/rules/contextual/web-search.md +25 -0
  168. package/rules/contextual/write-verify.md +23 -0
  169. package/rules/core/00-truth.md +20 -0
  170. package/rules/core/01-surgical.md +19 -0
  171. package/rules/core/02-circuit-breaker.md +22 -0
  172. package/rules/core/03-ensemble.md +28 -0
  173. package/rules/core/04-testing.md +30 -0
  174. package/runtime/__init__.py +32 -0
  175. package/runtime/adapters/__init__.py +13 -0
  176. package/runtime/adapters/claude.py +60 -0
  177. package/runtime/adapters/gpt.py +53 -0
  178. package/runtime/adapters/local.py +53 -0
  179. package/runtime/business_workflow.py +220 -0
  180. package/runtime/compat.py +1299 -0
  181. package/runtime/custom_agent_loader.py +366 -0
  182. package/runtime/dispatcher.py +47 -0
  183. package/runtime/ecosystem.py +371 -0
  184. package/runtime/legacy_compat.py +7 -0
  185. package/runtime/omc_compat.py +7 -0
  186. package/runtime/omc_contract_snapshot.json +916 -0
  187. package/runtime/omg_compat_contract_snapshot.json +916 -0
  188. package/runtime/subagent_dispatcher.py +362 -0
  189. package/runtime/team_router.py +838 -0
  190. package/scripts/check-omc-contract-snapshot.py +12 -0
  191. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  192. package/scripts/check-omg-standalone-clean.py +102 -0
  193. package/scripts/legacy_to_omg_migrate.py +29 -0
  194. package/scripts/migrate-omc.py +464 -0
  195. package/scripts/omc_to_omg_migrate.py +12 -0
  196. package/scripts/omg.py +493 -0
  197. package/scripts/settings-merge.py +224 -0
  198. package/scripts/verify-no-omc.sh +5 -0
  199. package/scripts/verify-standalone.sh +21 -0
  200. package/templates/idea.yml +30 -0
  201. package/templates/policy.yaml +15 -0
  202. package/templates/profile.yaml +25 -0
  203. package/templates/runtime.yaml +12 -0
  204. package/templates/working-memory.md +17 -0
  205. package/tools/__init__.py +2 -0
  206. package/tools/browser_consent.py +289 -0
  207. package/tools/browser_stealth.py +481 -0
  208. package/tools/browser_tool.py +448 -0
  209. package/tools/changelog_generator.py +268 -0
  210. package/tools/commit_splitter.py +361 -0
  211. package/tools/config_discovery.py +151 -0
  212. package/tools/config_merger.py +449 -0
  213. package/tools/git_inspector.py +298 -0
  214. package/tools/lsp_client.py +275 -0
  215. package/tools/lsp_discovery.py +231 -0
  216. package/tools/lsp_operations.py +392 -0
  217. package/tools/python_repl.py +656 -0
  218. package/tools/python_sandbox.py +609 -0
  219. package/tools/search_providers/__init__.py +77 -0
  220. package/tools/search_providers/brave.py +115 -0
  221. package/tools/search_providers/exa.py +116 -0
  222. package/tools/search_providers/jina.py +104 -0
  223. package/tools/search_providers/perplexity.py +139 -0
  224. package/tools/search_providers/synthetic.py +74 -0
  225. package/tools/session_snapshot.py +736 -0
  226. package/tools/ssh_manager.py +912 -0
  227. package/tools/theme_engine.py +294 -0
  228. package/tools/theme_selector.py +137 -0
  229. package/tools/web_search.py +622 -0
package/OMG-setup.sh ADDED
@@ -0,0 +1,1093 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
6
+ BACKUP_TS="$(date +%Y%m%d_%H%M%S)"
7
+ BACKUP_DIR="$CLAUDE_DIR/.omg-backup-$BACKUP_TS"
8
+ VERSION="omg-v1-$(date +%Y%m%d)"
9
+
10
+ PLUGIN_NAME="omg"
11
+ PLUGIN_MARKETPLACE="oh-advanced-layer"
12
+ LEGACY_PLUGIN_MARKETPLACE="omg"
13
+ PLUGIN_REF="${PLUGIN_NAME}@${PLUGIN_MARKETPLACE}"
14
+ LEGACY_PLUGIN_REF="${PLUGIN_NAME}@${LEGACY_PLUGIN_MARKETPLACE}"
15
+ PLUGIN_CACHE_DIR="$CLAUDE_DIR/plugins/cache/$PLUGIN_MARKETPLACE/$PLUGIN_NAME"
16
+ LEGACY_PLUGIN_CACHE_DIR="$CLAUDE_DIR/plugins/cache/$LEGACY_PLUGIN_MARKETPLACE/$PLUGIN_NAME"
17
+ PLUGIN_BUNDLE_MARKER_FILE=".omg-plugin-bundle"
18
+
19
+ ACTION="install"
20
+ ACTION_EXPLICIT=false
21
+ DRY_RUN=false
22
+ NON_INTERACTIVE=false
23
+ MERGE_POLICY="ask"
24
+ FRESH_INSTALL=false
25
+ INSTALL_AS_PLUGIN=false
26
+ USE_SYMLINK=false
27
+ ERRORS=0
28
+ OMG_MANIFEST="$CLAUDE_DIR/.omg-manifest"
29
+ NEW_MANIFEST_ENTRIES=()
30
+
31
+ V3_RULES=(
32
+ "00-truth-evidence.md" "01-enforcement-map.md" "02-doc-check.md"
33
+ "03-working-memory.md" "04-quality-gate.md" "05-structured-reports.md"
34
+ "06-infra-safety.md" "07-cross-model.md" "08-big-picture.md"
35
+ "09-surgical-changes.md" "10-code-simplifier.md" "11-dependency-safety.md"
36
+ "12-circuit-breaker.md" "13-planning-checklist.md" "14-auto-commands.md"
37
+ "15-context-management.md" "16-honest-testing.md" "17-ensemble-collaboration.md"
38
+ "18-collaborative-solving.md" "19-outside-in.md" "20-project-identity.md"
39
+ "21-verified-claims.md" "22-auto-plugin-mcp.md"
40
+ )
41
+ V3_AGENTS_REMOVE=(cross-validator.md dependency-guardian.md infra-guardian.md perf-analyst.md ui-reviewer.md)
42
+ OLD_OMG_AGENTS=(architect.md critic.md executor.md qa-tester.md escalation-router.md)
43
+ V3_COMMANDS_REMOVE=(cross-review.md simplify.md)
44
+ V4_COMMANDS_REMOVE=(
45
+ code-review.md deep-plan.md domain-init.md escalate.md handoff.md
46
+ health-check.md learn.md project-init.md security-review.md
47
+ )
48
+
49
+ # Dynamic hook discovery — no hardcoded list.
50
+ # Used by remove_omg_files() as fallback when manifest is absent.
51
+ build_omg_hooks_list() {
52
+ OMG_HOOKS=()
53
+ for f in "$SCRIPT_DIR"/hooks/*.py; do
54
+ [ -f "$f" ] && OMG_HOOKS+=("$(basename "$f")")
55
+ done
56
+ }
57
+
58
+ usage() {
59
+ cat <<EOF
60
+ OMG Setup Manager
61
+
62
+ Usage:
63
+ ./OMG-setup.sh <action> [OPTIONS]
64
+ ./OMG-setup.sh [OPTIONS] # defaults to install
65
+ ./OMG-setup.sh # interactive menu in terminal mode
66
+
67
+ Actions:
68
+ install Install or upgrade OMG components
69
+ update Alias of install (explicit update mode)
70
+ reinstall Clean reinstall (remove OMG files, then install)
71
+ uninstall Remove OMG-managed files from ~/.claude
72
+
73
+ Options:
74
+ --fresh For install/update: clean reinstall before install
75
+ --symlink Use symlinks instead of copies (dev mode - live updates)
76
+ --install-as-plugin
77
+ Install plugin bundle (plugin.json + MCP + HUD) together
78
+ --dry-run Show what would happen without writing files
79
+ --non-interactive Skip prompts (CI/automation mode)
80
+ --merge-policy=X Settings merge: ask (default), apply, skip
81
+ -h, --help Show this help
82
+
83
+ Examples:
84
+ ./OMG-setup.sh install
85
+ ./OMG-setup.sh install --symlink # Dev mode: live updates from repo
86
+ ./OMG-setup.sh install --install-as-plugin
87
+ ./OMG-setup.sh update --non-interactive --merge-policy=apply
88
+ ./OMG-setup.sh reinstall --dry-run
89
+ ./OMG-setup.sh uninstall --dry-run
90
+ EOF
91
+ }
92
+
93
+ is_standalone_installed() {
94
+ [ -f "$CLAUDE_DIR/hooks/.omg-version" ] || [ -d "$CLAUDE_DIR/omg-runtime" ]
95
+ }
96
+
97
+ is_plugin_installed() {
98
+ local marker_new="$PLUGIN_CACHE_DIR/$PLUGIN_BUNDLE_MARKER_FILE"
99
+ local marker_legacy="$LEGACY_PLUGIN_CACHE_DIR/$PLUGIN_BUNDLE_MARKER_FILE"
100
+ if [ -f "$marker_new" ] || [ -f "$marker_legacy" ]; then
101
+ return 0
102
+ fi
103
+ local installed_plugins="$CLAUDE_DIR/plugins/installed_plugins.json"
104
+ if [ -f "$installed_plugins" ] && grep -Eq "\"$PLUGIN_REF\"|\"$LEGACY_PLUGIN_REF\"" "$installed_plugins" 2>/dev/null; then
105
+ return 0
106
+ fi
107
+ return 1
108
+ }
109
+
110
+ prompt_start_action() {
111
+ if $ACTION_EXPLICIT || $NON_INTERACTIVE || $DRY_RUN; then
112
+ return 0
113
+ fi
114
+
115
+ local standalone_installed=false
116
+ local plugin_installed=false
117
+ is_standalone_installed && standalone_installed=true
118
+ is_plugin_installed && plugin_installed=true
119
+
120
+ echo ""
121
+ echo "Select OMG setup action:"
122
+ echo " 1. Install standalone"
123
+ if $standalone_installed; then
124
+ echo " 2. Update standalone"
125
+ fi
126
+ echo " 3. Install as plugin"
127
+ if $plugin_installed; then
128
+ echo " 4. Update plugin install"
129
+ fi
130
+ echo " 5. Uninstall"
131
+ echo " 0. Cancel"
132
+ echo ""
133
+
134
+ read -p "Choose [1/2/3/4/5/0]: " -r
135
+ case "${REPLY:-}" in
136
+ 1)
137
+ ACTION="install"
138
+ INSTALL_AS_PLUGIN=false
139
+ ;;
140
+ 2)
141
+ if $standalone_installed; then
142
+ ACTION="update"
143
+ INSTALL_AS_PLUGIN=false
144
+ else
145
+ echo "Standalone update unavailable (not installed)."
146
+ exit 1
147
+ fi
148
+ ;;
149
+ 3)
150
+ ACTION="install"
151
+ INSTALL_AS_PLUGIN=true
152
+ ;;
153
+ 4)
154
+ if $plugin_installed; then
155
+ ACTION="update"
156
+ INSTALL_AS_PLUGIN=true
157
+ else
158
+ echo "Plugin update unavailable (plugin install not detected)."
159
+ exit 1
160
+ fi
161
+ ;;
162
+ 5)
163
+ ACTION="uninstall"
164
+ ;;
165
+ 0)
166
+ echo "Cancelled by user."
167
+ exit 0
168
+ ;;
169
+ *)
170
+ echo "Invalid selection."
171
+ exit 1
172
+ ;;
173
+ esac
174
+ }
175
+
176
+ parse_args() {
177
+ if [ $# -gt 0 ]; then
178
+ case "$1" in
179
+ install|update|reinstall|uninstall)
180
+ ACTION="$1"
181
+ ACTION_EXPLICIT=true
182
+ shift
183
+ ;;
184
+ help|-h|--help)
185
+ usage
186
+ exit 0
187
+ ;;
188
+ esac
189
+ fi
190
+
191
+ for arg in "$@"; do
192
+ case "$arg" in
193
+ --dry-run) DRY_RUN=true ;;
194
+ --symlink) USE_SYMLINK=true ;;
195
+ --non-interactive) NON_INTERACTIVE=true ;;
196
+ --fresh) FRESH_INSTALL=true ;;
197
+ --install-as-plugin) INSTALL_AS_PLUGIN=true ;;
198
+ --merge-policy=*) MERGE_POLICY="${arg#*=}" ;;
199
+ --help|-h)
200
+ usage
201
+ exit 0
202
+ ;;
203
+ *)
204
+ echo "Unknown option: $arg"
205
+ echo ""
206
+ usage
207
+ exit 1
208
+ ;;
209
+ esac
210
+ done
211
+
212
+ if [ "$ACTION" = "reinstall" ]; then
213
+ FRESH_INSTALL=true
214
+ fi
215
+ }
216
+
217
+ preflight() {
218
+ echo "Pre-flight checks..."
219
+ if ! command -v python3 &>/dev/null; then
220
+ echo " ❌ python3 not found. Install: https://www.python.org/downloads/"
221
+ exit 1
222
+ fi
223
+ local py_ver py_maj py_min
224
+ py_ver=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
225
+ py_maj=$(echo "$py_ver" | cut -d. -f1)
226
+ py_min=$(echo "$py_ver" | cut -d. -f2)
227
+ if [ "$py_maj" -lt 3 ] || { [ "$py_maj" -eq 3 ] && [ "$py_min" -lt 8 ]; }; then
228
+ echo " ❌ Python $py_ver found, 3.8+ required"
229
+ exit 1
230
+ fi
231
+ echo " ✓ Python $py_ver"
232
+ }
233
+
234
+ ensure_backup() {
235
+ if ! $DRY_RUN; then
236
+ mkdir -p "$BACKUP_DIR"
237
+ for dir in rules hooks agents commands; do
238
+ [ -d "$CLAUDE_DIR/$dir" ] && cp -r "$CLAUDE_DIR/$dir" "$BACKUP_DIR/$dir" 2>/dev/null || true
239
+ done
240
+ [ -f "$CLAUDE_DIR/settings.json" ] && cp "$CLAUDE_DIR/settings.json" "$BACKUP_DIR/"
241
+ prune_old_backups
242
+ fi
243
+ }
244
+
245
+ prune_old_backups() {
246
+ local backups=()
247
+ while IFS= read -r path; do
248
+ backups+=("$path")
249
+ done < <(find "$CLAUDE_DIR" -maxdepth 1 -type d -name ".omg-backup-*" | sort)
250
+
251
+ local total=${#backups[@]}
252
+ if [ "$total" -le 2 ]; then
253
+ return 0
254
+ fi
255
+
256
+ local remove_count=$((total - 2))
257
+ for old in "${backups[@]:0:$remove_count}"; do
258
+ [[ "$old" == "$CLAUDE_DIR"/* ]] || {
259
+ echo "ERROR: backup prune target outside expected directory: $old" >&2
260
+ exit 1
261
+ }
262
+ rm -rf "$old"
263
+ done
264
+ }
265
+
266
+ is_omg_managed_command_file() {
267
+ local file="$1"
268
+ if [ ! -f "$file" ]; then
269
+ return 1
270
+ fi
271
+
272
+ if grep -q "OMG-AUTO-COMPAT-ALIAS" "$file" 2>/dev/null; then
273
+ return 0
274
+ fi
275
+ if grep -q "OMG-MANAGED-COMMAND" "$file" 2>/dev/null; then
276
+ return 0
277
+ fi
278
+
279
+ local base
280
+ base="$(basename "$file")"
281
+ if [[ "$base" == OMG:* ]] && grep -q "/OMG:" "$file" 2>/dev/null; then
282
+ return 0
283
+ fi
284
+ return 1
285
+ }
286
+
287
+ mark_omg_managed_command_file() {
288
+ local file="$1"
289
+ if [ ! -f "$file" ]; then
290
+ return 0
291
+ fi
292
+ if ! grep -q "OMG-MANAGED-COMMAND" "$file" 2>/dev/null; then
293
+ printf "\n<!-- OMG-MANAGED-COMMAND -->\n" >> "$file"
294
+ fi
295
+ }
296
+
297
+ # Install a file or directory - either copy or symlink based on USE_SYMLINK
298
+ # Usage: install_file <source> <target> [type: file|dir]
299
+ install_file() {
300
+ local src="$1"
301
+ local target="$2"
302
+ local type="${3:-file}"
303
+
304
+ if $USE_SYMLINK; then
305
+ # In symlink mode, create symlink from target -> source
306
+ # First remove existing file/dir if present
307
+ if [ -e "$target" ] || [ -L "$target" ]; then
308
+ rm -rf "$target"
309
+ fi
310
+ ln -s "$src" "$target"
311
+ else
312
+ # In copy mode, do regular copy
313
+ if [ "$type" = "dir" ]; then
314
+ cp -R "$src" "$target"
315
+ else
316
+ cp "$src" "$target"
317
+ fi
318
+ fi
319
+ }
320
+
321
+ track_file() {
322
+ NEW_MANIFEST_ENTRIES+=("$1")
323
+ }
324
+
325
+ reconcile_stale_files() {
326
+ if [ ! -f "$OMG_MANIFEST" ]; then
327
+ echo " (no previous manifest — first install, skipping reconciliation)"
328
+ return 0
329
+ fi
330
+ local stale=0
331
+ while IFS= read -r old_entry; do
332
+ [ -n "$old_entry" ] || continue
333
+ [[ "$old_entry" == "#"* ]] && continue
334
+ local found=false
335
+ for new_entry in "${NEW_MANIFEST_ENTRIES[@]}"; do
336
+ if [ "$old_entry" = "$new_entry" ]; then
337
+ found=true
338
+ break
339
+ fi
340
+ done
341
+ if ! $found; then
342
+ local target="$CLAUDE_DIR/$old_entry"
343
+ if [ -f "$target" ]; then
344
+ if ! $DRY_RUN; then
345
+ rm -f "$target"
346
+ fi
347
+ echo " - $old_entry (removed from source)"
348
+ stale=$((stale + 1))
349
+ fi
350
+ fi
351
+ done < "$OMG_MANIFEST"
352
+ if [ $stale -eq 0 ]; then
353
+ echo " (no stale files)"
354
+ elif $DRY_RUN; then
355
+ echo " (dry-run: would remove $stale stale file(s))"
356
+ else
357
+ echo " ✓ Cleaned $stale stale file(s)"
358
+ fi
359
+ }
360
+
361
+ write_omg_manifest() {
362
+ if ! $DRY_RUN; then
363
+ printf '%s\n' "${NEW_MANIFEST_ENTRIES[@]}" | sort > "$OMG_MANIFEST"
364
+ fi
365
+ }
366
+
367
+ prune_plugin_mcp_from_settings() {
368
+ local mcp_path="$CLAUDE_DIR/.mcp.json"
369
+ if [ ! -f "$mcp_path" ]; then
370
+ return 0
371
+ fi
372
+ python3 - "$mcp_path" <<'PY'
373
+ import json
374
+ import sys
375
+ from pathlib import Path
376
+
377
+ path = Path(sys.argv[1])
378
+ try:
379
+ data = json.loads(path.read_text(encoding="utf-8"))
380
+ except Exception:
381
+ print("0")
382
+ raise SystemExit(0)
383
+
384
+ servers = data.get("mcpServers")
385
+ if not isinstance(servers, dict):
386
+ print("0")
387
+ raise SystemExit(0)
388
+
389
+ removed = 0
390
+ for key in ("context7", "filesystem", "websearch", "chrome-devtools"):
391
+ if key in servers:
392
+ servers.pop(key, None)
393
+ removed += 1
394
+
395
+ if removed:
396
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
397
+
398
+ print(str(removed))
399
+ PY
400
+ }
401
+
402
+ merge_plugin_mcp_into_settings() {
403
+ local mcp_path="$CLAUDE_DIR/.mcp.json"
404
+ local source_mcp_path="$SCRIPT_DIR/.mcp.json"
405
+ python3 - "$mcp_path" "$source_mcp_path" <<'PY'
406
+ import json
407
+ import sys
408
+ from pathlib import Path
409
+
410
+ mcp_path = Path(sys.argv[1])
411
+ source_mcp_path = Path(sys.argv[2])
412
+
413
+ mcp_config = {}
414
+ if mcp_path.exists():
415
+ try:
416
+ mcp_config = json.loads(mcp_path.read_text(encoding="utf-8"))
417
+ except Exception:
418
+ mcp_config = {}
419
+ if not isinstance(mcp_config, dict):
420
+ mcp_config = {}
421
+
422
+ try:
423
+ source_mcp = json.loads(source_mcp_path.read_text(encoding="utf-8"))
424
+ except Exception:
425
+ source_mcp = {}
426
+
427
+ incoming = source_mcp.get("mcpServers") if isinstance(source_mcp, dict) else {}
428
+ if not isinstance(incoming, dict):
429
+ incoming = {}
430
+
431
+ servers = mcp_config.get("mcpServers")
432
+ if not isinstance(servers, dict):
433
+ servers = {}
434
+ for key, value in incoming.items():
435
+ if key not in servers:
436
+ servers[key] = value
437
+ mcp_config["mcpServers"] = servers
438
+
439
+ mcp_path.parent.mkdir(parents=True, exist_ok=True)
440
+ mcp_path.write_text(json.dumps(mcp_config, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
441
+ print(str(len(incoming)))
442
+ PY
443
+ }
444
+
445
+ write_plugin_mcp_file() {
446
+ local target_path="$1"
447
+ local source_mcp_path="$SCRIPT_DIR/.mcp.json"
448
+ python3 - "$target_path" "$source_mcp_path" <<'PY'
449
+ import json
450
+ import sys
451
+ from pathlib import Path
452
+
453
+ target_path = Path(sys.argv[1])
454
+ source_mcp_path = Path(sys.argv[2])
455
+
456
+ try:
457
+ source_mcp = json.loads(source_mcp_path.read_text(encoding="utf-8"))
458
+ except Exception:
459
+ source_mcp = {}
460
+
461
+ mcp_servers = source_mcp.get("mcpServers") if isinstance(source_mcp, dict) else {}
462
+ if not isinstance(mcp_servers, dict):
463
+ mcp_servers = {}
464
+
465
+ payload = {"mcpServers": mcp_servers}
466
+ target_path.parent.mkdir(parents=True, exist_ok=True)
467
+ target_path.write_text(json.dumps(payload, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
468
+ print(str(len(mcp_servers)))
469
+ PY
470
+ }
471
+
472
+ register_plugin_in_registry() {
473
+ local plugin_ref="$1"
474
+ local install_path="$2"
475
+ local version="$3"
476
+ local settings_path="$CLAUDE_DIR/settings.json"
477
+ local installed_plugins_path="$CLAUDE_DIR/plugins/installed_plugins.json"
478
+
479
+ python3 - "$settings_path" "$installed_plugins_path" "$plugin_ref" "$install_path" "$version" <<'PY'
480
+ import json
481
+ import sys
482
+ from pathlib import Path
483
+ from datetime import datetime, timezone
484
+
485
+ settings_path = Path(sys.argv[1])
486
+ installed_plugins_path = Path(sys.argv[2])
487
+ plugin_ref = sys.argv[3]
488
+ install_path = sys.argv[4]
489
+ version = sys.argv[5]
490
+ now = datetime.now(timezone.utc).isoformat()
491
+
492
+ settings = {}
493
+ if settings_path.exists():
494
+ try:
495
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
496
+ except Exception:
497
+ settings = {}
498
+ if not isinstance(settings, dict):
499
+ settings = {}
500
+ enabled = settings.get("enabledPlugins")
501
+ if not isinstance(enabled, dict):
502
+ enabled = {}
503
+ enabled[plugin_ref] = True
504
+ settings["enabledPlugins"] = enabled
505
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
506
+ settings_path.write_text(json.dumps(settings, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
507
+
508
+ installed = {}
509
+ if installed_plugins_path.exists():
510
+ try:
511
+ installed = json.loads(installed_plugins_path.read_text(encoding="utf-8"))
512
+ except Exception:
513
+ installed = {}
514
+ if not isinstance(installed, dict):
515
+ installed = {}
516
+ installed["version"] = 2
517
+ plugins = installed.get("plugins")
518
+ if not isinstance(plugins, dict):
519
+ plugins = {}
520
+ plugins[plugin_ref] = [{
521
+ "scope": "user",
522
+ "installPath": install_path,
523
+ "version": version,
524
+ "installedAt": now,
525
+ "lastUpdated": now,
526
+ }]
527
+ installed["plugins"] = plugins
528
+ installed_plugins_path.parent.mkdir(parents=True, exist_ok=True)
529
+ installed_plugins_path.write_text(json.dumps(installed, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
530
+ PY
531
+ }
532
+
533
+ unregister_plugin_from_registry() {
534
+ local plugin_ref="$1"
535
+ local settings_path="$CLAUDE_DIR/settings.json"
536
+ local installed_plugins_path="$CLAUDE_DIR/plugins/installed_plugins.json"
537
+
538
+ python3 - "$settings_path" "$installed_plugins_path" "$plugin_ref" <<'PY'
539
+ import json
540
+ import sys
541
+ from pathlib import Path
542
+
543
+ settings_path = Path(sys.argv[1])
544
+ installed_plugins_path = Path(sys.argv[2])
545
+ plugin_ref = sys.argv[3]
546
+
547
+ if settings_path.exists():
548
+ try:
549
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
550
+ except Exception:
551
+ settings = {}
552
+ if isinstance(settings, dict):
553
+ enabled = settings.get("enabledPlugins")
554
+ if isinstance(enabled, dict) and plugin_ref in enabled:
555
+ enabled.pop(plugin_ref, None)
556
+ settings["enabledPlugins"] = enabled
557
+ settings_path.write_text(json.dumps(settings, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
558
+
559
+ if installed_plugins_path.exists():
560
+ try:
561
+ installed = json.loads(installed_plugins_path.read_text(encoding="utf-8"))
562
+ except Exception:
563
+ installed = {}
564
+ if isinstance(installed, dict):
565
+ plugins = installed.get("plugins")
566
+ if isinstance(plugins, dict) and plugin_ref in plugins:
567
+ plugins.pop(plugin_ref, None)
568
+ installed["plugins"] = plugins
569
+ installed_plugins_path.write_text(json.dumps(installed, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
570
+ PY
571
+ }
572
+
573
+ remove_omg_files() {
574
+ if ! $DRY_RUN; then
575
+ local plugin_bundle_marker="$PLUGIN_CACHE_DIR/$PLUGIN_BUNDLE_MARKER_FILE"
576
+ local plugin_bundle_marker_legacy="$LEGACY_PLUGIN_CACHE_DIR/$PLUGIN_BUNDLE_MARKER_FILE"
577
+ local remove_plugin_managed_mcp=false
578
+ if [ -f "$plugin_bundle_marker" ] || [ -f "$plugin_bundle_marker_legacy" ]; then
579
+ remove_plugin_managed_mcp=true
580
+ fi
581
+
582
+ # Use manifest for precise removal if available.
583
+ if [ -f "$OMG_MANIFEST" ]; then
584
+ while IFS= read -r entry; do
585
+ [ -n "$entry" ] || continue
586
+ [[ "$entry" == "#"* ]] && continue
587
+ rm -f "$CLAUDE_DIR/$entry"
588
+ done < "$OMG_MANIFEST"
589
+ rm -f "$OMG_MANIFEST"
590
+ fi
591
+
592
+ # Also remove by pattern (covers pre-manifest installs + compat aliases).
593
+ build_omg_hooks_list
594
+ for h in "${OMG_HOOKS[@]}"; do
595
+ rm -f "$CLAUDE_DIR/hooks/$h"
596
+ done
597
+ rm -f "$CLAUDE_DIR/hooks/.omg-version" "$CLAUDE_DIR/hooks/.omg-coexist"
598
+
599
+ # Remove OMG rules and old v3 rule set.
600
+ for r in "$CLAUDE_DIR"/rules/0[0-4]-*.md; do
601
+ [ -f "$r" ] && rm "$r"
602
+ done
603
+ for rule in "${V3_RULES[@]}"; do
604
+ rm -f "$CLAUDE_DIR/rules/$rule"
605
+ done
606
+
607
+ # Remove OMG agents, commands, templates.
608
+ rm -f "$CLAUDE_DIR"/agents/omg-*.md
609
+ if [ -d "$CLAUDE_DIR/commands" ]; then
610
+ while IFS= read -r cmd_path; do
611
+ if [ -n "$cmd_path" ] && is_omg_managed_command_file "$cmd_path"; then
612
+ rm -f "$cmd_path"
613
+ fi
614
+ done < <(find "$CLAUDE_DIR/commands" -maxdepth 1 -type f -name "*.md" 2>/dev/null | sort)
615
+ fi
616
+ [[ "$CLAUDE_DIR/templates/omg" == "$CLAUDE_DIR"* ]] || { echo "ERROR: rm -rf target outside expected directory: $CLAUDE_DIR/templates/omg" >&2; exit 1; }
617
+ rm -rf "$CLAUDE_DIR/templates/omg"
618
+ [[ "$CLAUDE_DIR/omg-runtime" == "$CLAUDE_DIR"* ]] || { echo "ERROR: rm -rf target outside expected directory: $CLAUDE_DIR/omg-runtime" >&2; exit 1; }
619
+ rm -rf "$CLAUDE_DIR/omg-runtime"
620
+
621
+ [[ "$PLUGIN_CACHE_DIR" == "$CLAUDE_DIR"* ]] || { echo "ERROR: rm -rf target outside expected directory: $PLUGIN_CACHE_DIR" >&2; exit 1; }
622
+ rm -rf "$PLUGIN_CACHE_DIR"
623
+ [[ "$LEGACY_PLUGIN_CACHE_DIR" == "$CLAUDE_DIR"* ]] || { echo "ERROR: rm -rf target outside expected directory: $LEGACY_PLUGIN_CACHE_DIR" >&2; exit 1; }
624
+ rm -rf "$LEGACY_PLUGIN_CACHE_DIR"
625
+ rm -f "$CLAUDE_DIR/hud/omg-hud.mjs"
626
+ unregister_plugin_from_registry "$PLUGIN_REF"
627
+ unregister_plugin_from_registry "$LEGACY_PLUGIN_REF"
628
+
629
+ if $remove_plugin_managed_mcp; then
630
+ local pruned_mcp=0
631
+ pruned_mcp=$(prune_plugin_mcp_from_settings)
632
+ if [ "${pruned_mcp:-0}" -gt 0 ]; then
633
+ echo " ✓ Plugin-managed MCP servers removed from settings.json ($pruned_mcp)"
634
+ fi
635
+ fi
636
+ fi
637
+ }
638
+
639
+ install_plugin_bundle() {
640
+ local plugin_ref="$PLUGIN_REF"
641
+ local plugin_root="$PLUGIN_CACHE_DIR/$VERSION"
642
+ local plugin_manifest_src="$SCRIPT_DIR/.claude-plugin/plugin.json"
643
+ local plugin_manifest_target="$plugin_root/.claude-plugin/plugin.json"
644
+ local plugin_mcp_target="$plugin_root/.mcp.json"
645
+ local hud_src="$SCRIPT_DIR/hud/omg-hud.mjs"
646
+ local hud_target="$CLAUDE_DIR/hud/omg-hud.mjs"
647
+
648
+ echo " Plugin bundle mode enabled: install plugin + MCP + HUD together"
649
+ if $DRY_RUN; then
650
+ echo " (would install plugin bundle under $plugin_root and deploy HUD to $hud_target)"
651
+ echo " (would register plugin in ~/.claude/plugins/installed_plugins.json and enable it in settings.json)"
652
+ echo " (would merge plugin MCP servers into user-level settings.json)"
653
+ return 0
654
+ fi
655
+
656
+ mkdir -p "$plugin_root/.claude-plugin"
657
+ mkdir -p "$CLAUDE_DIR/hud"
658
+ cp "$plugin_manifest_src" "$plugin_manifest_target"
659
+ write_plugin_mcp_file "$plugin_mcp_target" >/dev/null
660
+ cp "$hud_src" "$hud_target"
661
+ mkdir -p "$PLUGIN_CACHE_DIR"
662
+ printf '%s\n' "omg-plugin-bundle-v1" > "$PLUGIN_CACHE_DIR/$PLUGIN_BUNDLE_MARKER_FILE"
663
+
664
+ unregister_plugin_from_registry "$LEGACY_PLUGIN_REF"
665
+ register_plugin_in_registry "$plugin_ref" "$plugin_root" "$VERSION"
666
+ merge_plugin_mcp_into_settings >/dev/null
667
+
668
+ track_file "plugins/cache/$PLUGIN_MARKETPLACE/$PLUGIN_NAME/$VERSION/.claude-plugin/plugin.json"
669
+ track_file "plugins/cache/$PLUGIN_MARKETPLACE/$PLUGIN_NAME/$VERSION/.mcp.json"
670
+ track_file "plugins/cache/$PLUGIN_MARKETPLACE/$PLUGIN_NAME/$PLUGIN_BUNDLE_MARKER_FILE"
671
+ track_file "hud/omg-hud.mjs"
672
+ echo " ✓ Plugin bundle installed and registered in Claude plugin settings"
673
+ }
674
+
675
+ run_uninstall() {
676
+ echo "═══════════════════════════════════════════════════════════════"
677
+ echo " OMG Setup Manager — uninstall"
678
+ echo "═══════════════════════════════════════════════════════════════"
679
+ echo ""
680
+
681
+ if $DRY_RUN; then
682
+ echo " *** DRY RUN — no files will be changed ***"
683
+ echo ""
684
+ fi
685
+
686
+ preflight
687
+
688
+ local existing_ver=""
689
+ if [ -f "$CLAUDE_DIR/hooks/.omg-version" ]; then
690
+ existing_ver=$(cat "$CLAUDE_DIR/hooks/.omg-version" 2>/dev/null || echo "")
691
+ fi
692
+ if [ -n "$existing_ver" ]; then
693
+ echo " ✓ Existing OMG install: $existing_ver"
694
+ else
695
+ echo " ~ No .omg-version marker found; uninstall will still remove known OMG files."
696
+ fi
697
+
698
+ if ! $NON_INTERACTIVE && ! $DRY_RUN; then
699
+ read -p "Proceed with uninstall? [y/N] " -n 1 -r
700
+ echo ""
701
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
702
+ echo "Cancelled."
703
+ exit 0
704
+ fi
705
+ fi
706
+
707
+ echo ""
708
+ echo "Uninstall: removing OMG-managed files from $CLAUDE_DIR"
709
+ ensure_backup
710
+ if ! $DRY_RUN; then
711
+ echo " ✓ Backup: $BACKUP_DIR"
712
+ else
713
+ echo " (would backup to $BACKUP_DIR)"
714
+ fi
715
+ remove_omg_files
716
+ if $DRY_RUN; then
717
+ echo " (would remove OMG hooks/rules/agents/commands/templates)"
718
+ else
719
+ echo " ✓ Removed OMG hooks/rules/agents/commands/templates"
720
+ fi
721
+
722
+ echo ""
723
+ echo "Uninstall complete."
724
+ echo " ✓ If plugin bundle was installed, plugin + MCP + HUD were removed together"
725
+ echo "Preserved:"
726
+ echo " - $CLAUDE_DIR/settings.json"
727
+ echo " - project .omg/ data"
728
+ echo " - non-OMG custom files"
729
+ }
730
+
731
+ run_install_like() {
732
+ local existing_ver=""
733
+ local removed=0
734
+ local installed_rules=0
735
+ local installed_hooks=0
736
+ local hook_errors=0
737
+ local installed_agents=0
738
+ local installed_cmds=0
739
+
740
+ echo "═══════════════════════════════════════════════════════════════"
741
+ echo " OMG Setup Manager — $ACTION"
742
+ echo "═══════════════════════════════════════════════════════════════"
743
+ echo ""
744
+
745
+ if $DRY_RUN; then
746
+ echo " *** DRY RUN — no files will be changed ***"
747
+ echo ""
748
+ fi
749
+
750
+ preflight
751
+
752
+ if [ -f "$CLAUDE_DIR/hooks/.omg-version" ]; then
753
+ existing_ver=$(cat "$CLAUDE_DIR/hooks/.omg-version" 2>/dev/null || echo "")
754
+ fi
755
+
756
+ if [ "$ACTION" = "update" ] && [ -z "$existing_ver" ]; then
757
+ echo " ~ No existing OMG install detected. update will proceed as install."
758
+ fi
759
+
760
+ if [ -n "$existing_ver" ]; then
761
+ echo " ✓ Existing: $existing_ver → target $VERSION"
762
+ else
763
+ echo " ✓ Fresh install"
764
+ fi
765
+ echo " ✓ Standalone mode: OMG-only command surface (no OMC aliases)"
766
+
767
+ if $FRESH_INSTALL; then
768
+ echo ""
769
+ echo "Fresh/reinstall mode: remove OMG files before install."
770
+ if ! $NON_INTERACTIVE && ! $DRY_RUN; then
771
+ read -p "Proceed with fresh cleanup? [y/N] " -n 1 -r
772
+ echo ""
773
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
774
+ echo "Cancelled."
775
+ exit 0
776
+ fi
777
+ fi
778
+
779
+ ensure_backup
780
+ if ! $DRY_RUN; then
781
+ echo " ✓ Backup: $BACKUP_DIR"
782
+ else
783
+ echo " (would backup to $BACKUP_DIR)"
784
+ fi
785
+ remove_omg_files
786
+ if $DRY_RUN; then
787
+ echo " (would remove OMG hooks/rules/agents/commands/templates)"
788
+ else
789
+ echo " ✓ Clean slate ready"
790
+ fi
791
+ existing_ver=""
792
+ fi
793
+
794
+ if [ -n "$existing_ver" ] && ! $FRESH_INSTALL; then
795
+ echo ""
796
+ echo "Step 0/7: Backup existing installation..."
797
+ ensure_backup
798
+ if ! $DRY_RUN; then
799
+ echo " ✓ Backup: $BACKUP_DIR"
800
+ else
801
+ echo " (would backup to $BACKUP_DIR)"
802
+ fi
803
+ fi
804
+
805
+ echo ""
806
+ echo "Step 1/7: Remove deprecated files..."
807
+
808
+ for rule in "${V3_RULES[@]}"; do
809
+ target="$CLAUDE_DIR/rules/$rule"
810
+ if [ -f "$target" ]; then
811
+ ! $DRY_RUN && rm "$target"
812
+ echo " - rules/$rule"
813
+ removed=$((removed + 1))
814
+ fi
815
+ done
816
+
817
+ for agent in "${V3_AGENTS_REMOVE[@]}"; do
818
+ target="$CLAUDE_DIR/agents/$agent"
819
+ if [ -f "$target" ]; then
820
+ ! $DRY_RUN && rm "$target"
821
+ echo " - agents/$agent (v3 deprecated)"
822
+ removed=$((removed + 1))
823
+ fi
824
+ done
825
+
826
+ for agent in "${OLD_OMG_AGENTS[@]}"; do
827
+ target="$CLAUDE_DIR/agents/$agent"
828
+ if [ -f "$target" ]; then
829
+ if grep -q "OMG\|omg\|circuit.breaker\|escalat" "$target" 2>/dev/null; then
830
+ ! $DRY_RUN && rm "$target"
831
+ echo " - agents/$agent (renamed to omg-$agent)"
832
+ removed=$((removed + 1))
833
+ else
834
+ echo " ~ agents/$agent (kept — appears to be non-OMG/custom)"
835
+ fi
836
+ fi
837
+ done
838
+
839
+ for cmd in "${V3_COMMANDS_REMOVE[@]}" "${V4_COMMANDS_REMOVE[@]}"; do
840
+ target="$CLAUDE_DIR/commands/$cmd"
841
+ if [ -f "$target" ]; then
842
+ if is_omg_managed_command_file "$target"; then
843
+ ! $DRY_RUN && rm "$target"
844
+ echo " - commands/$cmd (v4 → OMG:$cmd)"
845
+ removed=$((removed + 1))
846
+ else
847
+ echo " ~ commands/$cmd (kept — appears to be non-OMG/custom)"
848
+ fi
849
+ fi
850
+ done
851
+
852
+ if compgen -G "$CLAUDE_DIR/commands/*omc*.md" > /dev/null; then
853
+ for cmd in "$CLAUDE_DIR"/commands/*omc*.md; do
854
+ [ -f "$cmd" ] || continue
855
+ if is_omg_managed_command_file "$cmd"; then
856
+ ! $DRY_RUN && rm "$cmd"
857
+ echo " - commands/$(basename "$cmd") (removed OMC command alias)"
858
+ removed=$((removed + 1))
859
+ fi
860
+ done
861
+ fi
862
+
863
+ if [ -d "$CLAUDE_DIR/hooks/__pycache__" ]; then
864
+ ! $DRY_RUN && rm -rf "$CLAUDE_DIR/hooks/__pycache__"
865
+ removed=$((removed + 1))
866
+ fi
867
+ ! $DRY_RUN && find "$CLAUDE_DIR/hooks/" -name "*.pyc" -delete 2>/dev/null || true
868
+ [ $removed -eq 0 ] && echo " (nothing to remove)" || echo " ✓ Removed $removed deprecated files"
869
+
870
+ echo ""
871
+ echo "Step 2/7: Core Rules → $CLAUDE_DIR/rules/"
872
+ ! $DRY_RUN && mkdir -p "$CLAUDE_DIR/rules"
873
+ for f in "$SCRIPT_DIR"/rules/core/*.md; do
874
+ name=$(basename "$f")
875
+ target="$CLAUDE_DIR/rules/$name"
876
+ ! $DRY_RUN && cp "$f" "$target"
877
+ installed_rules=$((installed_rules + 1))
878
+ track_file "rules/$name"
879
+ done
880
+ echo " ✓ $installed_rules core rules"
881
+
882
+ echo ""
883
+ echo "Step 3/7: Hooks → $CLAUDE_DIR/hooks/"
884
+ ! $DRY_RUN && mkdir -p "$CLAUDE_DIR/hooks"
885
+ for f in "$SCRIPT_DIR"/hooks/*.py; do
886
+ name=$(basename "$f")
887
+ target="$CLAUDE_DIR/hooks/$name"
888
+ if ! $DRY_RUN; then
889
+ install_file "$f" "$target"
890
+ if ! $USE_SYMLINK; then
891
+ chmod +x "$target"
892
+ fi
893
+ fi
894
+ if python3 -c "import py_compile; py_compile.compile('$f', doraise=True)" 2>/dev/null; then
895
+ echo " ✓ $name"
896
+ else
897
+ echo " ❌ $name (SYNTAX ERROR)"
898
+ hook_errors=$((hook_errors + 1))
899
+ ERRORS=$((ERRORS + 1))
900
+ fi
901
+ installed_hooks=$((installed_hooks + 1))
902
+ track_file "hooks/$name"
903
+ done
904
+ ! $DRY_RUN && echo "$VERSION" > "$CLAUDE_DIR/hooks/.omg-version"
905
+ echo " ✓ $installed_hooks hooks ($hook_errors errors)"
906
+
907
+ echo ""
908
+ echo "Step 4/7: Agents → $CLAUDE_DIR/agents/"
909
+ ! $DRY_RUN && mkdir -p "$CLAUDE_DIR/agents"
910
+ for f in "$SCRIPT_DIR"/agents/*.md; do
911
+ name=$(basename "$f")
912
+ target="$CLAUDE_DIR/agents/$name"
913
+ if ! $DRY_RUN; then
914
+ [ -f "$target" ] && [ -z "$existing_ver" ] && cp "$target" "$target.bak.$BACKUP_TS"
915
+ install_file "$f" "$target"
916
+ fi
917
+ echo " ✓ $name"
918
+ installed_agents=$((installed_agents + 1))
919
+ track_file "agents/$name"
920
+ done
921
+ echo " ✓ $installed_agents agents"
922
+
923
+ echo ""
924
+ echo "Step 5/7: Commands → $CLAUDE_DIR/commands/"
925
+ ! $DRY_RUN && mkdir -p "$CLAUDE_DIR/commands"
926
+ for f in "$SCRIPT_DIR"/commands/*.md; do
927
+ name=$(basename "$f")
928
+ if [[ "$name" == *omc* ]]; then
929
+ echo " - /$(basename "$name" .md) (skipped: OMC alias commands are unsupported)"
930
+ continue
931
+ fi
932
+ target="$CLAUDE_DIR/commands/$name"
933
+ if [ -f "$target" ] && ! is_omg_managed_command_file "$target"; then
934
+ echo " ~ /$(basename "$name" .md) (kept existing custom command)"
935
+ continue
936
+ fi
937
+ if ! $DRY_RUN; then
938
+ install_file "$f" "$target"
939
+ if ! $USE_SYMLINK; then
940
+ mark_omg_managed_command_file "$target"
941
+ fi
942
+ fi
943
+ echo " ✓ /$(basename "$name" .md)"
944
+ installed_cmds=$((installed_cmds + 1))
945
+ track_file "commands/$name"
946
+ done
947
+ echo ""
948
+ echo "Step 6/7: Settings + Templates..."
949
+ MERGE="$SCRIPT_DIR/scripts/settings-merge.py"
950
+ TARGET="$CLAUDE_DIR/settings.json"
951
+ SOURCE="$SCRIPT_DIR/settings.json"
952
+ if ! $DRY_RUN; then
953
+ if [ ! -f "$TARGET" ]; then
954
+ cp "$SOURCE" "$TARGET"
955
+ echo " ✓ Created settings.json"
956
+ else
957
+ if [ "$MERGE_POLICY" = "skip" ]; then
958
+ echo " ⊘ Skipped settings merge (--merge-policy=skip)"
959
+ elif [ "$MERGE_POLICY" = "apply" ] || $NON_INTERACTIVE; then
960
+ python3 "$MERGE" "$TARGET" "$SOURCE"
961
+ echo " ✓ Settings merged (auto)"
962
+ else
963
+ echo " Merging settings.json..."
964
+ python3 "$MERGE" "$TARGET" "$SOURCE" --dry-run 2>&1 | head -5 | sed 's/^/ /'
965
+ echo ""
966
+ read -p " Apply merge? [Y/n] " -n 1 -r
967
+ echo ""
968
+ if [[ ! $REPLY =~ ^[Nn]$ ]]; then
969
+ python3 "$MERGE" "$TARGET" "$SOURCE"
970
+ echo " ✓ Settings merged"
971
+ else
972
+ echo " ⊘ Skipped (manual merge needed)"
973
+ fi
974
+ fi
975
+ fi
976
+
977
+ if $USE_SYMLINK; then
978
+ # In symlink mode, link entire directories for templates
979
+ if [ -e "$CLAUDE_DIR/templates/omg" ] || [ -L "$CLAUDE_DIR/templates/omg" ]; then
980
+ rm -rf "$CLAUDE_DIR/templates/omg"
981
+ fi
982
+ ln -s "$SCRIPT_DIR/templates" "$CLAUDE_DIR/templates/omg"
983
+ # Also link contextual rules
984
+ mkdir -p "$CLAUDE_DIR/templates/omg/contextual-rules"
985
+ for cr in "$SCRIPT_DIR"/rules/contextual/*.md; do
986
+ [ -f "$cr" ] && track_file "templates/omg/contextual-rules/$(basename "$cr")"
987
+ done
988
+ else
989
+ mkdir -p "$CLAUDE_DIR/templates/omg"
990
+ cp "$SCRIPT_DIR"/templates/* "$CLAUDE_DIR/templates/omg/" 2>/dev/null || true
991
+ for t in "$SCRIPT_DIR"/templates/*; do
992
+ [ -f "$t" ] && track_file "templates/omg/$(basename "$t")"
993
+ done
994
+ mkdir -p "$CLAUDE_DIR/templates/omg/contextual-rules"
995
+ cp "$SCRIPT_DIR"/rules/contextual/*.md "$CLAUDE_DIR/templates/omg/contextual-rules/" 2>/dev/null || true
996
+ for cr in "$SCRIPT_DIR"/rules/contextual/*.md; do
997
+ [ -f "$cr" ] && track_file "templates/omg/contextual-rules/$(basename "$cr")"
998
+ done
999
+ fi
1000
+ mkdir -p "$CLAUDE_DIR/templates/omg/state/memory"
1001
+ mkdir -p "$CLAUDE_DIR/templates/omg/state/learnings"
1002
+ mkdir -p "$CLAUDE_DIR/templates/omg/state/ledger"
1003
+ echo " \u2713 State directory templates (memory, learnings, ledger)"
1004
+ echo " \u2713 Templates + contextual rules"
1005
+
1006
+ if $USE_SYMLINK; then
1007
+ # In symlink mode, link runtime directories instead of copying
1008
+ mkdir -p "$CLAUDE_DIR/omg-runtime/scripts"
1009
+ install_file "$SCRIPT_DIR/scripts/omg.py" "$CLAUDE_DIR/omg-runtime/scripts/omg.py"
1010
+
1011
+ [[ "$CLAUDE_DIR/omg-runtime/runtime" == "$CLAUDE_DIR"* ]] || { echo "ERROR: symlink target outside expected directory: $CLAUDE_DIR/omg-runtime/runtime" >&2; exit 1; }
1012
+ [[ "$CLAUDE_DIR/omg-runtime/hooks" == "$CLAUDE_DIR"* ]] || { echo "ERROR: symlink target outside expected directory: $CLAUDE_DIR/omg-runtime/hooks" >&2; exit 1; }
1013
+ [[ "$CLAUDE_DIR/omg-runtime/lab" == "$CLAUDE_DIR"* ]] || { echo "ERROR: symlink target outside expected directory: $CLAUDE_DIR/omg-runtime/lab" >&2; exit 1; }
1014
+
1015
+ rm -rf "$CLAUDE_DIR/omg-runtime/runtime" "$CLAUDE_DIR/omg-runtime/hooks" "$CLAUDE_DIR/omg-runtime/lab"
1016
+ ln -s "$SCRIPT_DIR/runtime" "$CLAUDE_DIR/omg-runtime/runtime"
1017
+ ln -s "$SCRIPT_DIR/hooks" "$CLAUDE_DIR/omg-runtime/hooks"
1018
+ ln -s "$SCRIPT_DIR/lab" "$CLAUDE_DIR/omg-runtime/lab"
1019
+
1020
+ [[ "$CLAUDE_DIR/omg-runtime" == "$CLAUDE_DIR"* ]] || { echo "ERROR: rm -rf target outside expected directory: $CLAUDE_DIR/omg-runtime" >&2; exit 1; }
1021
+ find "$CLAUDE_DIR/omg-runtime" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
1022
+ find "$CLAUDE_DIR/omg-runtime" -name "*.pyc" -delete 2>/dev/null || true
1023
+ echo " ✓ Portable runtime → $CLAUDE_DIR/omg-runtime (symlinked to $SCRIPT_DIR)"
1024
+ else
1025
+ mkdir -p "$CLAUDE_DIR/omg-runtime/scripts"
1026
+ cp "$SCRIPT_DIR/scripts/omg.py" "$CLAUDE_DIR/omg-runtime/scripts/omg.py"
1027
+ [[ "$CLAUDE_DIR/omg-runtime/runtime" == "$CLAUDE_DIR"* ]] || { echo "ERROR: rm -rf target outside expected directory: $CLAUDE_DIR/omg-runtime/runtime" >&2; exit 1; }
1028
+ rm -rf "$CLAUDE_DIR/omg-runtime/runtime" "$CLAUDE_DIR/omg-runtime/hooks" "$CLAUDE_DIR/omg-runtime/lab"
1029
+ cp -R "$SCRIPT_DIR/runtime" "$CLAUDE_DIR/omg-runtime/"
1030
+ cp -R "$SCRIPT_DIR/hooks" "$CLAUDE_DIR/omg-runtime/"
1031
+ cp -R "$SCRIPT_DIR/lab" "$CLAUDE_DIR/omg-runtime/"
1032
+ [[ "$CLAUDE_DIR/omg-runtime" == "$CLAUDE_DIR"* ]] || { echo "ERROR: rm -rf target outside expected directory: $CLAUDE_DIR/omg-runtime" >&2; exit 1; }
1033
+ find "$CLAUDE_DIR/omg-runtime" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
1034
+ find "$CLAUDE_DIR/omg-runtime" -name "*.pyc" -delete 2>/dev/null || true
1035
+ echo " ✓ Portable runtime → $CLAUDE_DIR/omg-runtime"
1036
+ fi
1037
+ if $INSTALL_AS_PLUGIN; then
1038
+ install_plugin_bundle
1039
+ fi
1040
+ else
1041
+ echo " (would merge settings.json + copy templates + provision portable runtime)"
1042
+ if $INSTALL_AS_PLUGIN; then
1043
+ install_plugin_bundle
1044
+ fi
1045
+ fi
1046
+
1047
+
1048
+ echo ""
1049
+ echo "Step 7/7: Reconcile stale files..."
1050
+ reconcile_stale_files
1051
+ write_omg_manifest
1052
+
1053
+ echo ""
1054
+ echo "═══════════════════════════════════════════════════════════════"
1055
+ if [ $ERRORS -eq 0 ]; then
1056
+ echo " ✅ OMG v1 ${ACTION} completed successfully"
1057
+ else
1058
+ echo " ⚠ OMG v1 ${ACTION} completed with $ERRORS error(s)"
1059
+ fi
1060
+ echo "═══════════════════════════════════════════════════════════════"
1061
+ echo ""
1062
+ echo " Files: $installed_rules rules, $installed_hooks hooks, $installed_agents agents, $installed_cmds commands"
1063
+ echo " Version: $VERSION"
1064
+ if $USE_SYMLINK; then
1065
+ echo " Mode: Symlink (live updates from $SCRIPT_DIR)"
1066
+ echo " Source: $SCRIPT_DIR"
1067
+ elif $FRESH_INSTALL; then
1068
+ echo " Mode: Fresh reinstall"
1069
+ elif [ -n "$existing_ver" ]; then
1070
+ echo " Upgraded: $existing_ver → $VERSION"
1071
+ echo " Backup: $BACKUP_DIR"
1072
+ fi
1073
+ echo ""
1074
+ if $DRY_RUN; then
1075
+ echo " *** DRY RUN — no files were changed ***"
1076
+ echo " Run without --dry-run to apply changes."
1077
+ fi
1078
+ }
1079
+
1080
+ main() {
1081
+ parse_args "$@"
1082
+ prompt_start_action
1083
+ case "$ACTION" in
1084
+ uninstall) run_uninstall ;;
1085
+ install|update|reinstall) run_install_like ;;
1086
+ *)
1087
+ usage
1088
+ exit 1
1089
+ ;;
1090
+ esac
1091
+ }
1092
+
1093
+ main "$@"