Files
web-terminal/scripts/setup-hooks.mjs
Yaojia Wang c81cebdc47 feat(v0.3): H4 — live Claude status badge + notifications + setup-hooks
- terminal-session: handle 'status' frame → claudeStatus + onClaudeStatus
- tabs: per-tab Claude glyph (⚙ working /  waiting / ✓ idle); inactive
  'waiting' tab gets an amber highlight + a browser notification (permission
  requested on first tab switch)
- scripts/setup-hooks.mjs + 'npm run setup-hooks': idempotently install/remove
  command hooks in ~/.claude/settings.json (inline curl to $WEBTERM_HOOK_URL with
  $WEBTERM_SESSION; no-op outside web-terminal); backs up settings.json
- Browser-verified end-to-end: PermissionRequest→, PreToolUse→⚙, Stop→✓;
  inactive waiting tab shows amber + glyph. 205 tests green.
2026-06-17 19:00:34 +02:00

78 lines
3.0 KiB
JavaScript

#!/usr/bin/env node
/**
* scripts/setup-hooks.mjs — install/remove web-terminal's Claude Code hooks (H2).
*
* Adds `command` hooks to ~/.claude/settings.json that POST each hook event to
* the web-terminal server so it can show live Claude status per tab. The command
* is an inline curl that uses two env vars the server injects into each spawned
* shell: $WEBTERM_HOOK_URL (the server's /hook URL, with the right port) and
* $WEBTERM_SESSION (which tab). Outside web-terminal those vars are unset, so
* curl no-ops (|| true) — the hooks are harmless in normal terminals.
*
* Usage:
* node scripts/setup-hooks.mjs # install (idempotent)
* node scripts/setup-hooks.mjs --remove # uninstall
*
* Backs up settings.json to settings.json.webterm-bak before writing.
*/
import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from 'node:fs'
import { homedir } from 'node:os'
import path from 'node:path'
const SETTINGS = path.join(homedir(), '.claude', 'settings.json')
const MARKER = 'WEBTERM_HOOK_URL' // identifies our hook entries
const COMMAND =
'curl -s -X POST "$WEBTERM_HOOK_URL" -H "X-Webterm-Session: $WEBTERM_SESSION"' +
' -H "Content-Type: application/json" --data-binary @- >/dev/null 2>&1 || true'
// Events we care about (status derives from the event/notification_type server-side).
const EVENTS = ['UserPromptSubmit', 'PreToolUse', 'Notification', 'PermissionRequest', 'Stop', 'SessionEnd']
const remove = process.argv.includes('--remove')
let settings = {}
if (existsSync(SETTINGS)) {
try {
settings = JSON.parse(readFileSync(SETTINGS, 'utf8'))
} catch {
console.error(`${SETTINGS} is not valid JSON — aborting (fix it first).`)
process.exit(1)
}
copyFileSync(SETTINGS, `${SETTINGS}.webterm-bak`)
}
if (typeof settings.hooks !== 'object' || settings.hooks === null) settings.hooks = {}
// Strip any existing web-terminal hook groups (makes install idempotent + powers --remove).
for (const ev of Object.keys(settings.hooks)) {
const groups = settings.hooks[ev]
if (!Array.isArray(groups)) continue
settings.hooks[ev] = groups.filter(
(g) =>
!(
g &&
Array.isArray(g.hooks) &&
g.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(MARKER))
),
)
if (settings.hooks[ev].length === 0) delete settings.hooks[ev]
}
if (!remove) {
for (const ev of EVENTS) {
if (!Array.isArray(settings.hooks[ev])) settings.hooks[ev] = []
settings.hooks[ev].push({ hooks: [{ type: 'command', command: COMMAND }] })
}
}
mkdirSync(path.dirname(SETTINGS), { recursive: true })
writeFileSync(SETTINGS, `${JSON.stringify(settings, null, 2)}\n`)
console.log(
remove
? `Removed web-terminal hooks from ${SETTINGS}`
: `Installed web-terminal hooks into ${SETTINGS} (events: ${EVENTS.join(', ')})`,
)
if (existsSync(`${SETTINGS}.webterm-bak`)) console.log(`Backup: ${SETTINGS}.webterm-bak`)
console.log('Restart any running `claude` sessions for the hooks to take effect.')