claws-code 0.8.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.
Files changed (180) hide show
  1. package/.claude/commands/claws-auto.md +90 -0
  2. package/.claude/commands/claws-bin.md +28 -0
  3. package/.claude/commands/claws-cleanup.md +28 -0
  4. package/.claude/commands/claws-do.md +82 -0
  5. package/.claude/commands/claws-fix.md +40 -0
  6. package/.claude/commands/claws-goal.md +111 -0
  7. package/.claude/commands/claws-help.md +54 -0
  8. package/.claude/commands/claws-plan.md +103 -0
  9. package/.claude/commands/claws-report.md +29 -0
  10. package/.claude/commands/claws-status.md +37 -0
  11. package/.claude/commands/claws-update.md +32 -0
  12. package/.claude/commands/claws.md +64 -0
  13. package/.claude/rules/claws-default-behavior.md +76 -0
  14. package/.claude/settings.json +112 -0
  15. package/.claude/settings.local.json +19 -0
  16. package/.claude/skills/claws-auto-engine/SKILL.md +97 -0
  17. package/.claude/skills/claws-goal-tracker/SKILL.md +106 -0
  18. package/.claude/skills/claws-prompt-templates/SKILL.md +203 -0
  19. package/.claude/skills/claws-wave-lead/SKILL.md +126 -0
  20. package/.claude/skills/claws-wave-subworker/SKILL.md +60 -0
  21. package/CHANGELOG.md +1949 -0
  22. package/LICENSE +21 -0
  23. package/README.md +420 -0
  24. package/bin/cli.js +84 -0
  25. package/cli.js +223 -0
  26. package/docs/ARCHITECTURE.md +511 -0
  27. package/docs/event-protocol.md +588 -0
  28. package/docs/features.md +562 -0
  29. package/docs/guide.md +891 -0
  30. package/docs/index.html +716 -0
  31. package/docs/protocol.md +323 -0
  32. package/extension/.vscodeignore +15 -0
  33. package/extension/CHANGELOG.md +1906 -0
  34. package/extension/LICENSE +21 -0
  35. package/extension/README.md +137 -0
  36. package/extension/docs/features.md +424 -0
  37. package/extension/docs/protocol.md +197 -0
  38. package/extension/esbuild.mjs +25 -0
  39. package/extension/icon.png +0 -0
  40. package/extension/native/.metadata.json +10 -0
  41. package/extension/native/node-pty/LICENSE +69 -0
  42. package/extension/native/node-pty/README.md +165 -0
  43. package/extension/native/node-pty/lib/conpty_console_list_agent.js +16 -0
  44. package/extension/native/node-pty/lib/conpty_console_list_agent.js.map +1 -0
  45. package/extension/native/node-pty/lib/eventEmitter2.js +47 -0
  46. package/extension/native/node-pty/lib/eventEmitter2.js.map +1 -0
  47. package/extension/native/node-pty/lib/index.js +52 -0
  48. package/extension/native/node-pty/lib/index.js.map +1 -0
  49. package/extension/native/node-pty/lib/interfaces.js +7 -0
  50. package/extension/native/node-pty/lib/interfaces.js.map +1 -0
  51. package/extension/native/node-pty/lib/shared/conout.js +11 -0
  52. package/extension/native/node-pty/lib/shared/conout.js.map +1 -0
  53. package/extension/native/node-pty/lib/terminal.js +190 -0
  54. package/extension/native/node-pty/lib/terminal.js.map +1 -0
  55. package/extension/native/node-pty/lib/types.js +7 -0
  56. package/extension/native/node-pty/lib/types.js.map +1 -0
  57. package/extension/native/node-pty/lib/unixTerminal.js +346 -0
  58. package/extension/native/node-pty/lib/unixTerminal.js.map +1 -0
  59. package/extension/native/node-pty/lib/utils.js +39 -0
  60. package/extension/native/node-pty/lib/utils.js.map +1 -0
  61. package/extension/native/node-pty/lib/windowsConoutConnection.js +125 -0
  62. package/extension/native/node-pty/lib/windowsConoutConnection.js.map +1 -0
  63. package/extension/native/node-pty/lib/windowsPtyAgent.js +320 -0
  64. package/extension/native/node-pty/lib/windowsPtyAgent.js.map +1 -0
  65. package/extension/native/node-pty/lib/windowsTerminal.js +199 -0
  66. package/extension/native/node-pty/lib/windowsTerminal.js.map +1 -0
  67. package/extension/native/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  68. package/extension/native/node-pty/lib/worker/conoutSocketWorker.js.map +1 -0
  69. package/extension/native/node-pty/package.json +64 -0
  70. package/extension/native/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  71. package/extension/native/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  72. package/extension/native/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  73. package/extension/native/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  74. package/extension/native/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
  75. package/extension/native/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
  76. package/extension/native/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  77. package/extension/native/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  78. package/extension/native/node-pty/prebuilds/win32-arm64/pty.node +0 -0
  79. package/extension/native/node-pty/prebuilds/win32-arm64/winpty-agent.exe +0 -0
  80. package/extension/native/node-pty/prebuilds/win32-arm64/winpty.dll +0 -0
  81. package/extension/native/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
  82. package/extension/native/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
  83. package/extension/native/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  84. package/extension/native/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  85. package/extension/native/node-pty/prebuilds/win32-x64/pty.node +0 -0
  86. package/extension/native/node-pty/prebuilds/win32-x64/winpty-agent.exe +0 -0
  87. package/extension/native/node-pty/prebuilds/win32-x64/winpty.dll +0 -0
  88. package/extension/package-lock.json +605 -0
  89. package/extension/package.json +343 -0
  90. package/extension/scripts/bundle-native.mjs +104 -0
  91. package/extension/scripts/deploy-dev.mjs +60 -0
  92. package/extension/src/ansi-strip.ts +52 -0
  93. package/extension/src/backends/vscode/claws-pty.ts +483 -0
  94. package/extension/src/backends/vscode/status-bar.ts +99 -0
  95. package/extension/src/backends/vscode/vscode-backend.ts +282 -0
  96. package/extension/src/capture-store.ts +125 -0
  97. package/extension/src/event-log.ts +629 -0
  98. package/extension/src/event-schemas.ts +478 -0
  99. package/extension/src/extension.js +492 -0
  100. package/extension/src/extension.ts +873 -0
  101. package/extension/src/lifecycle-engine.ts +60 -0
  102. package/extension/src/lifecycle-rules.ts +171 -0
  103. package/extension/src/lifecycle-store.ts +506 -0
  104. package/extension/src/peer-registry.ts +176 -0
  105. package/extension/src/pipeline-registry.ts +82 -0
  106. package/extension/src/platform.ts +64 -0
  107. package/extension/src/protocol.ts +532 -0
  108. package/extension/src/server-config.ts +98 -0
  109. package/extension/src/server.ts +2210 -0
  110. package/extension/src/task-registry.ts +51 -0
  111. package/extension/src/terminal-backend.ts +211 -0
  112. package/extension/src/terminal-manager.ts +395 -0
  113. package/extension/src/topic-registry.ts +70 -0
  114. package/extension/src/topic-utils.ts +46 -0
  115. package/extension/src/transport.ts +45 -0
  116. package/extension/src/uninstall-cleanup.ts +232 -0
  117. package/extension/src/wave-registry.ts +314 -0
  118. package/extension/src/websocket-transport.ts +153 -0
  119. package/extension/tsconfig.json +23 -0
  120. package/lib/capabilities.js +145 -0
  121. package/lib/dry-run.js +43 -0
  122. package/lib/install.js +1018 -0
  123. package/lib/mcp-setup.js +92 -0
  124. package/lib/platform.js +240 -0
  125. package/lib/preflight.js +152 -0
  126. package/lib/shell-hook.js +343 -0
  127. package/lib/uninstall.js +162 -0
  128. package/lib/verify.js +166 -0
  129. package/mcp_server.js +3529 -0
  130. package/package.json +48 -0
  131. package/rules/claws-default-behavior.md +72 -0
  132. package/scripts/_helpers/atomic-file.mjs +137 -0
  133. package/scripts/_helpers/fix-repair.js +64 -0
  134. package/scripts/_helpers/json-safe.mjs +218 -0
  135. package/scripts/bump-version.sh +84 -0
  136. package/scripts/codegen/gen-docs.mjs +61 -0
  137. package/scripts/codegen/gen-json-schema.mjs +62 -0
  138. package/scripts/codegen/gen-mcp-tools.mjs +358 -0
  139. package/scripts/codegen/gen-types.mjs +172 -0
  140. package/scripts/codegen/index.mjs +42 -0
  141. package/scripts/dev-hooks/check-extension-dirs.js +77 -0
  142. package/scripts/dev-hooks/check-open-claws-terminals.js +70 -0
  143. package/scripts/dev-hooks/check-stale-main.js +55 -0
  144. package/scripts/dev-hooks/check-tag-pushed.js +51 -0
  145. package/scripts/dev-hooks/check-tag-vs-main.js +56 -0
  146. package/scripts/dev-vsix-install.sh +60 -0
  147. package/scripts/fix.sh +702 -0
  148. package/scripts/gen-client-types.mjs +81 -0
  149. package/scripts/git-hooks/pre-commit +31 -0
  150. package/scripts/hooks/lifecycle-state.js +61 -0
  151. package/scripts/hooks/package.json +4 -0
  152. package/scripts/hooks/post-tool-use-claws.js +292 -0
  153. package/scripts/hooks/pre-bash-no-verify-block.js +72 -0
  154. package/scripts/hooks/pre-tool-use-claws.js +206 -0
  155. package/scripts/hooks/session-start-claws.js +97 -0
  156. package/scripts/hooks/stop-claws.js +88 -0
  157. package/scripts/inject-claude-md.js +205 -0
  158. package/scripts/inject-dev-hooks.js +96 -0
  159. package/scripts/inject-global-claude-md.js +140 -0
  160. package/scripts/inject-settings-hooks.js +370 -0
  161. package/scripts/install.ps1 +146 -0
  162. package/scripts/install.sh +1729 -0
  163. package/scripts/monitor-arm-watch.js +155 -0
  164. package/scripts/rebuild-node-pty.sh +245 -0
  165. package/scripts/report.sh +232 -0
  166. package/scripts/shell-hook.fish +164 -0
  167. package/scripts/shell-hook.ps1 +33 -0
  168. package/scripts/shell-hook.sh +232 -0
  169. package/scripts/stream-events.js +399 -0
  170. package/scripts/terminal-wrapper.sh +36 -0
  171. package/scripts/test-enforcement.sh +132 -0
  172. package/scripts/test-install.sh +174 -0
  173. package/scripts/test-installer-parity.sh +135 -0
  174. package/scripts/test-template-enforcement.sh +76 -0
  175. package/scripts/uninstall.sh +143 -0
  176. package/scripts/update.sh +337 -0
  177. package/scripts/verify-release.sh +323 -0
  178. package/scripts/verify-wrapped.sh +194 -0
  179. package/templates/CLAUDE.global.md +135 -0
  180. package/templates/CLAUDE.project.md +37 -0
@@ -0,0 +1,1729 @@
1
+ #!/usr/bin/env bash
2
+ # Claws — project-local installer
3
+ # Usage: cd /path/to/project && bash <(curl -fsSL https://raw.githubusercontent.com/neunaha/claws/main/scripts/install.sh)
4
+ #
5
+ # Env overrides:
6
+ # CLAWS_DIR=/path Where to clone the source (default: ~/.claws-src)
7
+ # CLAWS_EDITOR=cursor|insiders|windsurf|skip Which editor's extensions dir to use
8
+ # CLAWS_SKIP_MCP=1 Don't write .mcp.json
9
+ # CLAWS_GLOBAL_MCP=1 Also register globally in ~/.claude/settings.json
10
+ # CLAWS_GLOBAL_CONFIG=1 Also write commands/skills/rules into ~/.claude/
11
+ # CLAWS_DEBUG=1 Enable bash -x trace
12
+ # CLAWS_NO_LOG=1 Disable the /tmp/claws-install-*.log file
13
+ # CLAWS_STRICT=1 Hard-block long-running Bash via PreToolUse hook (v0.6.4+)
14
+ # CLAWS_NO_GLOBAL_HOOKS=1 Skip ~/.claude/settings.json hook registration (test/CI safe)
15
+
16
+ # ─── Strict-ish mode ────────────────────────────────────────────────────────
17
+ # -e: exit on unhandled error
18
+ # -o pipefail: catch errors inside pipes
19
+ # We do NOT use -u because optional env vars are allowed to be unset.
20
+ set -eo pipefail
21
+
22
+ # If CLAWS_DEBUG=1, trace every line.
23
+ # Unset any env vars that may contain secrets before enabling xtrace
24
+ # so they don't appear in the trace log.
25
+ if [ "${CLAWS_DEBUG:-0}" = "1" ]; then
26
+ { unset AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_ACCESS_KEY_ID \
27
+ GITHUB_TOKEN NPM_TOKEN ANTHROPIC_API_KEY OPENAI_API_KEY \
28
+ CLAWS_TOKEN HOMEBREW_GITHUB_API_TOKEN; } 2>/dev/null || true
29
+ set -x
30
+ fi
31
+
32
+ # ─── Logging ────────────────────────────────────────────────────────────────
33
+ CLAWS_LOG="${CLAWS_LOG:-/tmp/claws-install-$(date +%Y%m%d-%H%M%S)-$$.log}"
34
+ if [ "${CLAWS_NO_LOG:-0}" != "1" ]; then
35
+ # Tee all stdout and stderr through the log file.
36
+ # Using process substitution so the log captures both the script's own
37
+ # output and anything child processes emit.
38
+ exec > >(tee -a "$CLAWS_LOG") 2> >(tee -a "$CLAWS_LOG" >&2)
39
+ trap 'printf "\n\nInstall log saved to: %s\n" "$CLAWS_LOG" >&2' EXIT
40
+ fi
41
+
42
+ # ─── Colors and progress helpers ────────────────────────────────────────────
43
+ if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
44
+ C_RESET='\033[0m'; C_BOLD='\033[1m'
45
+ C_BLUE='\033[0;34m'; C_GREEN='\033[0;32m'; C_YELLOW='\033[0;33m'; C_RED='\033[0;31m'; C_DIM='\033[2m'
46
+ else
47
+ C_RESET=''; C_BOLD=''; C_BLUE=''; C_GREEN=''; C_YELLOW=''; C_RED=''; C_DIM=''
48
+ fi
49
+
50
+ STEP_NUM=0
51
+ STEP_TOTAL=8
52
+ step() { STEP_NUM=$((STEP_NUM+1)); printf "\n${C_BOLD}${C_BLUE}[%d/%d]${C_RESET} %s\n" "$STEP_NUM" "$STEP_TOTAL" "$*"; }
53
+ ok() { printf " ${C_GREEN}✓${C_RESET} %s\n" "$*"; }
54
+ warn() { printf " ${C_YELLOW}!${C_RESET} %s\n" "$*"; }
55
+ bad() { printf " ${C_RED}✗${C_RESET} %s\n" "$*"; }
56
+ info() { printf " ${C_DIM}%s${C_RESET}\n" "$*"; }
57
+ die() { bad "$*"; exit 1; }
58
+
59
+ # Treat any unhandled error as fatal with a clear message.
60
+ trap 'ec=$?; if [ $ec -ne 0 ]; then printf "\n${C_RED}${C_BOLD}INSTALL FAILED${C_RESET} at line $LINENO (exit $ec). See log: %s\n" "$CLAWS_LOG" >&2; fi' ERR
61
+
62
+ # ─── Globals ────────────────────────────────────────────────────────────────
63
+ REPO="https://github.com/neunaha/claws.git"
64
+ INSTALL_DIR="${CLAWS_DIR:-$HOME/.claws-src}"
65
+ CLAWS_REF="${CLAWS_REF:-main}"
66
+ USER_PWD="$(pwd)"
67
+ PLATFORM="$(uname -s)"
68
+ case "$PLATFORM" in
69
+ MINGW*|MSYS*|CYGWIN*)
70
+ die "Windows (Git Bash/MSYS/Cygwin) not supported — use WSL2: https://aka.ms/wslinstall" ;;
71
+ esac
72
+
73
+ # ─── Banner ─────────────────────────────────────────────────────────────────
74
+ cat <<BANNER
75
+
76
+ ${C_BOLD}╔═══════════════════════════════════════════╗
77
+ ║ ║
78
+ ║ CLAWS — Terminal Control Bridge ║
79
+ ║ Project-local orchestration setup ║
80
+ ║ ║
81
+ ╚═══════════════════════════════════════════╝${C_RESET}
82
+
83
+ BANNER
84
+
85
+ # ─── Project safety check ──────────────────────────────────────────────────
86
+ is_safe_project_dir() {
87
+ case "$1" in
88
+ "" | "/" | "$HOME" | "/tmp" | "/tmp/" | "/var" | "/var/" \
89
+ | "/opt" | "/opt/" | "/Users" | "/Users/" | "/etc" | "/etc/") return 1 ;;
90
+ esac
91
+ return 0
92
+ }
93
+
94
+ if is_safe_project_dir "$USER_PWD"; then
95
+ PROJECT_ROOT="$USER_PWD"
96
+ PROJECT_INSTALL=1
97
+ echo " Installing into project: $PROJECT_ROOT"
98
+ else
99
+ PROJECT_ROOT=""
100
+ PROJECT_INSTALL=0
101
+ warn "$USER_PWD is not a safe project dir — skipping project-local install."
102
+ info "For a full per-project setup: cd into your project and re-run."
103
+ fi
104
+ echo ""
105
+
106
+ # ─── Preflight: dependencies ───────────────────────────────────────────────
107
+ # Every dependency the installer or the extension's build chain touches is
108
+ # checked here with a specific install command for the ones that are missing.
109
+ # Fatal checks (die) are things Claws literally cannot work without. Warning
110
+ # checks are things that degrade specific features.
111
+ echo "Checking dependencies..."
112
+ # Disk space: clone + npm install + native build + VSIX needs ~500MB
113
+ _avail_kb=$(df -k "$HOME" 2>/dev/null | awk 'NR==2{print $4}' || echo "")
114
+ if [ -n "$_avail_kb" ] && [ "$_avail_kb" -lt 512000 ] 2>/dev/null; then
115
+ warn "Low disk space: $(( _avail_kb / 1024 ))MB free in \$HOME — Claws needs ~500MB for clone, build, and VSIX packaging"
116
+ fi
117
+ unset _avail_kb
118
+
119
+ # ── Required: git ──────────────────────────────────────────────────────────
120
+ if command -v git &>/dev/null; then
121
+ GIT_VERSION_STR="$(git --version | awk '{print $3}')"
122
+ GIT_MAJOR="$(echo "$GIT_VERSION_STR" | cut -d. -f1)"
123
+ if [ "${GIT_MAJOR:-0}" -lt 2 ] 2>/dev/null; then
124
+ die "git $GIT_VERSION_STR too old — Claws requires git 2+. Upgrade via your package manager."
125
+ fi
126
+ ok "git ($GIT_VERSION_STR)"
127
+ else
128
+ case "$PLATFORM" in
129
+ Darwin) die "git not found — install with: xcode-select --install" ;;
130
+ Linux) die "git not found — install with: sudo apt install git (or your distro's package manager)" ;;
131
+ *) die "git not found — install from https://git-scm.com/" ;;
132
+ esac
133
+ fi
134
+
135
+ # ── Required: Node.js 18+ (for MCP server + extension build) ───────────────
136
+ if ! command -v node &>/dev/null; then
137
+ [ -d "$HOME/.nvm" ] && info "nvm detected — run: nvm use --lts then re-run this installer."
138
+ [ -d "$HOME/.fnm" ] && info "fnm detected — run: fnm use --lts then re-run this installer."
139
+ case "$PLATFORM" in
140
+ Darwin) die "node not found — install with: brew install node (or from https://nodejs.org/)" ;;
141
+ Linux) die "node not found — install with: sudo apt install nodejs (or use nvm: https://github.com/nvm-sh/nvm)" ;;
142
+ *) die "node not found — install from https://nodejs.org/" ;;
143
+ esac
144
+ fi
145
+ NODE_MAJOR=$(node -e "console.log(process.versions.node.split('.')[0])" 2>/dev/null || echo "0")
146
+ if [ "$NODE_MAJOR" -lt 18 ] 2>/dev/null; then
147
+ die "node $(node --version) is too old — Claws requires Node 18+. Upgrade via nvm or your package manager."
148
+ fi
149
+ ok "node ($(node --version))"
150
+
151
+ # ── Required: npm (bundled with node, but verify separately) ───────────────
152
+ if ! command -v npm &>/dev/null; then
153
+ die "npm not found — should ship with Node. Reinstall Node or install npm separately."
154
+ fi
155
+ ok "npm ($(npm --version))"
156
+ NPM_MAJOR=$(npm --version 2>/dev/null | cut -d. -f1 || echo "0")
157
+ if [ "$NPM_MAJOR" -lt 7 ] 2>/dev/null; then
158
+ die "npm $(npm --version) too old — requires npm 7+. Upgrade: npm install -g npm"
159
+ fi
160
+
161
+ # ── Optional but strongly recommended: C++ toolchain for native modules ────
162
+ # node-pty (optional dep) compiles C++ when no prebuilt binary matches the
163
+ # user's Node version. Without it, wrapped terminals fall back to pipe-mode
164
+ # which doesn't render TUIs correctly. Warn proactively so the user can fix
165
+ # it BEFORE npm install rather than discovering the silent failure later.
166
+ TOOLCHAIN_OK=1
167
+ case "$PLATFORM" in
168
+ Darwin)
169
+ if xcode-select -p &>/dev/null; then
170
+ ok "Xcode Command Line Tools ($(xcode-select -p))"
171
+ else
172
+ warn "Xcode Command Line Tools not installed"
173
+ info "install with: xcode-select --install"
174
+ info "without this, wrapped terminals will use degraded pipe-mode (TUIs render poorly)"
175
+ TOOLCHAIN_OK=0
176
+ fi
177
+ ;;
178
+ Linux)
179
+ if command -v g++ &>/dev/null && command -v make &>/dev/null; then
180
+ ok "C++ toolchain (g++ $(g++ -dumpversion 2>/dev/null), make $(make --version | head -1 | awk '{print $3}'))"
181
+ else
182
+ warn "C++ toolchain missing — wrapped terminals may fall back to pipe-mode"
183
+ info "install with: sudo apt install build-essential (Debian/Ubuntu)"
184
+ info " or: sudo dnf install gcc-c++ make (Fedora/RHEL)"
185
+ TOOLCHAIN_OK=0
186
+ fi
187
+ ;;
188
+ esac
189
+
190
+ # ── Optional: python3 for node-gyp ─────────────────────────────────────────
191
+ # node-gyp spawns python3 to run its gyp files. Missing python3 means
192
+ # node-pty source build will fail even if the C++ toolchain is present.
193
+ if command -v python3 &>/dev/null; then
194
+ ok "python3 ($(python3 --version 2>&1 | awk '{print $2}'))"
195
+ elif command -v python &>/dev/null && python --version 2>&1 | grep -q "Python 3"; then
196
+ ok "python ($(python --version 2>&1 | awk '{print $2}'))"
197
+ else
198
+ warn "python3 not found — needed by node-gyp for native module compilation"
199
+ case "$PLATFORM" in
200
+ Darwin) info "install with: brew install python3 (or xcode-select --install provides it)" ;;
201
+ Linux) info "install with: sudo apt install python3" ;;
202
+ esac
203
+ TOOLCHAIN_OK=0
204
+ fi
205
+
206
+ # ── Optional: bundled VS Code CLI (for VSIX install in step 2c) ────────────
207
+ # We don't fail if absent — the installer falls back to a symlink. But log
208
+ # what we'd use so /claws-report shows the editor state.
209
+ FOUND_EDITOR_CLIS=()
210
+ for pair in \
211
+ "code:/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" \
212
+ "code-insiders:/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code-insiders" \
213
+ "cursor:/Applications/Cursor.app/Contents/Resources/app/bin/cursor" \
214
+ "windsurf:/Applications/Windsurf.app/Contents/Resources/app/bin/windsurf"; do
215
+ label="${pair%%:*}"
216
+ bundled="${pair#*:}"
217
+ if command -v "$label" &>/dev/null; then
218
+ FOUND_EDITOR_CLIS+=("$label (PATH)")
219
+ elif [ -x "$bundled" ]; then
220
+ FOUND_EDITOR_CLIS+=("$label (bundled)")
221
+ fi
222
+ done
223
+ if [ "${#FOUND_EDITOR_CLIS[@]}" -gt 0 ]; then
224
+ ok "editor CLI(s): ${FOUND_EDITOR_CLIS[*]}"
225
+ else
226
+ warn "no editor CLI detected — extension will install via symlink fallback"
227
+ info "to enable VSIX install on macOS: open VS Code and run 'Shell Command: Install code command in PATH'"
228
+ fi
229
+
230
+ info "Platform: $PLATFORM $(uname -r)"
231
+ ARCH="$(uname -m)"
232
+ info "Architecture: $ARCH"
233
+
234
+ # ─── Platform/arch → prebuild key ──────────────────────────────────────────
235
+ # Maps uname -s / uname -m to the node-pty prebuild directory name used by
236
+ # bundle-native.mjs: darwin-x64 | darwin-arm64 | linux-x64 | linux-arm64.
237
+ # Mirrors lib/platform.js detectPlatformArch(); used by prebuilt-pty consumers.
238
+ CLAWS_PLATFORM_ARCH=""
239
+ case "$PLATFORM" in
240
+ Darwin)
241
+ case "$ARCH" in
242
+ arm64) CLAWS_PLATFORM_ARCH="darwin-arm64" ;;
243
+ *) CLAWS_PLATFORM_ARCH="darwin-x64" ;;
244
+ esac ;;
245
+ Linux)
246
+ case "$ARCH" in
247
+ aarch64|arm64) CLAWS_PLATFORM_ARCH="linux-arm64" ;;
248
+ *) CLAWS_PLATFORM_ARCH="linux-x64" ;;
249
+ esac ;;
250
+ *) CLAWS_PLATFORM_ARCH="linux-x64" ;;
251
+ esac
252
+ info "Prebuild key: $CLAWS_PLATFORM_ARCH"
253
+
254
+ info "Shell: $SHELL ($BASH_VERSION)"
255
+ info "Install log: $CLAWS_LOG"
256
+ [ "$TOOLCHAIN_OK" = "0" ] && warn "toolchain issues above — install will still run, but node-pty may not compile"
257
+ echo ""
258
+
259
+ # ─── Pre-install: Kill stale worker processes from prior sessions ──────────
260
+ # Returns true when a process has CLAWS_TERMINAL_CORR_ID in its environment.
261
+ # Worker children are spawned with that var; the outer orchestrator running
262
+ # this install does NOT have it — only worker children are killed.
263
+ _has_corr_id() {
264
+ local pid="$1"
265
+ case "$PLATFORM" in
266
+ Darwin)
267
+ # ps -E on macOS appends environment variables after each process line
268
+ ps -E -p "$pid" 2>/dev/null | grep -q "CLAWS_TERMINAL_CORR_ID" ;;
269
+ Linux)
270
+ # /proc/<pid>/environ is NUL-delimited; tr replaces NULs with newlines
271
+ tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | grep -q "CLAWS_TERMINAL_CORR_ID" ;;
272
+ *) return 1 ;;
273
+ esac
274
+ }
275
+
276
+ _claws_cleanup_stale_workers() {
277
+ local killed=0 cleared=0
278
+
279
+ if command -v pgrep &>/dev/null; then
280
+ # Kill stale mcp_server.js processes that match the project path + CORR_ID filter
281
+ if [ "$PROJECT_INSTALL" = "1" ]; then
282
+ local target_mcp="$PROJECT_ROOT/.claws-bin/mcp_server.js"
283
+ while IFS= read -r line; do
284
+ [ -z "$line" ] && continue
285
+ local pid="${line%% *}"
286
+ local cmdline="${line#* }"
287
+ # Full path match prevents collateral kills of other projects' MCP servers
288
+ case "$cmdline" in
289
+ *"$target_mcp"*) ;;
290
+ *) continue ;;
291
+ esac
292
+ # CORR_ID filter: only kill worker children, not the outer orchestrator
293
+ _has_corr_id "$pid" || continue
294
+ info "[claws-clean] kill PID $pid (stale worker mcp_server.js)"
295
+ kill -TERM "$pid" 2>/dev/null || true
296
+ sleep 2
297
+ kill -KILL "$pid" 2>/dev/null || true
298
+ killed=$((killed + 1))
299
+ done < <(pgrep -af 'mcp_server\.js' 2>/dev/null || true)
300
+ fi
301
+
302
+ # Kill stale spawn-helper processes (Mac/Linux only — Windows uses winpty/conpty)
303
+ while IFS= read -r line; do
304
+ [ -z "$line" ] && continue
305
+ local pid="${line%% *}"
306
+ _has_corr_id "$pid" || continue
307
+ info "[claws-clean] kill spawn-helper PID $pid"
308
+ kill -TERM "$pid" 2>/dev/null || true
309
+ killed=$((killed + 1))
310
+ done < <(pgrep -af 'spawn-helper' 2>/dev/null || true)
311
+ fi
312
+
313
+ # Clear stale .claws/sidecar.pid if the recorded PID is dead
314
+ if [ "$PROJECT_INSTALL" = "1" ] && [ -f "$PROJECT_ROOT/.claws/sidecar.pid" ]; then
315
+ local spid
316
+ spid="$(tr -d '[:space:]' < "$PROJECT_ROOT/.claws/sidecar.pid" 2>/dev/null || true)"
317
+ if [ -n "$spid" ] && ! kill -0 "$spid" 2>/dev/null; then
318
+ rm -f "$PROJECT_ROOT/.claws/sidecar.pid"
319
+ info "[claws-clean] cleared stale sidecar.pid (PID $spid dead)"
320
+ cleared=$((cleared + 1))
321
+ fi
322
+ fi
323
+
324
+ ok "[claws-clean] killed $killed stale processes, cleared $cleared stale pid files"
325
+ }
326
+
327
+ _claws_cleanup_stale_workers
328
+
329
+ # ─── Step 1: Clone or update ───────────────────────────────────────────────
330
+ step "Fetching Claws source"
331
+ if [ -d "$INSTALL_DIR/.git" ]; then
332
+ info "updating existing clone at $INSTALL_DIR (ref: $CLAWS_REF)"
333
+ PREV_SHA="$(cd "$INSTALL_DIR" && git rev-parse HEAD 2>/dev/null || echo 'unknown')"
334
+ # v0.7.11: install.sh OWNS $INSTALL_DIR (~/.claws-src by default). It is the
335
+ # script's working dir, not a user dev clone. Hard-reset is the right default —
336
+ # auto-generated build artifacts (.metadata.json bundledAt, etc.) and any other
337
+ # transient cruft just get wiped. Install Just Works(TM) in any folder.
338
+ #
339
+ # Contributors who want to use $INSTALL_DIR as a dev clone should set CLAWS_DIR
340
+ # to a different path (e.g., CLAWS_DIR=/Users/me/dev/claws bash <(curl ...)).
341
+ # That keeps their work isolated from the install script's working dir.
342
+ #
343
+ # CLAWS_DEV_PROTECT=1 opt-in re-enables the v0.7.9 dirty-tree guard for users
344
+ # who want explicit safety on $INSTALL_DIR (rare — they should be using
345
+ # CLAWS_DIR= override instead).
346
+ if [ "${CLAWS_DEV_PROTECT:-0}" = "1" ]; then
347
+ _claws_dirty="$(cd "$INSTALL_DIR" && git status --porcelain 2>/dev/null | head -c 1)"
348
+ if [ -n "$_claws_dirty" ] && [ "${CLAWS_FORCE_RESET:-0}" != "1" ]; then
349
+ bad "uncommitted changes — refusing to git reset ($INSTALL_DIR has local edits; CLAWS_DEV_PROTECT=1 active)."
350
+ bad "Choose one:"
351
+ bad " 1. Commit or stash your changes, then re-run."
352
+ bad " 2. Re-run with CLAWS_FORCE_RESET=1 to discard local changes."
353
+ bad " 3. Unset CLAWS_DEV_PROTECT (default: hard-reset is safe)."
354
+ bad " 4. Use a separate clone path: CLAWS_DIR=/tmp/claws-fresh bash <(curl ...)"
355
+ exit 1
356
+ fi
357
+ fi
358
+ if ( cd "$INSTALL_DIR" && git fetch origin "$CLAWS_REF" 2>>"$CLAWS_LOG" \
359
+ && git reset --hard --quiet "origin/$CLAWS_REF" ); then
360
+ NEW_SHA="$(cd "$INSTALL_DIR" && git rev-parse HEAD 2>/dev/null || echo 'unknown')"
361
+ if [ "$PREV_SHA" = "$NEW_SHA" ]; then
362
+ ok "already at origin/$CLAWS_REF (${NEW_SHA:0:7})"
363
+ else
364
+ ok "updated ${PREV_SHA:0:7} → ${NEW_SHA:0:7}"
365
+ fi
366
+ # GAP-3 (v0.7.8): if update.sh's --ff-only pull diverged and set GIT_PULL_OK=0,
367
+ # but our force-reset above succeeded, the source is now fresh — flip the flag back
368
+ # so CLAUDE.md re-injection (gated on GIT_PULL_OK) runs. Without this, users with a
369
+ # modified ~/.claws-src clone get fresh source but stale CLAUDE.md tool lists.
370
+ if [ "${GIT_PULL_OK:-1}" = "0" ]; then
371
+ info "force-reset succeeded after ff-only divergence — re-enabling CLAUDE.md injection"
372
+ GIT_PULL_OK=1
373
+ export GIT_PULL_OK
374
+ fi
375
+ git -C "$INSTALL_DIR" fsck --no-dangling 2>/dev/null || warn "clone integrity check failed — consider: rm -rf $INSTALL_DIR && re-run installer"
376
+ else
377
+ bad "failed to update $INSTALL_DIR (network error or corrupted clone)."
378
+ bad "Fix: rm -rf $INSTALL_DIR && re-run this installer."
379
+ exit 1
380
+ fi
381
+ elif [ -d "$INSTALL_DIR" ]; then
382
+ die "$INSTALL_DIR exists but is not a git clone — remove it or set CLAWS_DIR to a different path"
383
+ else
384
+ info "cloning $REPO → $INSTALL_DIR (ref: $CLAWS_REF, shallow)"
385
+ git clone --depth 1 --branch "$CLAWS_REF" "$REPO" "$INSTALL_DIR" 2>>"$CLAWS_LOG" \
386
+ || git clone --depth 1 "$REPO" "$INSTALL_DIR" 2>>"$CLAWS_LOG"
387
+ ok "cloned to $INSTALL_DIR (${CLAWS_REF})"
388
+ git -C "$INSTALL_DIR" fsck --no-dangling 2>/dev/null || warn "clone integrity check failed — consider: rm -rf $INSTALL_DIR && re-run installer"
389
+ fi
390
+
391
+ # ─── Step 2: Build + symlink extension ─────────────────────────────────────
392
+ step "Installing extension"
393
+
394
+ # 2a. Build
395
+ # Since Claws isn't on the VS Code Marketplace, the install script IS the
396
+ # release mechanism. Every run must produce a bundle that reflects whatever
397
+ # `extension/src/` is currently on main. Rebuild whenever:
398
+ # - the bundle is missing, OR
399
+ # - any `src/` file is newer than the bundle, OR
400
+ # - the current git HEAD doesn't match the SHA the bundle was built from,
401
+ # OR the user explicitly sets CLAWS_FORCE_BUILD=1.
402
+ # Build is ~seconds on a primed node_modules — rebuilding conservatively is
403
+ # cheaper than a stale bundle.
404
+ BUILD_OK=0
405
+ BUNDLE="$INSTALL_DIR/extension/dist/extension.js"
406
+ BUILD_SHA_FILE="$INSTALL_DIR/extension/dist/.build-sha"
407
+ CURRENT_SHA="$(cd "$INSTALL_DIR" && git rev-parse HEAD 2>/dev/null || echo 'nogit')"
408
+ LAST_BUILD_SHA="$(cat "$BUILD_SHA_FILE" 2>/dev/null || echo '')"
409
+
410
+ needs_build() {
411
+ [ "${CLAWS_FORCE_BUILD:-0}" = "1" ] && return 0
412
+ [ ! -f "$BUNDLE" ] && return 0
413
+ [ "$CURRENT_SHA" != "$LAST_BUILD_SHA" ] && return 0
414
+ # Any TS source newer than the bundle — catches local edits and any file
415
+ # changed by git pull, not just extension.ts.
416
+ if find "$INSTALL_DIR/extension/src" -type f \( -name '*.ts' -o -name '*.js' \) -newer "$BUNDLE" 2>/dev/null | grep -q .; then
417
+ return 0
418
+ fi
419
+ return 1
420
+ }
421
+
422
+ if command -v npm &>/dev/null && [ -f "$INSTALL_DIR/extension/package.json" ]; then
423
+ # ── Canonical build path ─────────────────────────────────────────────────
424
+ # `npm run build` = esbuild (TS → dist/extension.js) + bundle-native.mjs
425
+ # (detects the user's VS Code Electron version, runs @electron/rebuild
426
+ # against node_modules/node-pty, and copies the ABI-correct binary into
427
+ # extension/native/node-pty/). That native/ directory is what the VSIX
428
+ # ships (via .vscodeignore's `!native/**`) and what claws-pty.ts loads
429
+ # at runtime. ONE build step owns both outputs — there is no second
430
+ # rebuild, and there is no fallback to a stale binary.
431
+ #
432
+ # If @electron/rebuild fails (missing Xcode CLT, node-gyp/Python issue,
433
+ # network failure while fetching Electron headers), the build fails
434
+ # loud and the installer aborts. A silently-broken binary would ship a
435
+ # VSIX that lands but can't load node-pty — the exact pipe-mode bug
436
+ # we're trying to kill.
437
+ NATIVE_PTY_BIN="$INSTALL_DIR/extension/native/node-pty/build/Release/pty.node"
438
+
439
+ # Pre-flight: on macOS, @electron/rebuild needs Xcode Command Line Tools
440
+ # to compile node-pty. Check up front so the error is obvious, not
441
+ # buried 200 lines into npm output.
442
+ if [ "$PLATFORM" = "Darwin" ] && ! xcode-select -p &>/dev/null; then
443
+ bad "Xcode Command Line Tools required to build node-pty."
444
+ bad "Fix: run 'xcode-select --install', wait for it to finish, then re-run this installer."
445
+ exit 1
446
+ fi
447
+
448
+ # Pre-detect Electron version from installed editors before building.
449
+ # bundle-native.mjs detects this too, but surfacing it here lets users
450
+ # see the ABI target before a potentially long compile.
451
+ _ELECTRON_PRE=""
452
+ case "$PLATFORM" in
453
+ Darwin)
454
+ for _plist in \
455
+ "/Applications/Visual Studio Code.app/Contents/Resources/app/package.json" \
456
+ "/Applications/Cursor.app/Contents/Resources/app/package.json" \
457
+ "/Applications/Windsurf.app/Contents/Resources/app/package.json"; do
458
+ if [ -f "$_plist" ]; then
459
+ _v=$(node -e "try{console.log(require('$_plist').electronVersion||'')}catch{}" 2>/dev/null || true)
460
+ if [ -n "$_v" ]; then
461
+ _label=$(basename "$(dirname "$(dirname "$(dirname "$_plist")")")" | sed 's/\.app//')
462
+ info "Detected Electron $_v from $_label"
463
+ _ELECTRON_PRE="$_v"
464
+ break
465
+ fi
466
+ fi
467
+ done
468
+ ;;
469
+ Linux)
470
+ for _ep in /usr/share/code/electron /snap/code/current/usr/share/code/electron /opt/visual-studio-code/electron; do
471
+ if [ -x "$_ep" ]; then
472
+ _v=$("$_ep" --version 2>/dev/null | sed 's/^v//' || true)
473
+ if [ -n "$_v" ]; then
474
+ info "Detected Electron $_v from $_ep"
475
+ _ELECTRON_PRE="$_v"
476
+ break
477
+ fi
478
+ fi
479
+ done
480
+ ;;
481
+ esac
482
+ [ -z "$_ELECTRON_PRE" ] && info "Electron version not pre-detected — bundle-native.mjs will detect at build time"
483
+
484
+ needs_rebuild_native=0
485
+ [ ! -f "$NATIVE_PTY_BIN" ] && needs_rebuild_native=1
486
+ [ "${CLAWS_FORCE_REBUILD_NPTY:-0}" = "1" ] && needs_rebuild_native=1
487
+ if needs_build; then needs_rebuild_native=1; fi
488
+
489
+ # Electron-ABI drift detection (v0.7.3). If the user updated VS Code/Cursor/
490
+ # Windsurf to a newer Electron version since the last build, the bundled
491
+ # pty.node is now ABI-mismatched and the extension will silently fall into
492
+ # pipe-mode. Read native/.metadata.json's electronVersion (written by
493
+ # bundle-native.mjs at build time) and compare with the currently-installed
494
+ # editor's Electron version. If they differ, force a rebuild.
495
+ if [ -f "$NATIVE_PTY_BIN" ] && [ -f "$INSTALL_DIR/extension/native/.metadata.json" ] && [ "$needs_rebuild_native" = "0" ]; then
496
+ _claws_last_elec=$(node -e "try{console.log(require('$INSTALL_DIR/extension/native/.metadata.json').electronVersion||'')}catch(e){}" 2>/dev/null || echo "")
497
+ _claws_curr_elec=""
498
+ if [ "$PLATFORM" = "Darwin" ]; then
499
+ # M-22: build candidate list with TERM_PROGRAM-matching editor first so the
500
+ # user's daily-driver wins the ABI check instead of the hardcoded VS Code path.
501
+ _tp="${TERM_PROGRAM:-}"
502
+ # F2: use bash array — avoids eval footgun while keeping TERM_PROGRAM ordering.
503
+ case "$_tp" in
504
+ cursor) _claws_darwin_apps=('/Applications/Cursor.app' '/Applications/Visual Studio Code.app' '/Applications/Visual Studio Code - Insiders.app' '/Applications/Windsurf.app') ;;
505
+ windsurf) _claws_darwin_apps=('/Applications/Windsurf.app' '/Applications/Visual Studio Code.app' '/Applications/Visual Studio Code - Insiders.app' '/Applications/Cursor.app') ;;
506
+ *) _claws_darwin_apps=('/Applications/Visual Studio Code.app' '/Applications/Visual Studio Code - Insiders.app' '/Applications/Cursor.app' '/Applications/Windsurf.app') ;;
507
+ esac
508
+ for _claws_app in "${_claws_darwin_apps[@]}"; do
509
+ _claws_plist="$_claws_app/Contents/Frameworks/Electron Framework.framework/Resources/Info.plist"
510
+ if [ -f "$_claws_plist" ]; then
511
+ _claws_curr_elec=$(plutil -extract CFBundleVersion raw "$_claws_plist" 2>/dev/null || true)
512
+ [ -n "$_claws_curr_elec" ] && break
513
+ fi
514
+ done
515
+ unset _tp _claws_darwin_apps
516
+ elif [ "$PLATFORM" = "Linux" ]; then
517
+ # M-22: prefer TERM_PROGRAM editor on Linux too.
518
+ # M-25: add Cursor + Windsurf Linux paths.
519
+ _tp="${TERM_PROGRAM:-}"
520
+ case "$_tp" in
521
+ cursor)
522
+ _claws_linux_eps="/usr/share/cursor/electron /opt/cursor/electron /snap/cursor/current/usr/share/cursor/electron /usr/share/code/electron /usr/lib/code/electron /opt/visual-studio-code/electron /snap/code/current/usr/share/code/electron /usr/share/windsurf/electron /opt/windsurf/electron"
523
+ ;;
524
+ windsurf)
525
+ _claws_linux_eps="/usr/share/windsurf/electron /opt/windsurf/electron /usr/share/code/electron /usr/lib/code/electron /opt/visual-studio-code/electron /snap/code/current/usr/share/code/electron /usr/share/cursor/electron /opt/cursor/electron"
526
+ ;;
527
+ *)
528
+ _claws_linux_eps="/usr/share/code/electron /usr/lib/code/electron /opt/visual-studio-code/electron /snap/code/current/usr/share/code/electron /usr/share/cursor/electron /opt/cursor/electron /snap/cursor/current/usr/share/cursor/electron /usr/share/windsurf/electron /opt/windsurf/electron"
529
+ ;;
530
+ esac
531
+ for _claws_ep in $_claws_linux_eps; do
532
+ if [ -x "$_claws_ep" ]; then
533
+ _claws_curr_elec=$("$_claws_ep" --version 2>/dev/null | sed 's/^v//' | head -1)
534
+ [ -n "$_claws_curr_elec" ] && break
535
+ fi
536
+ done
537
+ unset _tp _claws_linux_eps
538
+ fi
539
+ # M-23: warn when detection returns empty — don't silently skip drift check.
540
+ if [ -z "$_claws_curr_elec" ] && [ -n "$_claws_last_elec" ]; then
541
+ warn "Could not detect the current VS Code/Cursor/Windsurf Electron version."
542
+ warn "Set CLAWS_ELECTRON_VERSION=<version> to specify it explicitly."
543
+ warn "(run: plutil -extract CFBundleVersion raw '/Applications/Cursor.app/Contents/Frameworks/Electron Framework.framework/Resources/Info.plist')"
544
+ fi
545
+ if [ -n "$_claws_curr_elec" ] && [ -n "$_claws_last_elec" ] && [ "$_claws_curr_elec" != "$_claws_last_elec" ]; then
546
+ info "Electron version changed since last build ($_claws_last_elec → $_claws_curr_elec) — forcing pty.node rebuild"
547
+ info "without this rebuild, the extension would silently fall into pipe-mode"
548
+ needs_rebuild_native=1
549
+ fi
550
+ unset _claws_last_elec _claws_curr_elec _claws_app _claws_plist _claws_ep
551
+ fi
552
+
553
+ if [ "$needs_rebuild_native" = "1" ]; then
554
+ if [ ! -f "$NATIVE_PTY_BIN" ]; then
555
+ info "building extension + native node-pty (binary missing)"
556
+ elif [ "$CURRENT_SHA" != "$LAST_BUILD_SHA" ] && [ -n "$LAST_BUILD_SHA" ]; then
557
+ info "rebuilding extension (git HEAD ${LAST_BUILD_SHA:0:7} → ${CURRENT_SHA:0:7})"
558
+ else
559
+ info "building extension bundle + rebuilding node-pty for current Electron"
560
+ fi
561
+
562
+ # Network pre-check: @electron/rebuild fetches Electron headers from GitHub.
563
+ # Fail fast on air-gapped machines before a 3-minute build that will hang.
564
+ info "checking network connectivity for Electron headers fetch..."
565
+ if curl --silent --head --max-time 5 "https://github.com" >/dev/null 2>&1 \
566
+ || wget --spider --quiet --timeout=5 "https://github.com" >/dev/null 2>&1; then
567
+ info "network reachable — Electron headers fetch should succeed"
568
+ else
569
+ warn "network unreachable (GitHub) — @electron/rebuild may fail fetching Electron headers"
570
+ warn "If you are on an air-gapped machine, set CLAWS_ELECTRON_VERSION=<version> and ensure"
571
+ warn "headers are available at a local mirror, or use CLAWS_FORCE_REBUILD_NPTY=0 to skip."
572
+ fi
573
+
574
+ # Run with visible output — the user needs to see @electron/rebuild
575
+ # progress and any compile errors. --silent here hides the exact
576
+ # diagnostic that tells them what to fix.
577
+ if ( cd "$INSTALL_DIR/extension" \
578
+ && npm install --no-audit --no-fund --loglevel=error \
579
+ && { node -e "require.resolve('@electron/rebuild')" >/dev/null 2>&1 \
580
+ || warn "@electron/rebuild not found after npm install — pty.node build will likely fail"; true; } \
581
+ && npm run build ); then
582
+ echo "$CURRENT_SHA" > "$BUILD_SHA_FILE" 2>/dev/null || true
583
+ # F-11: write .electron-abi so fix.sh skips unnecessary rebuilds on next run.
584
+ mkdir -p "$INSTALL_DIR/extension/dist" 2>/dev/null || true
585
+ node --no-deprecation -e "try{const m=require('$INSTALL_DIR/extension/native/.metadata.json');if(m.electronVersion){require('fs').writeFileSync('$INSTALL_DIR/extension/dist/.electron-abi',m.electronVersion)}}catch(e){}" 2>/dev/null || true
586
+ BUILD_OK=1
587
+ else
588
+ bad "extension build failed — see $CLAWS_LOG for the full compile log."
589
+ # Scan the log to give a targeted hint
590
+ if grep -qi "xcode\|xcrun\|CLT\|command line tools" "$CLAWS_LOG" 2>/dev/null; then
591
+ bad "Likely cause: Xcode Command Line Tools missing or incomplete — run: xcode-select --install"
592
+ elif grep -qi "electron.*header\|ENOTFOUND\|ETIMEDOUT\|network\|fetch" "$CLAWS_LOG" 2>/dev/null; then
593
+ bad "Likely cause: network error fetching Electron headers — check internet connectivity and proxy settings"
594
+ elif grep -qi "python\|gyp" "$CLAWS_LOG" 2>/dev/null; then
595
+ bad "Likely cause: Python 3 or node-gyp issue — run: brew install python3 OR sudo apt install python3"
596
+ else
597
+ bad "Common causes: Xcode CLT missing, Python 3 missing, or network error during @electron/rebuild's Electron headers fetch"
598
+ fi
599
+ bad "After fixing, re-run: bash <(curl -fsSL https://raw.githubusercontent.com/neunaha/claws/main/scripts/install.sh)"
600
+ exit 1
601
+ fi
602
+ else
603
+ BUILD_OK=1
604
+ ok "extension bundle up to date (SHA ${CURRENT_SHA:0:7}, $(wc -c < "$BUNDLE" | tr -d ' ') bytes)"
605
+ fi
606
+
607
+ # Hard verification — the VSIX in step 2c packages from extension/, and
608
+ # without this binary present, wrapped terminals fall back to pipe-mode.
609
+ # Refusing to continue is the correct behavior; a silent pipe-mode
610
+ # install is worse than an explicit failure the user can fix.
611
+ if [ ! -f "$NATIVE_PTY_BIN" ]; then
612
+ bad "native/node-pty/build/Release/pty.node missing after build."
613
+ bad "This means @electron/rebuild failed silently or bundle-native.mjs didn't copy the output."
614
+ bad "Diagnostic: ( cd $INSTALL_DIR/extension && npm run bundle-native )"
615
+ bad "See $CLAWS_LOG for the bundle-native output."
616
+ exit 1
617
+ fi
618
+ NATIVE_PTY_SIZE=$(wc -c < "$NATIVE_PTY_BIN" | tr -d ' ')
619
+ NATIVE_PTY_ELECTRON=$(node -e "try{console.log(require('$INSTALL_DIR/extension/native/.metadata.json').electronVersion||'?')}catch(e){console.log('?')}" 2>/dev/null || echo '?')
620
+ ok "native node-pty ready (${NATIVE_PTY_SIZE} bytes, Electron $NATIVE_PTY_ELECTRON) — VSIX will ship this binary"
621
+ if command -v file &>/dev/null; then
622
+ # M-34: when bash runs under Rosetta 2 (x64 shell on Apple Silicon), uname -m returns
623
+ # x86_64 but bundle-native.mjs (M-05) builds for arm64. Detect Rosetta via sysctl so
624
+ # the expected arch is arm64, not x86_64 — prevents a spurious arch mismatch warning.
625
+ # Linux x86_64 false-positive: `uname -m` returns `x86_64` (underscore),
626
+ # but `file(1)` describes ELF binaries as `x86-64` (hyphen). Match both
627
+ # spellings so legitimate x86_64 bundles don't trigger the warning.
628
+ # Audit 1 finding H-1.
629
+ _claws_expected_arch="$(uname -m)"
630
+ if [ "$_claws_expected_arch" = "x86_64" ] && [ "$(uname -s)" = "Darwin" ]; then
631
+ _claws_rosetta=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo "0")
632
+ [ "$_claws_rosetta" = "1" ] && _claws_expected_arch="arm64"
633
+ fi
634
+ _claws_arch_alt="$(echo "$_claws_expected_arch" | sed 's/_/-/g')"
635
+ file "$NATIVE_PTY_BIN" 2>/dev/null | grep -qiE "$_claws_expected_arch|$_claws_arch_alt" \
636
+ || warn "pty.node architecture may not match current machine ($(uname -m) → expected $_claws_expected_arch) — check bundle-native.mjs output in $CLAWS_LOG"
637
+ unset _claws_arch_alt _claws_rosetta _claws_expected_arch
638
+ fi
639
+
640
+ # R3.7: Check if other installed editors use a different Electron version.
641
+ # The VSIX ships ONE binary built for one Electron ABI. If Cursor/Windsurf
642
+ # ship a different Electron than VS Code, the binary may load in pipe-mode
643
+ # for those editors. Warn so the user knows and can rebuild with CLAWS_ELECTRON_VERSION.
644
+ if [ -n "$NATIVE_PTY_ELECTRON" ] && [ "$NATIVE_PTY_ELECTRON" != "?" ]; then
645
+ _check_editor_electron() {
646
+ local app_label="$1"
647
+ local pkg_json="$2"
648
+ if [ -f "$pkg_json" ]; then
649
+ local editor_ver
650
+ editor_ver=$(node -e "try{console.log(require('$pkg_json').electronVersion||'')}catch{}" 2>/dev/null || true)
651
+ if [ -n "$editor_ver" ] && [ "$editor_ver" != "$NATIVE_PTY_ELECTRON" ]; then
652
+ warn "$app_label uses Electron $editor_ver but pty.node was built for Electron $NATIVE_PTY_ELECTRON"
653
+ warn " node-pty will load in pipe-mode in $app_label — rebuild with: CLAWS_ELECTRON_VERSION=$editor_ver bash <(curl -fsSL https://raw.githubusercontent.com/neunaha/claws/main/scripts/install.sh)"
654
+ fi
655
+ fi
656
+ }
657
+ case "$PLATFORM" in
658
+ Darwin)
659
+ _check_editor_electron "Cursor" "/Applications/Cursor.app/Contents/Resources/app/package.json"
660
+ _check_editor_electron "Windsurf" "/Applications/Windsurf.app/Contents/Resources/app/package.json"
661
+ _check_editor_electron "VS Code Insiders" "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/package.json"
662
+ ;;
663
+ esac
664
+ unset -f _check_editor_electron
665
+ fi
666
+ else
667
+ bad "npm or extension/package.json missing — cannot build extension."
668
+ bad "Install Node.js 18+ and re-run: bash <(curl -fsSL https://raw.githubusercontent.com/neunaha/claws/main/scripts/install.sh)"
669
+ exit 1
670
+ fi
671
+
672
+ # 2b. Read extension version from manifest so the symlink matches.
673
+ # EXPECTED_MIN_VERSION is hardcoded at script-release time. EXT_VERSION is
674
+ # read from the clone at runtime. If the clone is behind EXPECTED_MIN_VERSION,
675
+ # the working tree is stale and the installer aborts — that was the v0.5.1 bug
676
+ # where users saw "v0.4.0 — installed" because their ~/.claws-src/ was stale.
677
+ EXPECTED_MIN_VERSION=$(node -e "try{console.log(require('$INSTALL_DIR/extension/package.json').version)}catch(e){console.log('0.8.0')}" 2>/dev/null || echo "0.8.0")
678
+ EXT_VERSION=$(node -e "try{console.log(require('$INSTALL_DIR/extension/package.json').version)}catch(e){console.log('0.0.0')}" 2>/dev/null || echo "0.0.0")
679
+
680
+ # Flag stale clones loudly so users don't silently run on an old version.
681
+ if [ "$EXT_VERSION" != "$EXPECTED_MIN_VERSION" ]; then
682
+ if ! node -e "
683
+ const [a,b]=[process.argv[1],process.argv[2]].map(s=>s.split('.').map(Number));
684
+ for (let i=0;i<3;i++){ if((a[i]||0)<(b[i]||0)) process.exit(1); if((a[i]||0)>(b[i]||0)) process.exit(0); }
685
+ process.exit(0);
686
+ " "$EXT_VERSION" "$EXPECTED_MIN_VERSION" 2>/dev/null; then
687
+ bad "extension version $EXT_VERSION < expected $EXPECTED_MIN_VERSION — clone is stale."
688
+ bad "Fix: rm -rf $INSTALL_DIR && re-run this installer."
689
+ exit 1
690
+ fi
691
+ fi
692
+
693
+ # 2c. Install the extension into every detected editor.
694
+ #
695
+ # Strategy (v0.5.3+):
696
+ # 1. VSIX install via `code --install-extension` — the proper way.
697
+ # VS Code registers the extension in its extensions.json, extracts
698
+ # to the extensions dir, and (if a window is open) shows a
699
+ # "Reload to activate?" toast automatically. For a closed VS Code,
700
+ # next window-open loads it with zero clicks.
701
+ # 2. Symlink fallback — used when vsce packaging or the editor CLI
702
+ # isn't available (network error, no `code` binary). Also what
703
+ # CLAWS_DEV_SYMLINK=1 forces.
704
+ #
705
+ # Why VSIX works now when it didn't before: Phase 2 moved node-pty out
706
+ # of node_modules/ into native/node-pty/. .vscodeignore excludes
707
+ # node_modules/** but un-ignores !native/**, so the packaged VSIX
708
+ # contains the ABI-correct binary at the exact path the runtime loader
709
+ # expects (<ext>/native/node-pty/).
710
+
711
+ INSTALLED_EDITORS=()
712
+ VSIX_INSTALL_METHOD="" # "vsix" or "symlink" — reported in the banner
713
+
714
+ # Zed uses a proprietary extension format (.zedbundle) — not VSIX-compatible.
715
+ # Claws extension is VS Code-only. If Zed is detected, inform the user.
716
+ if command -v zed &>/dev/null; then
717
+ info "Zed editor detected — Claws extension is VS Code/Cursor/Windsurf-only (VSIX format). Zed is not supported."
718
+ fi
719
+
720
+ # Editor CLI discovery. Checks $PATH first, then macOS app bundle paths.
721
+ _find_editor_cli() {
722
+ local label="$1"
723
+ # In $PATH?
724
+ if command -v "$label" &>/dev/null; then
725
+ command -v "$label"
726
+ return 0
727
+ fi
728
+ # Bundled in an app (macOS). Windows/Linux paths fall through.
729
+ case "$PLATFORM" in
730
+ Darwin)
731
+ case "$label" in
732
+ code) [ -x "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" ] && echo "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" && return 0 ;;
733
+ code-insiders) [ -x "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code-insiders" ] && echo "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code-insiders" && return 0 ;;
734
+ cursor) [ -x "/Applications/Cursor.app/Contents/Resources/app/bin/cursor" ] && echo "/Applications/Cursor.app/Contents/Resources/app/bin/cursor" && return 0 ;;
735
+ windsurf) [ -x "/Applications/Windsurf.app/Contents/Resources/app/bin/windsurf" ] && echo "/Applications/Windsurf.app/Contents/Resources/app/bin/windsurf" && return 0 ;;
736
+ esac
737
+ ;;
738
+ esac
739
+ return 1
740
+ }
741
+
742
+ # Fallback: symlink the source clone into the editor's extensions dir.
743
+ _link_extension_into() {
744
+ local label="$1"
745
+ local ext_dir="$2"
746
+ [ ! -d "$ext_dir" ] && return 1
747
+ local link="$ext_dir/neunaha.claws-$EXT_VERSION"
748
+ rm -rf "$ext_dir"/neunaha.claws-* 2>/dev/null \
749
+ || sudo rm -rf "$ext_dir"/neunaha.claws-* 2>/dev/null \
750
+ || true
751
+ if ln -sf "$INSTALL_DIR/extension" "$link" 2>/dev/null \
752
+ || sudo ln -sf "$INSTALL_DIR/extension" "$link" 2>/dev/null; then
753
+ ok "linked Claws into $label ($link)"
754
+ INSTALLED_EDITORS+=("$label")
755
+ return 0
756
+ fi
757
+ bad "could not link Claws into $label"
758
+ return 1
759
+ }
760
+
761
+ # VSIX install path: package once, install into every editor CLI we find.
762
+ _install_via_vsix() {
763
+ local VSIX_PATH="/tmp/claws-$EXT_VERSION.vsix"
764
+ info "packaging VSIX for VS Code install"
765
+
766
+ # Sanity-check publisher field — vsce fails silently if it's missing.
767
+ local pub
768
+ pub=$(node -e "try{console.log(require('$INSTALL_DIR/extension/package.json').publisher||'')}catch(e){console.log('')}" 2>/dev/null || echo "")
769
+ if [ -z "$pub" ]; then
770
+ warn "extension/package.json missing 'publisher' field — vsce will fail"
771
+ return 1
772
+ fi
773
+
774
+ # Package. vsce reads package.json + .vscodeignore to produce the VSIX.
775
+ if ! ( cd "$INSTALL_DIR/extension" \
776
+ && rm -f "$VSIX_PATH" 2>/dev/null \
777
+ && npx --yes @vscode/vsce package --skip-license --no-git-tag-version --no-update-package-json --out "$VSIX_PATH" ) >>"$CLAWS_LOG" 2>&1; then
778
+ warn "vsce package failed — see $CLAWS_LOG for details"
779
+ info "diagnostic: cd $INSTALL_DIR/extension && npx @vscode/vsce package --out $VSIX_PATH"
780
+ return 1
781
+ fi
782
+
783
+ if [ ! -f "$VSIX_PATH" ]; then
784
+ warn "vsce reported success but VSIX missing — falling back to symlink"
785
+ return 1
786
+ fi
787
+
788
+ local vsix_size; vsix_size=$(wc -c < "$VSIX_PATH" | tr -d ' ')
789
+ if [ "$vsix_size" -lt 50000 ] 2>/dev/null; then
790
+ warn "VSIX suspiciously small (${vsix_size} bytes < 50KB) — native binary may be missing from package"
791
+ warn "Check .vscodeignore includes !native/** and re-run installer"
792
+ return 1
793
+ fi
794
+ ok "packaged $VSIX_PATH ($(numfmt --to=iec-i --suffix=B "$vsix_size" 2>/dev/null || echo "${vsix_size} bytes"))"
795
+
796
+ # Install into every detected editor CLI.
797
+ local any_installed=0
798
+ for label in code code-insiders cursor windsurf; do
799
+ local cli
800
+ cli="$(_find_editor_cli "$label")" || continue
801
+ info "installing into $label via $cli"
802
+
803
+ # Try normal install, then sudo on permission failure (R4.7/B7).
804
+ local install_ok=0
805
+ if "$cli" --install-extension "$VSIX_PATH" --force >/dev/null 2>&1; then
806
+ install_ok=1
807
+ elif sudo "$cli" --install-extension "$VSIX_PATH" --force >/dev/null 2>&1; then
808
+ info " installed via sudo (extensions dir required elevated permissions)"
809
+ install_ok=1
810
+ fi
811
+
812
+ if [ "$install_ok" = "1" ]; then
813
+ # R4.10: Verify the extension actually landed in the extensions directory
814
+ # rather than trusting the exit code alone (VS Code exit codes are undocumented).
815
+ local ext_dir="$HOME/.vscode/extensions"
816
+ case "$label" in
817
+ code-insiders) ext_dir="$HOME/.vscode-insiders/extensions" ;;
818
+ cursor) ext_dir="$HOME/.cursor/extensions" ;;
819
+ windsurf) ext_dir="$HOME/.windsurf/extensions" ;;
820
+ esac
821
+ if ls "$ext_dir"/neunaha.claws-* 2>/dev/null | grep -q .; then
822
+ ok "Claws extension installed in $label (verified in $ext_dir)"
823
+ # Clean stale older-version directories. VS Code itself usually does
824
+ # this on VSIX install, but if a prior install hit a lock or used a
825
+ # different CLI, old <publisher>.<name>-X.Y.Z dirs can linger and
826
+ # confuse VS Code's extension picker. Keep only the just-installed
827
+ # version (matches EXT_VERSION).
828
+ # M-06: gate cleanup on kept_dir existing first. VS Code extracts VSIX
829
+ # asynchronously — if kept_dir hasn't appeared yet, the safety guard
830
+ # ([ "$stale" = "$kept_dir" ]) never matches and the loop would delete
831
+ # every installed version. Skip and warn instead of destroying all installs.
832
+ local kept_dir="$ext_dir/neunaha.claws-$EXT_VERSION"
833
+ # FINDING-B-4: VS Code extracts VSIX asynchronously — poll up to 1s
834
+ # (5×200ms) for kept_dir to appear before deciding it's absent.
835
+ for _poll in 1 2 3 4 5; do
836
+ [ -d "$kept_dir" ] && break
837
+ sleep 0.2
838
+ done
839
+ unset _poll
840
+ if [ -d "$kept_dir" ]; then
841
+ for stale in "$ext_dir"/neunaha.claws-*; do
842
+ [ -d "$stale" ] || continue
843
+ [ "$stale" = "$kept_dir" ] && continue
844
+ if rm -rf "$stale" 2>/dev/null || sudo rm -rf "$stale" 2>/dev/null; then
845
+ info " removed stale install $(basename "$stale")"
846
+ fi
847
+ done
848
+ else
849
+ warn " kept_dir not yet present ($kept_dir) — skipping stale cleanup to avoid removing all versions"
850
+ warn " (VS Code may still be extracting the VSIX — stale dirs will be cleaned on next install)"
851
+ fi
852
+ else
853
+ ok "Claws extension installed in $label (via VSIX — extensions dir not found for verification)"
854
+ fi
855
+ INSTALLED_EDITORS+=("$label")
856
+ any_installed=1
857
+ else
858
+ # Non-zero exit and sudo also failed — likely a running window holds an
859
+ # exclusive lock on the .node binary. The VSIX is staged in /tmp; VS Code
860
+ # will pick it up on next Reload Window.
861
+ warn "$label --install-extension refused (likely a running window holds the current version)"
862
+ info " this is fine — the new VSIX is staged; Reload Window activates it"
863
+ # R4.10: Still verify — the extensions dir may already have the new version
864
+ local ext_dir2="$HOME/.vscode/extensions"
865
+ case "$label" in
866
+ code-insiders) ext_dir2="$HOME/.vscode-insiders/extensions" ;;
867
+ cursor) ext_dir2="$HOME/.cursor/extensions" ;;
868
+ windsurf) ext_dir2="$HOME/.windsurf/extensions" ;;
869
+ esac
870
+ if ls "$ext_dir2"/neunaha.claws-* 2>/dev/null | grep -q .; then
871
+ INSTALLED_EDITORS+=("$label (pending reload)")
872
+ any_installed=1
873
+ else
874
+ warn "$label extensions dir has no neunaha.claws-* entry — install may have failed"
875
+ fi
876
+ fi
877
+ done
878
+
879
+ [ "$any_installed" = "1" ] && return 0
880
+ return 1
881
+ }
882
+
883
+ # Choose install path.
884
+ if [ "${CLAWS_DEV_SYMLINK:-0}" = "1" ]; then
885
+ info "CLAWS_DEV_SYMLINK=1 — using symlink install (live-edit dev workflow)"
886
+ VSIX_INSTALL_METHOD="symlink"
887
+ elif [ "${BUILD_OK:-0}" = "1" ] && command -v npx &>/dev/null; then
888
+ if _install_via_vsix; then
889
+ VSIX_INSTALL_METHOD="vsix"
890
+ else
891
+ warn "VSIX install failed — falling back to symlink"
892
+ VSIX_INSTALL_METHOD="symlink"
893
+ fi
894
+ else
895
+ info "no npx or build failed — using symlink install"
896
+ VSIX_INSTALL_METHOD="symlink"
897
+ fi
898
+
899
+ # Symlink fallback — only runs if VSIX path didn't cover us.
900
+ if [ "$VSIX_INSTALL_METHOD" = "symlink" ] || [ "${#INSTALLED_EDITORS[@]}" -eq 0 ]; then
901
+ for pair in \
902
+ "vscode:$HOME/.vscode/extensions" \
903
+ "vscode-insiders:$HOME/.vscode-insiders/extensions" \
904
+ "cursor:$HOME/.cursor/extensions" \
905
+ "windsurf:$HOME/.windsurf/extensions"; do
906
+ label="${pair%%:*}"
907
+ dir="${pair#*:}"
908
+ [ -d "$dir" ] && _link_extension_into "$label" "$dir" || true
909
+ done
910
+
911
+ # If no editor dir existed at all, create the default one and link there.
912
+ if [ "${#INSTALLED_EDITORS[@]}" -eq 0 ] && [ "$CLAWS_EDITOR" != "skip" ]; then
913
+ mkdir -p "$HOME/.vscode/extensions" 2>/dev/null
914
+ _link_extension_into "vscode (new)" "$HOME/.vscode/extensions" || true
915
+ fi
916
+ fi
917
+
918
+ if [ "${#INSTALLED_EDITORS[@]}" -eq 0 ]; then
919
+ warn "extension not installed in any editor — check CLAWS_EDITOR env var"
920
+ fi
921
+
922
+ # ─── Step 3: Script permissions ────────────────────────────────────────────
923
+ step "Setting file permissions"
924
+ chmod +x "$INSTALL_DIR"/scripts/*.sh 2>/dev/null || true
925
+ chmod +x "$INSTALL_DIR/mcp_server.js" 2>/dev/null || true
926
+ ok "scripts executable"
927
+
928
+ # ─── Step 4: Runtime check ─────────────────────────────────────────────────
929
+ step "Runtime check"
930
+ # Verify Node.js is still reachable (a PATH change mid-install would break MCP)
931
+ if command -v node &>/dev/null; then
932
+ ok "Node.js reachable at $(node -e 'process.stdout.write(process.execPath)' 2>/dev/null) ($(node --version))"
933
+ else
934
+ bad "node not reachable in current PATH — MCP server and extension build will fail"
935
+ die "Install Node.js 18+ and re-run: bash <(curl -fsSL https://raw.githubusercontent.com/neunaha/claws/main/scripts/install.sh)"
936
+ fi
937
+ # Verify mcp_server.js is present in the clone before we try to copy it in step 5
938
+ if [ ! -f "$INSTALL_DIR/mcp_server.js" ]; then
939
+ bad "$INSTALL_DIR/mcp_server.js missing — clone may be incomplete"
940
+ die "Fix: rm -rf $INSTALL_DIR && re-run this installer"
941
+ fi
942
+ ok "runtime ready"
943
+
944
+ # ─── Step 5: MCP server (project-local primary, global opt-in) ─────────────
945
+ step "Configuring MCP server"
946
+
947
+ MCP_PATH="$INSTALL_DIR/mcp_server.js"
948
+ if [ "${CLAWS_SKIP_MCP:-0}" = "1" ]; then
949
+ warn "CLAWS_SKIP_MCP=1 — skipping MCP registration"
950
+ else
951
+ if [ "$PROJECT_INSTALL" = "1" ]; then
952
+ # FINDING-B-2: guard against dangling / loop / unexpected symlinks at .claws-bin
953
+ # before mkdir -p, which would silently create files at the symlink target.
954
+ if [ -L "$PROJECT_ROOT/.claws-bin" ]; then
955
+ [ -e "$PROJECT_ROOT/.claws-bin" ] \
956
+ && warn ".claws-bin is a symlink → $(readlink "$PROJECT_ROOT/.claws-bin") — removing and replacing with directory" \
957
+ || warn ".claws-bin is a dangling symlink — removing"
958
+ rm -f "$PROJECT_ROOT/.claws-bin"
959
+ fi
960
+ mkdir -p "$PROJECT_ROOT/.claws-bin"
961
+ # Fix #1 (v0.7.12): write package.json with {"type":"commonjs"} to override the parent
962
+ # project's ESM default. Without this, mcp_server.js and hooks/*.js crash with
963
+ # "require is not defined in ES module scope" in projects that have "type":"module"
964
+ # in their root package.json (Next.js, Vite, ESM-default tooling — ~50% of modern projects).
965
+ printf '{\n "type": "commonjs",\n "_comment": "Forces CommonJS for .js files in .claws-bin/. Required when the parent project has type:module in its package.json (Next.js, Vite, etc.)"\n}\n' > "$PROJECT_ROOT/.claws-bin/package.json"
966
+ if [ "$(realpath "$INSTALL_DIR" 2>/dev/null)" = "$(realpath "$PROJECT_ROOT" 2>/dev/null)" ]; then
967
+ # Dev mode: source repo IS the install target — use symlinks to eliminate drift.
968
+ ln -sf ../mcp_server.js "$INSTALL_DIR/.claws-bin/mcp_server.js"
969
+ ln -sf ../scripts/shell-hook.sh "$INSTALL_DIR/.claws-bin/shell-hook.sh"
970
+ ln -sf ../scripts/stream-events.js "$INSTALL_DIR/.claws-bin/stream-events.js"
971
+ ln -sf ../scripts/monitor-arm-watch.js "$INSTALL_DIR/.claws-bin/monitor-arm-watch.js"
972
+ rm -rf "$INSTALL_DIR/.claws-bin/hooks"
973
+ ln -sf ../scripts/hooks "$INSTALL_DIR/.claws-bin/hooks"
974
+ echo '[install] dev mode: symlinked .claws-bin → source'
975
+ else
976
+ cp "$INSTALL_DIR/mcp_server.js" "$PROJECT_ROOT/.claws-bin/mcp_server.js"
977
+ chmod +x "$PROJECT_ROOT/.claws-bin/mcp_server.js"
978
+ # Copy generated schema artifacts: mcp-tools.json (consumed by mcp_server.js
979
+ # at startup) + json/ (20 per-topic JSON Schemas) + types/ (TS .d.ts).
980
+ # External schema consumers (worker SDKs, validators, IDE hints) need the
981
+ # json/ and types/ files even though mcp_server.js itself only needs
982
+ # mcp-tools.json.
983
+ if [ -d "$INSTALL_DIR/schemas" ]; then
984
+ mkdir -p "$PROJECT_ROOT/.claws-bin/schemas/json" "$PROJECT_ROOT/.claws-bin/schemas/types"
985
+ cp "$INSTALL_DIR/schemas/mcp-tools.json" "$PROJECT_ROOT/.claws-bin/schemas/" 2>/dev/null || true
986
+ # P3-1: deploy client-types.d.ts for typed SDK consumers
987
+ cp "$INSTALL_DIR/schemas/client-types.d.ts" "$PROJECT_ROOT/.claws-bin/schemas/" 2>/dev/null || true
988
+ cp "$INSTALL_DIR"/schemas/json/*.json "$PROJECT_ROOT/.claws-bin/schemas/json/" 2>/dev/null || true
989
+ cp "$INSTALL_DIR"/schemas/types/*.d.ts "$PROJECT_ROOT/.claws-bin/schemas/types/" 2>/dev/null || true
990
+ fi
991
+ # Copy Claws SDK (v0.7.0+) for typed publish helpers in worker scripts
992
+ if [ -f "$INSTALL_DIR/claws-sdk.js" ]; then
993
+ cp "$INSTALL_DIR/claws-sdk.js" "$PROJECT_ROOT/.claws-bin/claws-sdk.js"
994
+ chmod +x "$PROJECT_ROOT/.claws-bin/claws-sdk.js" 2>/dev/null || true
995
+ else
996
+ warn "claws-sdk.js not found in $INSTALL_DIR — SDK helpers unavailable (P3-3)"
997
+ fi
998
+ cp "$INSTALL_DIR/scripts/shell-hook.sh" "$PROJECT_ROOT/.claws-bin/shell-hook.sh"
999
+ # Copy event-streaming sidecar (v0.6.2+) so Bash run_in_background + Monitor can stream pub/sub frames
1000
+ if [ -f "$INSTALL_DIR/scripts/stream-events.js" ]; then
1001
+ cp "$INSTALL_DIR/scripts/stream-events.js" "$PROJECT_ROOT/.claws-bin/stream-events.js"
1002
+ chmod +x "$PROJECT_ROOT/.claws-bin/stream-events.js" 2>/dev/null || true
1003
+ fi
1004
+ # Copy per-spawn Monitor-arm watcher (v0.7.14+) — invoked by post-tool-use-claws.js
1005
+ if [ -f "$INSTALL_DIR/scripts/monitor-arm-watch.js" ]; then
1006
+ cp "$INSTALL_DIR/scripts/monitor-arm-watch.js" "$PROJECT_ROOT/.claws-bin/monitor-arm-watch.js"
1007
+ chmod +x "$PROJECT_ROOT/.claws-bin/monitor-arm-watch.js" 2>/dev/null || true
1008
+ fi
1009
+ # Copy lifecycle hook scripts for inject-settings-hooks.js to reference.
1010
+ # Source-of-truth is $INSTALL_DIR/scripts/hooks/ (committed to git).
1011
+ # Previously this read from $INSTALL_DIR/.claws-bin/hooks/, which is
1012
+ # gitignored and therefore missing on every fresh clone — silent skip.
1013
+ #
1014
+ # Wipe-then-copy: removed-in-newer-release files (e.g. post-tool-use-claws.js
1015
+ # deprecated in v0.6.5) used to survive in users' .claws-bin/hooks/ and the
1016
+ # Claude Code hook runner would still try to invoke them. Audit 4 finding I.
1017
+ #
1018
+ # We also ship a package.json shim with {"type":"commonjs"} alongside the
1019
+ # hooks. Without it, projects whose root package.json declares
1020
+ # "type":"module" (modern Node/TS projects) load the hook scripts as ESM,
1021
+ # and the CommonJS require() call at the top of each hook crashes.
1022
+ # Reported by user (Miles) on v0.7.0.
1023
+ if [ -d "$INSTALL_DIR/scripts/hooks" ]; then
1024
+ # M-09: atomic rename pattern — copy to tmp dir first, then swap into place.
1025
+ # Prevents kill-window leaving an empty hooks dir that breaks every Bash hook.
1026
+ # F1: set +e around heredoc so $? is readable (under set -eo pipefail the script
1027
+ # would abort at the heredoc before reaching any if [ $? ] check).
1028
+ set +e
1029
+ node --no-deprecation --input-type=module <<HOOKSATOMICEOF
1030
+ import { copyDirAtomic } from '${INSTALL_DIR}/scripts/_helpers/atomic-file.mjs';
1031
+ try {
1032
+ await copyDirAtomic('${INSTALL_DIR}/scripts/hooks', '${PROJECT_ROOT}/.claws-bin/hooks');
1033
+ } catch (e) {
1034
+ process.stderr.write('[M-09] atomic hooks copy failed: ' + e.message + '\\n');
1035
+ process.exit(1);
1036
+ }
1037
+ HOOKSATOMICEOF
1038
+ _hooks_exit=$?
1039
+ set -e
1040
+ if [ "$_hooks_exit" -ne 0 ]; then
1041
+ warn "hooks dir copy failed — .claws-bin/hooks may be incomplete"
1042
+ fi
1043
+ # package.json shim: write if not present after copy (older hooks dirs omit it).
1044
+ if [ ! -f "$PROJECT_ROOT/.claws-bin/hooks/package.json" ]; then
1045
+ printf '{"type":"commonjs","private":true}\n' > "$PROJECT_ROOT/.claws-bin/hooks/package.json"
1046
+ fi
1047
+ fi
1048
+ fi
1049
+ ok "vendored $PROJECT_ROOT/.claws-bin/"
1050
+
1051
+ # ── Copy the built VS Code extension into the project for visibility ────
1052
+ # VS Code still loads the extension from the user-level install
1053
+ # (~/.vscode/extensions/neunaha.claws-<version>). This project-local
1054
+ # copy is purely for visibility + portability — teammates who clone the
1055
+ # project can SEE what's installed, and users running `ls .claws-bin/`
1056
+ # can confirm Claws is present.
1057
+ #
1058
+ # Opt out with CLAWS_SKIP_EXTENSION_COPY=1.
1059
+ if [ "${CLAWS_SKIP_EXTENSION_COPY:-0}" != "1" ]; then
1060
+ PROJECT_EXT_DIR="$PROJECT_ROOT/.claws-bin/extension"
1061
+ rm -rf "$PROJECT_EXT_DIR" 2>/dev/null || true
1062
+ mkdir -p "$PROJECT_EXT_DIR"
1063
+
1064
+ # Copy the runtime-required pieces only — skip src/, test/, scripts/,
1065
+ # tsconfig, esbuild, node_modules (we have native/ as the bundle).
1066
+ for entry in dist native package.json package-lock.json README.md CHANGELOG.md icon.png .vscodeignore; do
1067
+ if [ -e "$INSTALL_DIR/extension/$entry" ]; then
1068
+ cp -R "$INSTALL_DIR/extension/$entry" "$PROJECT_EXT_DIR/" 2>/dev/null || true
1069
+ fi
1070
+ done
1071
+
1072
+ EXT_COPY_BYTES=$(du -sk "$PROJECT_EXT_DIR" 2>/dev/null | awk '{print $1*1024}')
1073
+ ok "copied extension → $PROJECT_EXT_DIR ($(numfmt --to=iec-i --suffix=B "$EXT_COPY_BYTES" 2>/dev/null || echo "${EXT_COPY_BYTES} bytes"))"
1074
+ fi
1075
+
1076
+ # ── Write a visible README.md in .claws-bin/ so teammates see what's there ──
1077
+ cat > "$PROJECT_ROOT/.claws-bin/README.md" <<CLAWSBIN
1078
+ # .claws-bin/
1079
+
1080
+ Project-local Claws runtime and artifacts. Auto-generated by the Claws
1081
+ installer — don't edit by hand; it's refreshed on every \`/claws-update\`.
1082
+
1083
+ ## Contents
1084
+
1085
+ | File / dir | Role |
1086
+ |---|---|
1087
+ | \`mcp_server.js\` | Node MCP server. Spawned by Claude Code when it reads \`../.mcp.json\`. Bridges MCP protocol ⇄ Claws socket. |
1088
+ | \`shell-hook.sh\` | Shell initialization hook — copied from \`scripts/shell-hook.sh\` for reference. Actual hook lives in your \`~/.zshrc\`/\`~/.bashrc\` (appended by the installer). |
1089
+ | \`extension/\` | Full built copy of the VS Code extension (dist + native node-pty binary + manifest). **For visibility only** — VS Code itself loads the extension from the user-level install at \`~/.vscode/extensions/neunaha.claws-<version>\`, not from here. |
1090
+ | \`README.md\` | This file. |
1091
+
1092
+ ## How the extension actually loads
1093
+
1094
+ The VS Code extension is installed at **user scope**, not per-project —
1095
+ that's how every VS Code extension works (Python, ESLint, Prettier, etc.).
1096
+ The symlinked location is:
1097
+
1098
+ \`\`\`
1099
+ ~/.vscode/extensions/neunaha.claws-<version> → ~/.claws-src/extension
1100
+ \`\`\`
1101
+
1102
+ The \`extension/\` directory inside \`.claws-bin/\` is a **reference copy** that
1103
+ lets teammates see "Claws is installed" in the project. It's also useful if
1104
+ you want to version-pin the extension files alongside your project's git
1105
+ history.
1106
+
1107
+ ## Recommended .gitignore
1108
+
1109
+ The \`extension/\` copy is ~300–400 KB. Common choices:
1110
+
1111
+ - **Commit it** if you want teammates who clone the repo to see the exact
1112
+ Claws version that's active, without running the installer.
1113
+ - **Ignore it** (\`.claws-bin/extension/\`) if you treat it as an install
1114
+ artifact that regenerates on demand.
1115
+
1116
+ \`.mcp.json\` is machine-specific (contains absolute paths) and is gitignored.
1117
+ Do not commit it. Re-run the installer to regenerate it if node or the
1118
+ project moves.
1119
+
1120
+ ## Installing Claws from this project
1121
+
1122
+ If a teammate clones this project without Claws installed, they just run:
1123
+
1124
+ \`\`\`bash
1125
+ bash <(curl -fsSL https://raw.githubusercontent.com/neunaha/claws/main/scripts/install.sh)
1126
+ \`\`\`
1127
+
1128
+ That installs the extension at user scope, registers the MCP server, and
1129
+ refreshes these project-local files.
1130
+
1131
+ ## Updating
1132
+
1133
+ \`\`\`bash
1134
+ bash <(curl -fsSL https://raw.githubusercontent.com/neunaha/claws/main/scripts/update.sh)
1135
+ \`\`\`
1136
+
1137
+ Or from inside a Claude Code session: \`/claws-update\`.
1138
+
1139
+ ---
1140
+
1141
+ Claws docs: https://github.com/neunaha/claws
1142
+ CLAWSBIN
1143
+ ok "wrote $PROJECT_ROOT/.claws-bin/README.md"
1144
+
1145
+ # Write or merge .mcp.json with absolute args path so Claude Code can start
1146
+ # the server regardless of its cwd. __dirname walk-up in mcp_server.js
1147
+ # handles socket discovery once the server is running — no CLAWS_SOCKET needed.
1148
+ #
1149
+ # M-02: use json-safe.mjs mergeIntoFile — JSONC-tolerant, never resets cfg to {}
1150
+ # on parse error (which would silently wipe the user's other MCP servers).
1151
+ # F5: PROJECT_MCP and PROJECT_ROOT passed as env vars (not string-literal-embedded in JS)
1152
+ # to avoid JS SyntaxError when paths contain single-quotes or backslashes.
1153
+ PROJECT_MCP="$PROJECT_ROOT/.mcp.json"
1154
+ # F1: set +e around heredoc so _mcp_exit is readable before set -e is restored.
1155
+ set +e
1156
+ PROJECT_MCP="$PROJECT_MCP" PROJECT_ROOT="$PROJECT_ROOT" INSTALL_DIR="$INSTALL_DIR" \
1157
+ node --no-deprecation --input-type=module <<MCPMERGEEOF
1158
+ const { mergeIntoFile } = await import(process.env.INSTALL_DIR + '/scripts/_helpers/json-safe.mjs');
1159
+ const mcpPath = process.env.PROJECT_MCP;
1160
+ const projectRoot = process.env.PROJECT_ROOT;
1161
+ const result = await mergeIntoFile(mcpPath, cfg => {
1162
+ if (!cfg.mcpServers) cfg.mcpServers = {};
1163
+ cfg.mcpServers.claws = { command: 'node', args: [projectRoot + '/.claws-bin/mcp_server.js'] };
1164
+ });
1165
+ if (!result.ok) {
1166
+ const e = result.error;
1167
+ process.stderr.write('[M-02] .mcp.json merge failed: ' + e.message + '\\n');
1168
+ if (e.backupSavedAt) {
1169
+ process.stderr.write('[M-02] Malformed original backed up to: ' + e.backupSavedAt + '\\n');
1170
+ process.stderr.write('[M-02] Fix the JSON then re-run /claws-update\\n');
1171
+ }
1172
+ process.exit(1);
1173
+ }
1174
+ MCPMERGEEOF
1175
+ _mcp_exit=$?
1176
+ set -e
1177
+ if [ "$_mcp_exit" -ne 0 ]; then
1178
+ die ".mcp.json merge failed — original preserved. Fix $PROJECT_MCP then re-run /claws-update"
1179
+ fi
1180
+ ok "wrote $PROJECT_MCP"
1181
+ if ! node -e "JSON.parse(require('fs').readFileSync('$PROJECT_ROOT/.mcp.json','utf8'))" 2>/dev/null; then
1182
+ bad ".mcp.json written to $PROJECT_ROOT but is not valid JSON — MCP server will fail to load"
1183
+ bad "Check $CLAWS_LOG for jq/cat errors above"
1184
+ fi
1185
+ touch "$PROJECT_ROOT/.gitignore" 2>/dev/null || true
1186
+ if ! grep -q "^\.claws/" "$PROJECT_ROOT/.gitignore" 2>/dev/null; then
1187
+ echo ".claws/" >> "$PROJECT_ROOT/.gitignore"
1188
+ ok "added .claws/ to $PROJECT_ROOT/.gitignore"
1189
+ fi
1190
+ if ! grep -q "^\.mcp\.json" "$PROJECT_ROOT/.gitignore" 2>/dev/null; then
1191
+ echo ".mcp.json" >> "$PROJECT_ROOT/.gitignore"
1192
+ ok "added .mcp.json to $PROJECT_ROOT/.gitignore"
1193
+ fi
1194
+ if ! grep -q "^\.claws-bin/" "$PROJECT_ROOT/.gitignore" 2>/dev/null; then
1195
+ echo ".claws-bin/" >> "$PROJECT_ROOT/.gitignore"
1196
+ ok "added .claws-bin/ to $PROJECT_ROOT/.gitignore"
1197
+ fi
1198
+
1199
+ # Write/merge .vscode/extensions.json so VS Code prompts anyone who opens
1200
+ # this project without Claws installed. Pins `neunaha.claws` as a
1201
+ # workspace-recommended extension. Doesn't force-install — just shows
1202
+ # the standard "this workspace recommends installing these extensions"
1203
+ # prompt.
1204
+ if [ "${CLAWS_SKIP_VSCODE_RECOMMEND:-0}" != "1" ]; then
1205
+ VSCODE_DIR="$PROJECT_ROOT/.vscode"
1206
+ VSCODE_EXT_JSON="$VSCODE_DIR/extensions.json"
1207
+ mkdir -p "$VSCODE_DIR"
1208
+ node --no-deprecation -e "
1209
+ const fs = require('fs');
1210
+ const p = process.argv[1];
1211
+ let cfg = {};
1212
+ let parseError = false;
1213
+ try {
1214
+ const raw = fs.readFileSync(p, 'utf8');
1215
+ // Strip JSONC line + block comments so existing commented files merge
1216
+ // cleanly instead of being overwritten.
1217
+ const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
1218
+ cfg = JSON.parse(stripped);
1219
+ } catch (e) {
1220
+ if (fs.existsSync(p)) parseError = true;
1221
+ }
1222
+ if (parseError) {
1223
+ console.log('existing extensions.json has non-standard syntax — leaving it untouched');
1224
+ process.exit(0);
1225
+ }
1226
+ if (!Array.isArray(cfg.recommendations)) cfg.recommendations = [];
1227
+ if (!cfg.recommendations.includes('neunaha.claws')) {
1228
+ cfg.recommendations.push('neunaha.claws');
1229
+ fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n');
1230
+ console.log('added neunaha.claws to workspace recommendations');
1231
+ } else {
1232
+ console.log('neunaha.claws already in workspace recommendations');
1233
+ }
1234
+ " "$VSCODE_EXT_JSON" | sed 's/^/ /' || true
1235
+ ok "wrote $VSCODE_EXT_JSON"
1236
+ fi
1237
+ else
1238
+ warn "no safe project dir — skipping project .mcp.json and .vscode/extensions.json"
1239
+ fi
1240
+
1241
+ if [ "${CLAWS_GLOBAL_MCP:-0}" = "1" ]; then
1242
+ mkdir -p "$HOME/.claude"
1243
+ node --no-deprecation -e "
1244
+ const fs = require('fs');
1245
+ const p = '$HOME/.claude/settings.json';
1246
+ const mcpServerPath = process.argv[1];
1247
+ let cfg = {};
1248
+ try { cfg = JSON.parse(fs.readFileSync(p, 'utf8')); } catch {}
1249
+ if (!cfg.mcpServers) cfg.mcpServers = {};
1250
+ cfg.mcpServers.claws = { command: 'node', args: [mcpServerPath] };
1251
+ fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n');
1252
+ " "$INSTALL_DIR/mcp_server.js"
1253
+ ok "global MCP registered in ~/.claude/settings.json"
1254
+ fi
1255
+ fi
1256
+
1257
+ # ─── Step 6: Claude Code capabilities (commands/rules/skills/CLAUDE.md) ────
1258
+ step "Installing Claude Code capabilities"
1259
+
1260
+ install_capabilities_into() {
1261
+ local TARGET="$1"
1262
+ local LABEL="$2"
1263
+ local CMD_DIR="$TARGET/.claude/commands"
1264
+ mkdir -p "$CMD_DIR" "$TARGET/.claude/rules" "$TARGET/.claude/skills"
1265
+
1266
+ local cmd_count=0
1267
+ if [ -d "$INSTALL_DIR/.claude/commands" ]; then
1268
+ # v0.7.14 — Bug 1 fix: sweep prior-version Claws commands before copying.
1269
+ # The Claws prefix is owned by the installer; user-added commands (anything
1270
+ # without the claws- prefix) are untouched.
1271
+ # Known gap: dev-protocol-* commands are not swept (no such commands ship currently).
1272
+ local _swept_cmds=0
1273
+ for stale in "$CMD_DIR"/claws-*.md "$CMD_DIR"/claws.md; do
1274
+ [ -f "$stale" ] || continue
1275
+ rm -f "$stale" && _swept_cmds=$((_swept_cmds+1))
1276
+ done
1277
+ [ "$_swept_cmds" -gt 0 ] && warn "swept $_swept_cmds stale Claws commands from $CMD_DIR"
1278
+ for cmd in "$INSTALL_DIR/.claude/commands"/claws*.md; do
1279
+ [ -f "$cmd" ] || continue
1280
+ cp "$cmd" "$CMD_DIR/" && cmd_count=$((cmd_count+1))
1281
+ done
1282
+ fi
1283
+
1284
+ # Self-referential /claws-install command (points at GitHub so it works in any project)
1285
+ cat > "$CMD_DIR/claws-install.md" <<'CLAWSCMD'
1286
+ ---
1287
+ name: claws-install
1288
+ description: Install or update Claws — Terminal Control Bridge for VS Code. Runs the installer inside the current project so this workspace gets the full project-local setup.
1289
+ ---
1290
+
1291
+ # /claws-install
1292
+
1293
+ Install or update Claws in THIS project from https://github.com/neunaha/claws
1294
+
1295
+ Run this from the project root:
1296
+
1297
+ ```bash
1298
+ bash <(curl -fsSL https://raw.githubusercontent.com/neunaha/claws/main/scripts/install.sh)
1299
+ ```
1300
+
1301
+ After the script completes:
1302
+ 1. Reload VS Code: Cmd+Shift+P → Developer: Reload Window
1303
+ 2. Restart Claude Code in this project so the project-local `.mcp.json` is picked up.
1304
+ 3. Try `/claws-help` or `/claws-status`.
1305
+
1306
+ If MCP tools don't appear after restart, run `/claws-fix` or `/claws-report`.
1307
+ CLAWSCMD
1308
+ cmd_count=$((cmd_count+1))
1309
+
1310
+ rm -f "$TARGET/.claude/rules/claws-default-behavior.md" 2>/dev/null || true
1311
+ [ -f "$INSTALL_DIR/rules/claws-default-behavior.md" ] \
1312
+ && cp "$INSTALL_DIR/rules/claws-default-behavior.md" "$TARGET/.claude/rules/" || true
1313
+
1314
+ # P3-2: glob all claws-* and dev-protocol-* skills so new skills are picked up
1315
+ # without editing this script. Current skills: claws-prompt-templates,
1316
+ # claws-wave-lead, claws-wave-subworker.
1317
+ # SELF-COLLISION GUARD: when TARGET == INSTALL_DIR (e.g. ~/.claws-src is a symlink
1318
+ # to the project root on dev machines), src and dest resolve to the SAME inode.
1319
+ # Without the -ef guard, `rm -rf $dest` would wipe the source before `cp` could
1320
+ # read it, aborting install.sh at this step. -ef is a bash test for same-inode.
1321
+ # v0.7.14 — Bug 2 fix: sweep prior-version Claws skill directories.
1322
+ # Only directories starting with "claws-" — preserves user-added skills and
1323
+ # dev-protocol-* skills (none currently ship with Claws, but any present are
1324
+ # treated as user-added and left untouched).
1325
+ local _swept_skills=0
1326
+ for stale_skill in "$TARGET/.claude/skills"/claws-*; do
1327
+ [ -d "$stale_skill" ] || continue
1328
+ rm -rf "$stale_skill" && _swept_skills=$((_swept_skills+1))
1329
+ done
1330
+ [ "$_swept_skills" -gt 0 ] && warn "swept $_swept_skills stale Claws skills from $TARGET/.claude/skills"
1331
+ for _skill_src in "$INSTALL_DIR/.claude/skills"/claws-* "$INSTALL_DIR/.claude/skills"/dev-protocol-*; do
1332
+ [ -d "$_skill_src" ] || continue
1333
+ _skill_name="$(basename "$_skill_src")"
1334
+ if [ "$_skill_src" -ef "$TARGET/.claude/skills/$_skill_name" ]; then continue; fi
1335
+ rm -rf "$TARGET/.claude/skills/$_skill_name" 2>/dev/null || true
1336
+ cp -r "$_skill_src" "$TARGET/.claude/skills/$_skill_name"
1337
+ done
1338
+ unset _skill_src _skill_name
1339
+
1340
+ # CLAUDE.md injection (project scope only — never inside $HOME)
1341
+ # M-21: GIT_PULL_OK=0 means git pull failed in update.sh — skip re-injection to
1342
+ # avoid overwriting the user's CLAUDE.md tool set with stale source.
1343
+ if [ "$TARGET" != "$HOME" ]; then
1344
+ if [ "${GIT_PULL_OK:-1}" = "0" ]; then
1345
+ note "CLAUDE.md injection skipped — git pull failed, stale source (M-21)"
1346
+ elif [ ! -f "$INSTALL_DIR/scripts/inject-claude-md.js" ] && [ ! -f "$INSTALL_DIR/.claws-bin/inject-claude-md.js" ]; then
1347
+ warn "inject-claude-md.js not found — CLAUDE.md injection skipped. Clone may be incomplete."
1348
+ else
1349
+ node --no-deprecation "$INSTALL_DIR/scripts/inject-claude-md.js" "$TARGET" 2>&1 | sed 's/^/ /' || warn "CLAUDE.md injector failed — see $CLAWS_LOG for details"
1350
+ fi
1351
+ # Global ~/.claude/CLAUDE.md injection (machine-wide Claws policy)
1352
+ # F2/M-21: same GIT_PULL_OK gate as project CLAUDE.md — avoids rewriting
1353
+ # the machine-wide policy from stale source when git pull failed.
1354
+ if [ "${GIT_PULL_OK:-1}" = "0" ]; then
1355
+ note "global CLAUDE.md injection skipped — git pull failed, stale source (F2/M-21)"
1356
+ elif [ -f "$INSTALL_DIR/scripts/inject-global-claude-md.js" ]; then
1357
+ node --no-deprecation "$INSTALL_DIR/scripts/inject-global-claude-md.js" 2>&1 | sed 's/^/ /' || warn "global CLAUDE.md injector failed"
1358
+ fi
1359
+ # Hook registration in ~/.claude/settings.json (SessionStart / PreToolUse / Stop).
1360
+ # Bin path passed to the injector is $INSTALL_DIR/scripts — hooks register pointing
1361
+ # to the source clone's committed scripts/hooks/ directory (NOT gitignored, kept
1362
+ # current by git pull). Using the shared INSTALL_DIR path is correct because
1363
+ # ~/.claude/settings.json is a per-user global file, not per-project.
1364
+ # NOT $INSTALL_DIR/.claws-bin — that directory IS gitignored and missing on fresh clones.
1365
+ # CLAWS_NO_GLOBAL_HOOKS=1 skips this step entirely — useful for testing
1366
+ # an isolated install without touching the user's global Claude Code config.
1367
+ if [ -f "$INSTALL_DIR/scripts/inject-settings-hooks.js" ]; then
1368
+ if [ "${CLAWS_NO_GLOBAL_HOOKS:-0}" != "1" ]; then
1369
+ echo "Updating Claws hooks..."
1370
+ # M-18: use --update (atomic remove+add in one read-modify-write) instead of
1371
+ # two-pass --remove + add, which has a kill-window with zero Claws hooks.
1372
+ node --no-deprecation "$INSTALL_DIR/scripts/inject-settings-hooks.js" "$INSTALL_DIR/scripts" --update 2>&1 | sed 's/^/ /' || warn "settings hooks update failed"
1373
+ else
1374
+ echo " CLAWS_NO_GLOBAL_HOOKS=1 — skipping ~/.claude/settings.json registration"
1375
+ fi
1376
+ fi
1377
+ fi
1378
+
1379
+ ok "$LABEL: $cmd_count commands, rules, skills"
1380
+ }
1381
+
1382
+ if [ "$PROJECT_INSTALL" = "1" ]; then
1383
+ install_capabilities_into "$PROJECT_ROOT" "project"
1384
+ else
1385
+ warn "skipped project-local capabilities (no safe project dir)"
1386
+ fi
1387
+
1388
+ if [ "${CLAWS_GLOBAL_CONFIG:-0}" = "1" ]; then
1389
+ install_capabilities_into "$HOME" "global (~/.claude)"
1390
+ fi
1391
+
1392
+ # ─── Step 6b: Dev-discipline hooks ──────────────────────────────────────────
1393
+ # Copies scripts/dev-hooks/*.js into <project>/.claws-bin/dev-hooks/ and
1394
+ # registers them in <project>/.claude/settings.json via inject-dev-hooks.js.
1395
+ # Idempotent: safe to re-run on every update.
1396
+ # Fix #2 (v0.7.12): dev-hooks are contributor diagnostics — only install when:
1397
+ # a) CLAWS_INSTALL_DEV_HOOKS=1 is set explicitly, OR
1398
+ # b) the install target IS the Claws source tree (contributor dev environment).
1399
+ # Default user installs skip dev-hooks entirely to avoid SessionStart hook spam.
1400
+ _claws_is_dev_tree=0
1401
+ if [ -f "$PROJECT_ROOT/scripts/install.sh" ] && [ -d "$PROJECT_ROOT/extension/src" ]; then
1402
+ _claws_is_dev_tree=1
1403
+ fi
1404
+ if { [ "${CLAWS_INSTALL_DEV_HOOKS:-0}" = "1" ] || [ "$_claws_is_dev_tree" = "1" ]; } && [ "$PROJECT_INSTALL" = "1" ] && [ -d "$INSTALL_DIR/scripts/dev-hooks" ]; then
1405
+ step "Installing dev-discipline hooks"
1406
+ DEV_HOOKS_DST="$PROJECT_ROOT/.claws-bin/dev-hooks"
1407
+ mkdir -p "$DEV_HOOKS_DST"
1408
+ _dh_count=0
1409
+ for _dh in "$INSTALL_DIR/scripts/dev-hooks"/*.js; do
1410
+ [ -f "$_dh" ] || continue
1411
+ cp "$_dh" "$DEV_HOOKS_DST/" && _dh_count=$((_dh_count+1))
1412
+ done
1413
+ ok "copied $_dh_count dev-hook scripts → $DEV_HOOKS_DST"
1414
+
1415
+ if [ -f "$INSTALL_DIR/scripts/inject-dev-hooks.js" ]; then
1416
+ node --no-deprecation "$INSTALL_DIR/scripts/inject-dev-hooks.js" "$PROJECT_ROOT" 2>&1 | sed 's/^/ /' \
1417
+ || warn "inject-dev-hooks.js failed — dev hooks not registered"
1418
+ else
1419
+ warn "inject-dev-hooks.js not found — dev hooks not registered"
1420
+ fi
1421
+ unset _dh _dh_count
1422
+ fi
1423
+ unset _claws_is_dev_tree
1424
+
1425
+ # ─── Step 7: Shell hook ────────────────────────────────────────────────────
1426
+ step "Injecting shell hook"
1427
+ HOOK_SOURCE="source \"$INSTALL_DIR/scripts/shell-hook.sh\""
1428
+ HOOK_MARKER="# CLAWS terminal hook"
1429
+
1430
+ inject_hook() {
1431
+ local rcfile="$1"
1432
+ touch "$rcfile" 2>/dev/null || true
1433
+
1434
+ # Detect prior state BEFORE rewriting so we can report accurately.
1435
+ local had_stale=0
1436
+ local had_marker=0
1437
+ if grep -q "CLAWS terminal hook" "$rcfile" 2>/dev/null; then
1438
+ had_marker=1
1439
+ if ! grep -Fq "$HOOK_SOURCE" "$rcfile" 2>/dev/null; then
1440
+ had_stale=1
1441
+ fi
1442
+ fi
1443
+ # Detect orphaned source lines (marker missing, source survived from a
1444
+ # prior install whose sed delete failed on BSD sed). Audit 4 finding G.
1445
+ if grep -Eq '^[[:space:]]*source[[:space:]].*/shell-hook\.sh' "$rcfile" 2>/dev/null \
1446
+ && ! grep -Fq "$HOOK_SOURCE" "$rcfile" 2>/dev/null; then
1447
+ had_stale=1
1448
+ fi
1449
+
1450
+ # M-01: create a timestamped backup of the dotfile BEFORE any modification.
1451
+ # Allows the user to restore if something goes wrong. Only created when the
1452
+ # file already has content — no backup for a freshly touch'd empty file.
1453
+ # Keep only the 3 most recent backups to prevent accumulation on repeated runs.
1454
+ local tmp="$rcfile.claws-tmp.$$"
1455
+ if [ -s "$rcfile" ]; then
1456
+ local _bak_ts
1457
+ _bak_ts=$(date -u +%Y%m%dT%H%M%SZ 2>/dev/null || date +%Y%m%dT%H%M%SZ)
1458
+ cp "$rcfile" "${rcfile}.claws-bak.${_bak_ts}" 2>/dev/null || true
1459
+ # Prune older backups — keep the 3 newest, remove the rest.
1460
+ ls -1t "${rcfile}.claws-bak."* 2>/dev/null | tail -n +4 | xargs rm -f 2>/dev/null || true
1461
+ fi
1462
+
1463
+ # Portable cleanup via awk (works on BSD awk and GNU awk identically).
1464
+ # Replaces the GNU-only `sed '/pat/,+1d'` form, which silently failed on
1465
+ # macOS ≤ Monterey and left orphaned `source ".../shell-hook.sh"` lines.
1466
+ #
1467
+ # M-01: strips ONLY lines inside a Claws-marked block:
1468
+ # 1. Strip every `# CLAWS terminal hook` marker line.
1469
+ # 2. Strip the immediately following line IF it is the Claws source line.
1470
+ # The previous generic `/source .../shell-hook\.sh/` regex is removed because
1471
+ # it matched non-Claws tools (oh-my-zsh, asdf, custom dotfiles) causing data loss.
1472
+ #
1473
+ # F4: orphaned-marker edge case — if a user manually deleted the source line but
1474
+ # left the marker, the old `skip { skip=0; next }` would silently strip whatever
1475
+ # user content happened to follow the marker. Fix: only skip the following line if
1476
+ # it matches the Claws source pattern; otherwise keep it (skip=0; print).
1477
+ #
1478
+ # M-17: always promote awk output when awk succeeds, even if output is empty.
1479
+ # When the file contains ONLY the Claws block, awk produces no output (empty tmp).
1480
+ # The old `[ -s "$tmp" -o ! -s "$rcfile" ]` guard prevented promotion in that case
1481
+ # (rcfile had content → `! -s` false; tmp empty → `-s` false → guard fails), so the
1482
+ # original was left intact and a new block was appended on the next install, creating
1483
+ # duplicate hooks. Fix: mv unconditionally when awk exits 0.
1484
+ if awk '
1485
+ /# CLAWS terminal hook/ { skip = 1; next }
1486
+ skip && /source.*shell-hook\.sh/ { skip = 0; next }
1487
+ skip { skip = 0; print }
1488
+ { print }
1489
+ ' "$rcfile" > "$tmp" 2>/dev/null; then
1490
+ mv "$tmp" "$rcfile" 2>/dev/null || rm -f "$tmp"
1491
+ else
1492
+ rm -f "$tmp" 2>/dev/null || true
1493
+ fi
1494
+
1495
+ if printf "\n%s\n%s\n" "$HOOK_MARKER" "$HOOK_SOURCE" >> "$rcfile" 2>/dev/null; then
1496
+ if [ "$had_stale" = "1" ]; then
1497
+ ok "refreshed in $(basename "$rcfile") (removed stale path)"
1498
+ elif [ "$had_marker" = "1" ]; then
1499
+ ok "refreshed in $(basename "$rcfile")"
1500
+ else
1501
+ ok "added to $(basename "$rcfile")"
1502
+ fi
1503
+ else
1504
+ warn "could not write to $rcfile"
1505
+ fi
1506
+ }
1507
+
1508
+ [ -f "$INSTALL_DIR/scripts/shell-hook.sh" ] \
1509
+ || die "shell-hook.sh missing from $INSTALL_DIR/scripts/ — clone may be incomplete."
1510
+ inject_hook "$HOME/.zshrc"
1511
+ # Use `zsh -n` for .zshrc (zsh-only syntax like `setopt`/`autoload -Uz` parses
1512
+ # fine in zsh but `bash -n` reports false-positive errors). Fall back to `bash -n`
1513
+ # only if zsh is not installed. Audit 1 finding H-2.
1514
+ if command -v zsh &>/dev/null; then
1515
+ zsh -n "$HOME/.zshrc" 2>/dev/null || warn "~/.zshrc has a syntax error after hook injection — check manually"
1516
+ else
1517
+ bash -n "$HOME/.zshrc" 2>/dev/null || warn "~/.zshrc has a syntax error after hook injection — check manually"
1518
+ fi
1519
+ inject_hook "$HOME/.bashrc"
1520
+ bash -n "$HOME/.bashrc" 2>/dev/null || warn "~/.bashrc has a syntax error after hook injection — check manually"
1521
+ if [ "$PLATFORM" = "Darwin" ]; then
1522
+ inject_hook "$HOME/.bash_profile"
1523
+ bash -n "$HOME/.bash_profile" 2>/dev/null || warn "~/.bash_profile has a syntax error after hook injection — check manually"
1524
+ fi
1525
+
1526
+ if [ -d "$HOME/.config/fish" ]; then
1527
+ FISH_CONF="$HOME/.config/fish/conf.d/claws.fish"
1528
+ mkdir -p "$HOME/.config/fish/conf.d" 2>/dev/null
1529
+ # Write a minimal conf.d loader that sets CLAWS_DIR and sources the
1530
+ # standalone shell-hook.fish (no bass dependency required).
1531
+ {
1532
+ echo "# CLAWS terminal hook (auto-generated — do not edit)"
1533
+ echo "set -gx CLAWS_DIR '$INSTALL_DIR'"
1534
+ echo "set -gx CLAWS_SOCKET '.claws/claws.sock'"
1535
+ echo "if test -f '$INSTALL_DIR/scripts/shell-hook.fish'"
1536
+ echo " source '$INSTALL_DIR/scripts/shell-hook.fish'"
1537
+ echo "end"
1538
+ } > "$FISH_CONF" && ok "wrote fish conf (native fish, no bass required)" || warn "could not write fish config"
1539
+ fi
1540
+
1541
+ # ── Nushell hook ─────────────────────────────────────────────────────────────
1542
+ # Nushell sources env.nu on startup. We append a CLAWS_DIR assignment if absent.
1543
+ _NU_ENV="$HOME/.config/nushell/env.nu"
1544
+ _NU_CONFIG="$HOME/.config/nushell/config.nu"
1545
+ if [ -f "$_NU_ENV" ] || [ -f "$_NU_CONFIG" ]; then
1546
+ _NU_TARGET="${_NU_ENV:-$_NU_CONFIG}"
1547
+ if ! grep -q "CLAWS_DIR" "$_NU_TARGET" 2>/dev/null; then
1548
+ {
1549
+ printf '\n# CLAWS terminal hook\n'
1550
+ printf '$env.CLAWS_DIR = "%s"\n' "$INSTALL_DIR"
1551
+ printf '$env.CLAWS_SOCKET = ".claws/claws.sock"\n'
1552
+ } >> "$_NU_TARGET" && ok "wrote nushell env ($( basename "$_NU_TARGET" ))" \
1553
+ || warn "could not write nushell config"
1554
+ else
1555
+ ok "nushell env already has CLAWS_DIR — skipped"
1556
+ fi
1557
+ fi
1558
+ unset _NU_ENV _NU_CONFIG _NU_TARGET
1559
+
1560
+ # ─── Step 8: Verify ────────────────────────────────────────────────────────
1561
+ step "Verifying"
1562
+
1563
+ CHECKS_PASS=0
1564
+ CHECKS_FAIL=0
1565
+ _ok() { ok "$*"; CHECKS_PASS=$((CHECKS_PASS+1)); }
1566
+ _miss() { bad "$*"; CHECKS_FAIL=$((CHECKS_FAIL+1)); }
1567
+
1568
+ if [ "${#INSTALLED_EDITORS[@]}" -gt 0 ]; then
1569
+ _ok "Extension installed in: ${INSTALLED_EDITORS[*]}"
1570
+ else
1571
+ _miss "Extension not installed in any editor — run /claws-fix"
1572
+ fi
1573
+ [ -f "$INSTALL_DIR/extension/dist/extension.js" ] && _ok "Extension bundle built" || warn "Extension bundle missing — fallback to legacy JS active"
1574
+ [ -f "$MCP_PATH" ] && _ok "MCP server exists at $MCP_PATH" || _miss "$MCP_PATH missing"
1575
+ if command -v node &>/dev/null; then
1576
+ _ok "Node.js available ($(node --version)) — $(node -e 'process.stdout.write(process.execPath)' 2>/dev/null)"
1577
+ info "Note: GUI-launched VS Code may resolve a different Node.js PATH than this shell"
1578
+ else
1579
+ _miss "node not found"
1580
+ fi
1581
+
1582
+ if [ "$PROJECT_INSTALL" = "1" ]; then
1583
+ if [ -f "$PROJECT_ROOT/.mcp.json" ] && node -e "JSON.parse(require('fs').readFileSync('$PROJECT_ROOT/.mcp.json','utf8'))" 2>/dev/null; then
1584
+ _ok "Project .mcp.json (present and valid JSON)"
1585
+ elif [ -f "$PROJECT_ROOT/.mcp.json" ]; then
1586
+ _miss "Project .mcp.json exists but is invalid JSON — MCP server will fail to load"
1587
+ else
1588
+ _miss "project .mcp.json missing"
1589
+ fi
1590
+ [ -f "$PROJECT_ROOT/.claws-bin/mcp_server.js" ] && _ok "Project .claws-bin/mcp_server.js" || _miss "project mcp_server.js copy missing"
1591
+ if [ "${CLAWS_SKIP_EXTENSION_COPY:-0}" != "1" ]; then
1592
+ [ -f "$PROJECT_ROOT/.claws-bin/extension/dist/extension.js" ] \
1593
+ && _ok "Project .claws-bin/extension/ (visible copy)" \
1594
+ || warn "project .claws-bin/extension/ not copied"
1595
+ fi
1596
+ [ -f "$PROJECT_ROOT/.claws-bin/README.md" ] && _ok "Project .claws-bin/README.md" || warn "project .claws-bin/README.md missing"
1597
+ [ -d "$PROJECT_ROOT/.claws-bin/hooks" ] && _ok "Project .claws-bin/hooks/ (lifecycle hooks)" || _miss "project .claws-bin/hooks/ missing"
1598
+ # BUG-28: verify MCP spawn-class PreToolUse hooks registered (Monitor arm gate)
1599
+ if [ -f "$HOME/.claude/settings.json" ]; then
1600
+ if CLAWS_SETTINGS_CHECK="$HOME/.claude/settings.json" node --no-deprecation -e "
1601
+ const s = JSON.parse(require('fs').readFileSync(process.env.CLAWS_SETTINGS_CHECK, 'utf8'));
1602
+ const h = (s.hooks && s.hooks.PreToolUse) || [];
1603
+ process.exit(h.some(e => e.matcher && e.matcher.includes('mcp__claws__claws_worker')) ? 0 : 1);
1604
+ " 2>/dev/null; then
1605
+ _ok "MCP spawn-class PreToolUse hooks registered in ~/.claude/settings.json"
1606
+ else
1607
+ _miss "MCP spawn-class PreToolUse hooks missing — re-run inject-settings-hooks.js to register Monitor gate"
1608
+ fi
1609
+ fi
1610
+ if [ -f "$HOME/.claude/settings.json" ]; then
1611
+ if CLAWS_SETTINGS_CHECK="$HOME/.claude/settings.json" node --no-deprecation -e "
1612
+ const s = JSON.parse(require('fs').readFileSync(process.env.CLAWS_SETTINGS_CHECK, 'utf8'));
1613
+ const h = (s.hooks && s.hooks.PostToolUse) || [];
1614
+ process.exit(h.some(e => e.matcher && e.matcher.includes('mcp__claws__claws_worker')) ? 0 : 1);
1615
+ " 2>/dev/null; then
1616
+ _ok "PostToolUse spawn-class hooks registered in ~/.claude/settings.json"
1617
+ else
1618
+ _miss "PostToolUse spawn-class hooks missing — re-run inject-settings-hooks.js to register Wave C monitor gate"
1619
+ fi
1620
+ fi
1621
+ [ -f "$PROJECT_ROOT/.vscode/extensions.json" ] && grep -q "neunaha.claws" "$PROJECT_ROOT/.vscode/extensions.json" 2>/dev/null && _ok "Project .vscode/extensions.json recommends claws" || warn "project .vscode/extensions.json missing claws recommendation"
1622
+ [ -d "$PROJECT_ROOT/.claude/commands" ] && _ok "Project .claude/commands" || _miss "project commands missing"
1623
+ [ -d "$PROJECT_ROOT/.claude/skills" ] && _ok "Project .claude/skills" || _miss "project skills missing"
1624
+ [ -d "$PROJECT_ROOT/.claude/rules" ] && _ok "Project .claude/rules" || _miss "project rules missing"
1625
+ [ -f "$PROJECT_ROOT/CLAUDE.md" ] && _ok "Project CLAUDE.md" || warn "project CLAUDE.md not created"
1626
+ fi
1627
+
1628
+ # Test MCP server handshake (portable — no dependency on GNU timeout)
1629
+ VERIFY_MCP="$MCP_PATH"
1630
+ [ "$PROJECT_INSTALL" = "1" ] && [ -f "$PROJECT_ROOT/.claws-bin/mcp_server.js" ] && VERIFY_MCP="$PROJECT_ROOT/.claws-bin/mcp_server.js"
1631
+ if command -v node &>/dev/null && [ -f "$VERIFY_MCP" ]; then
1632
+ if MCP_TEST=$(node --no-deprecation -e '
1633
+ const { spawn } = require("child_process");
1634
+ const mcp = spawn("node", [process.argv[1]], { stdio: ["pipe", "pipe", "ignore"] });
1635
+ const req = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "claws-install", version: "1" } } });
1636
+ let buf = "";
1637
+ const done = (code, out) => { try { mcp.kill(); } catch {} ; process.stdout.write(out); process.exit(code); };
1638
+ const timer = setTimeout(() => done(1, "TIMEOUT"), 5000);
1639
+ mcp.stdout.on("data", d => { buf += d.toString("utf8"); if (buf.includes("claws")) { clearTimeout(timer); done(0, buf.slice(0, 200)); } });
1640
+ mcp.on("error", e => { clearTimeout(timer); done(1, "SPAWN_ERROR: " + e.message); });
1641
+ mcp.stdin.write(req + "\n");
1642
+ ' "$VERIFY_MCP" 2>&1) && echo "$MCP_TEST" | grep -q "claws"; then
1643
+ _ok "MCP server starts and responds (initialize OK)"
1644
+ else
1645
+ _miss "MCP server failed initialize — run: node $VERIFY_MCP"
1646
+ info "$MCP_TEST"
1647
+ fi
1648
+ fi
1649
+
1650
+ echo ""
1651
+ if [ "$CHECKS_FAIL" -eq 0 ]; then
1652
+ ok "$CHECKS_PASS checks passed"
1653
+ else
1654
+ warn "$CHECKS_PASS passed, $CHECKS_FAIL issue(s) — see above"
1655
+ fi
1656
+
1657
+ # Verify shell hook is active in user's rc files
1658
+ _hook_verified=0
1659
+ for _rc in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile"; do
1660
+ if [ -f "$_rc" ] && grep -q "CLAWS terminal hook" "$_rc" 2>/dev/null; then
1661
+ _hook_verified=1
1662
+ break
1663
+ fi
1664
+ done
1665
+ if [ "$_hook_verified" = "0" ]; then
1666
+ warn "Shell hook not detected in any rc file — run: source $INSTALL_DIR/scripts/shell-hook.sh"
1667
+ fi
1668
+
1669
+ # ─── End-of-install banner ─────────────────────────────────────────────────
1670
+ cat <<BANNER
1671
+
1672
+ ${C_BOLD}██████╗██╗ █████╗ ██╗ ██╗███████╗
1673
+ ██╔════╝██║ ██╔══██╗██║ ██║██╔════╝
1674
+ ██║ ██║ ███████║██║ █╗ ██║███████╗
1675
+ ██║ ██║ ██╔══██║██║███╗██║╚════██║
1676
+ ╚██████╗███████╗██║ ██║╚███╔███╔╝███████║
1677
+ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝ ╚══════╝${C_RESET}
1678
+
1679
+ ${C_BOLD}Terminal Control Bridge${C_RESET} v$EXT_VERSION — installed.
1680
+
1681
+ BANNER
1682
+ if [ "$PROJECT_INSTALL" = "1" ]; then
1683
+ printf ' Project: %s\n' "$PROJECT_ROOT"
1684
+ printf ' MCP server: %s\n' "$PROJECT_ROOT/.claws-bin/mcp_server.js"
1685
+ printf ' Registered: %s\n' "$PROJECT_ROOT/.mcp.json"
1686
+ else
1687
+ printf ' Project: ${C_YELLOW}(none — re-run from your project root)${C_RESET}\n'
1688
+ printf ' MCP server: %s\n' "$MCP_PATH"
1689
+ fi
1690
+ if [ "${#INSTALLED_EDITORS[@]}" -gt 0 ]; then
1691
+ printf ' Extension: installed in %s\n' "${INSTALLED_EDITORS[*]}"
1692
+ printf ' (method: %s)\n' "$VSIX_INSTALL_METHOD"
1693
+ printf ' (loaded from %s)\n' "$HOME/.vscode/extensions/neunaha.claws-$EXT_VERSION"
1694
+ if [ "$PROJECT_INSTALL" = "1" ] && [ "${CLAWS_SKIP_EXTENSION_COPY:-0}" != "1" ]; then
1695
+ printf ' (visible copy in %s/.claws-bin/extension/)\n' "$PROJECT_ROOT"
1696
+ fi
1697
+ else
1698
+ printf ' Extension: ${C_YELLOW}NOT INSTALLED — run /claws-fix${C_RESET}\n'
1699
+ fi
1700
+ printf ' Install log: %s\n' "$CLAWS_LOG"
1701
+ cat <<NEXT
1702
+
1703
+ ${C_BOLD}── One step left to activate ──${C_RESET}
1704
+ ${C_BOLD}Reload VS Code:${C_RESET} Cmd+Shift+P → "Developer: Reload Window"
1705
+
1706
+ That's it. The extension activates on reload; the MCP tools come online
1707
+ the next time you start a Claude Code session in this project (new
1708
+ sessions auto-pick-up .mcp.json — no manual restart required if Claude
1709
+ Code isn't already running here).
1710
+
1711
+ ${C_BOLD}── If something is off ──${C_RESET}
1712
+ MCP tools not appearing? /claws-fix
1713
+ Want to report an issue? /claws-report
1714
+ Update later: /claws-update
1715
+
1716
+ ${C_DIM}Optional: export CLAWS_STRICT=1 to hard-block long-running Bash via PreToolUse hook${C_RESET}
1717
+ ${C_DIM}Worker binary: defaults to 'claude'. Override via:${C_RESET}
1718
+ /claws-bin <name> (slash command — recommended)
1719
+ echo name > .claws/claude-bin (file override, gitignored)
1720
+ export CLAWS_CLAUDE_BIN=<name> (shell env var)
1721
+
1722
+ Docs: https://github.com/neunaha/claws
1723
+ Website: https://neunaha.github.io/claws/
1724
+
1725
+ NEXT
1726
+
1727
+ # Source shell hook last so its output doesn't push the banner off-screen.
1728
+ # shellcheck disable=SC1090
1729
+ info "Open a new terminal to activate the shell hook."