synabun 2026.3.31003 → 2026.3.31005

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,164 +1,58 @@
1
- # SynaBun Changelog
2
-
3
- Raw internal changelogs are archived in [changelog/](changelog/).
4
-
5
- ## v.2026.03.30
6
-
7
- ### Added — Message Queue System
8
- - **Queue messages while Claude is processing** — Stack multiple messages in a FIFO queue that auto-advances between turns. Includes drag-to-reorder, inline editing, pause/resume, and a collapsible queue tray UI
9
- - **Keyboard behavior change** — Enter while running now queues the message; Shift+Enter triggers the existing /btw interrupt. Send button icon updates dynamically based on state
10
-
11
- ### Added — Notification Click-to-Navigate
12
- - **Clicking a notification focuses the exact source** — Toast clicks route to the specific terminal session or Claude panel tab that triggered the notification. OS banner notifications use service worker `postMessage` relay for click-through routing
13
- - **All 10 notification call sites updated** — 8 panel calls pass tab ID, 2 terminal calls pass session ID
14
-
15
- ### Added — Hook-Side Auto-Recall
16
- - **Top 3 related memories injected into every prompt** — The prompt-submit hook calls `/api/hook-recall` on every substantive user prompt, auto-detects project from `cwd`, and injects matching memories (score >= 0.4) directly into `additionalContext` with a 3-second timeout and silent fallback
17
-
18
- ### Added — Browser Stream Toggle
19
- - **Disable screencast to save ~50% CPU** — New toggle in Settings > Browser. When disabled, all CDP frame capture, encoding, and WebSocket broadcasting stops. All MCP browser tools continue working — only the Neural Interface preview stream is affected
20
-
21
- ### Added — Edit Plan in Code Editor
22
- - **Open plan files directly in the Neural Interface editor** — Clicking "Edit plan" on the post-plan action card opens the MD file in the code editor with a green accent banner. On save, the updated plan is auto-sent to Claude. Falls back gracefully for plans from prior sessions
23
-
24
- ### Added — Recall Settings Redesign
25
- - **4 recall profiles replace basic token budget** — Quick (3 results, tight matching), Balanced (5 results), Deep (10 results, loose matching), Custom (opens all individual controls). Profile selection auto-configures all parameters
26
- - **Fine-tune controls** — Results per recall, minimum importance, similarity threshold, content length, session context toggle, and recency boost
27
- - **Impact indicator** — Shows estimated tokens per recall, reachable memories, and session context status
28
-
29
- ### Added — Isolated Agent System
30
- - **Spawn Claude Code as independent child processes** — Full session isolation with no hook interference, no MCP cross-talk, and cross-platform process management (no node-pty dependency). Each agent gets its own session ID, strict MCP config, and bypass permissions
31
- - **Agent cards in Automation Studio** — Live-streaming output with tool call count, cost display, elapsed time, and stop/remove controls
32
-
33
- ### Added — Image Gallery
34
- - **Floating draggable panel for browsing captured images** — Search, type filters (Screenshots/Attachments/Whiteboard/Pastes), sort options, favorites toggle. Full lightbox with keyboard navigation (arrow keys, Escape) and action buttons (favorite, add to whiteboard, download, delete)
35
-
36
- ### Added — Cron-Driven Schedule System
37
- - **Create recurring automations with cron expressions** — Timezone support, day-of-week themes for content calendars, 18 cron presets across 3 groups (Frequency, Daily, Weekly). Full CRUD API with WebSocket events for live UI updates. Schedules tab in Automation Studio with status badges and test-fire button
38
-
39
- ### Added — Multi-Browser Session Isolation
40
- - **Dedicated browser per automation** — Each automation gets its own browser session with environment-based pinning via `SYNABUN_BROWSER_SESSION`. Concurrent automations no longer fight over the same browser. Browser cleanup on loop stop
41
-
42
- ### Added — Multi-Tab Browser Architecture
43
- - **Tabs per browser session** — Create, switch, and close tabs via REST and WebSocket. Each tab stores its own page, CDP session, URL, and title. Active tab aliases preserve backward compatibility with all existing MCP tools
44
-
45
- ### Added — Notifications Settings Tab
46
- - **Dedicated notifications panel** — Master toggle, system permission status with color-coded indicator, sound controls with volume slider and test button, OS banner toggle, action-only filter, and end-to-end test notification button
47
-
48
- ### Added — Auto Backup System
49
- - **Scheduled ZIP backups to a user-chosen folder** — Configurable frequency (30min to 24h), native OS folder picker, atomic write pattern, single-file replacement. Settings UI with last backup time, file size, and error status
50
-
51
- ### Added — Whiteboard Enhancements
52
- - **Shape picker submenu** — Rectangle, Circle, and Triangle presets with inline SVG icons
53
- - **Floating move handle** — 4-directional arrow on selected text/list elements, mirroring the rotate handle
54
- - **Backdrop toggle button** — Eye icon on all windows to show/hide the backdrop overlay
55
- - **Multi-select shortcut** — `M` key toggles multi-select mode
56
-
57
- ### Added — Automation Studio Icon Picker Expansion
58
- - **13 new icons** — Social media (X, Instagram, Facebook, LinkedIn, TikTok, YouTube, WhatsApp, Discord), AI platforms (Claude, OpenAI, Gemini), Leonardo, and pin
59
-
60
- ### Added — Content Negotiation & Markdown Extraction
61
- - **Browser markdown extraction** — Strips noise elements, prefers main/article content areas, converts via `node-html-markdown`. New `format: "markdown"` parameter on `browser_content` tool
62
- - **Lightweight HTTP fetch** — `Accept: text/markdown` header negotiation with Cloudflare header detection and HTML fallback
63
-
64
- ### Added — LinkedIn Jobs Browser Extractor
65
- - **Structured job listing extraction** — Handles search results and homepage recommendations. Extracts 11 fields per job including salary parsing across multiple currencies. De-duplicates by job ID
66
-
67
- ### Fixed — Permission Spam in Claude Code Sidepanel
68
- - **Events leaked through buffer gate** — All message types now blocked during active permission prompts or ask questions (except `control_request`). Denying a permission now properly kills the running process. Queue and buffer cleared immediately on deny
69
-
70
- ### Fixed — AskUserQuestion Card Duplication
71
- - **Buffer flush ordering** — Reordered to flush while dedup flag is still active. Added DOM-level guard against rendering duplicate ask cards when an active card already exists
72
-
73
- ### Fixed — Edit Plan Auto-Implementing
74
- - **Edited plans now show review card** — After saving in the code editor, a PLAN UPDATED card renders with Implement/Compact/Edit buttons instead of auto-sending to Claude. Edited content is preserved and sent when implementing
75
-
76
- ### Fixed — Neural Interface Memory Leaks (11GB+)
77
- - **DOM pruning** — Messages capped at 600 nodes with batch removal of oldest 150. Message buffer capped at 500 during permission prompts. Event listener cleanup on Automation Studio panel close — previously leaked 6 document-level listeners per open/close cycle
78
-
79
- ### Fixed — Context Gauge Showing Inflated Values
80
- - **Cumulative token counts replaced with per-turn values** — Gauge now uses correct per-turn usage from assistant events instead of cumulative result event values that could reach multiples of the context window
81
-
82
- ### Fixed — Plan System "PLAN COMPLETE" Card Not Rendering
83
- - **Detection moved to assistant events** — In `--print` mode, built-in tool results aren't emitted. ExitPlanMode detection now fires from tool_use blocks in assistant events with flag-based handoff to the result/done handlers
84
-
85
- ### Fixed — Plan Storage Cross-Session Leak
86
- - **Plans now matched by content instead of modification time** — Four matching strategies (filename, exact content, substring, heading) replace the old blind "newest file wins" approach that grabbed wrong plans when multiple sessions were in plan mode
87
-
88
- ### Fixed — Floating Browser Close Button After Reconnection
89
- - **ID-based lookup replaces object identity** — Close and pin handlers now re-find the live session by ID instead of using stale closure-captured references that go invalid after session reconnection
90
-
91
- ### Fixed — Loop Cross-Session Leaks
92
- - **Terminal session ownership tracking** — Loop driver, hook pending claims, fallback scans, and session-start detection all now filter by `terminalSessionId` instead of blindly grabbing the first available loop
93
-
94
- ### Fixed — Loop Time Cap Silently Clamped
95
- - **Raised from 60 to 480 minutes** — The terminal loop endpoint had a stale `Math.min(..., 60)` cap that silently truncated all values. Iteration cap raised from 50 to 200. UI sliders updated to match
96
-
97
- ### Fixed — Browser Screencast Freezing
98
- - **Backpressure-aware frame renderer** — Replaced unbounded `Image()` creation with a latest-frame-wins pattern. Only one decode runs at a time; stale frames are dropped. Applied to all three rendering surfaces (terminal, floating, panel)
99
-
100
- ### Fixed — White Dropdown Backgrounds on Windows
101
- - **`color-scheme: dark` declaration** — Added to `:root` so native form controls render in dark mode on Windows Chrome/Edge
102
-
103
- ### Fixed — Session Isolation Leak Across Tabs
104
- - **Project/model/effort are now per-tab state** — Previously stored in shared global storage, causing one tab's project selection to leak into another. Standalone chat page also fixed with `sessionStorage`-based tab isolation
105
-
106
- ### Fixed — Image Pipeline
107
- - **File attachments now reach Claude** — Fixed 5 separate issues: disabled send button ignoring files, early return blocking file-only sends, path chips persisting after send, no framing text for attached files, BTW interrupt discarding files
108
- - **Pasted/dropped images** — Saved as temp files with Read tool instruction instead of multimodal (unsupported in `--print` mode)
109
- - **Clipboard operations** — Fallback for non-HTTPS contexts when accessing Neural Interface over LAN
110
-
111
- ### Fixed — Agent Launch Dialog
112
- - **DOM values read before dialog destruction** — All input reads moved before `closeLaunchDialog()` which calls `.remove()`. Previously all 5 reads returned `null`, making agents launch with empty MCP config and no browser session
113
-
114
- ### Fixed — Cost Tracking
115
- - **Navbar label stuck at $0.00** — Event payload now includes session cost field. Widget no longer overwrites navbar with monthly total. Docked widget fetches data on init
116
-
117
- ### Fixed — Pencil Tool Screenshot Rendering
118
- - **Smooth bezier curves replace polygonal line segments** — Screenshot capture now uses pre-computed SVG path data instead of straight `lineTo()` segments. Increased smoothing passes and reduced jitter threshold
119
-
120
- ### Fixed — Window Controls Overlay on macOS
121
- - **CSS specificity fix** — WCO padding no longer silently overwritten by base styles. Theme color matched to navbar composited appearance. Vertical alignment matched to traffic light midpoint
122
-
123
- ### Fixed — Notifications in Safari PWA
124
- - **Service worker routing** — Replaced direct `Notification` constructor with `serviceWorker.postMessage()` → `registration.showNotification()` flow
125
-
126
- ### Fixed — Whiteboard List Items Lost on Reload
127
- - **Content synced before persisting** — List contenteditable DOM content now flushed to the model array before serialization. Added flush calls to all exit paths
128
-
129
- ### Changed — Toolbar Modernization
130
- - **Apple-style unified frosted glass pills** — Top-right navbar and bottom-right workspace toolbar converted from separate button groups to continuous pill bars with shared backdrop blur. Refined hover effects with spring animations, unified active state dots, and cleaner SVG icons
131
-
132
- ### Changed — Window Header Modernization
133
- - **Unified design language across 13+ window types** — Gradient backgrounds, softer borders, lighter title weight, ghost-style action buttons, and red-tint close hover
134
-
135
- ### Changed — Batched AskUserQuestion Submission
136
- - **Options toggle freely until submitted** — Submit button with progress counter (`Submit 2/3`) enables when all questions answered. Free-text questions render inline input instead of using the main chat box
137
-
138
- ### Changed — Leonardo.ai Converted to 100% Browser-Based
139
- - **Removed all API tools** — 4 browser tools replace 14 API tools. No API key required. Comprehensive UI map with 18 image models, 23 video models, 39 motion controls, and 3-layer style stacking. Skill modules expanded with full questionnaire flows
140
-
141
- ### Changed — Automation Studio Launch Flow
142
- - **Inline panel replaces overlay modal** — Launch configuration renders directly inside the detail view. New "Run in" destination toggle for Floating vs Side Panel
143
-
144
- ### Changed — Focus Mode Zero-Resource Consumption
145
- - **All event handlers paused in 3D and 2D views** — Pointer, keyboard, wheel, and camera input fully disabled during focus mode. Resize handlers intentionally left active
146
-
147
- ### Changed — Session Monitor Rewritten
148
- - **Proper draggable/resizable window** — Replaces narrow fixed panel. Three tabs: Sessions (cards with uptime/PID), Agents (status badges/cost), Health (leak severity grouping with cleanup actions)
149
-
150
- ### Changed — Browser Screencast Optimizations
151
- - **30fps frame cap** — Frames arriving faster than 33ms are dropped before decoding
152
- - **Pause on minimize** — Minimized browser pills stop CDP screencast entirely
153
- - **Focus-based throttling** — Non-focused browser windows throttle to ~2fps
154
- - **5-minute idle timeout** — Auto-pause after inactivity; canvas interaction resumes
155
- - **Server-side backpressure** — Clients with congested WebSocket buffers are skipped
156
-
157
- ### Changed — Window Close Behavior
158
- - **Outside-click closing disabled** — All windows now close exclusively via ESC or close button. ESC key handlers added to Settings, Sessions, Help, Image Gallery, and Terminal dir picker
159
-
160
- ### Changed — Custom Dropdowns for Settings
161
- - **Native `<select>` elements replaced** — All 4 Settings dropdowns converted to themed custom dropdown components for consistent cross-platform appearance
162
-
163
- ### Changed — Settings Add Project
164
- - **Native OS folder picker** — Replaces custom inline folder browser that had no drive/volume switching on Windows
1
+ # Changelog
2
+
3
+ ## 2026-03-31
4
+
5
+ ### Fixed — Browser Rendering Parity with Real Chrome
6
+ - Set Playwright `viewport: null` in headed mode so pages render at the actual window size instead of a forced 1280x800 emulated box
7
+ - Removed forced `deviceScaleFactor: 1` override — Chrome now uses its native DPR (2x on Retina) for correct rendering
8
+ - Fixes white-page rendering on complex SPAs like Twitter/X where the emulated viewport broke CSS flexbox layouts
9
+
10
+ ### Changed — Browser Stream Defaults to OFF
11
+ - Screencast streaming now disabled by default for fresh installs (was enabled)
12
+ - All 7 check sites updated: `disabled !== false` instead of `!!disabled` — stream only runs when explicitly toggled ON in Settings
13
+ - Affects server.js, ui-terminal.js (4 sites), ui-settings.js, ui-automation-studio.js
14
+
15
+ ### Fixed — Terminal Text Cropped at Bottom (FitAddon box-sizing Mismatch)
16
+ - **xterm FitAddon overcounted rows by ~2** — The global `* { box-sizing: border-box }` reset caused `getComputedStyle(parent).height` to return the border-box height (including 40px of vertical padding) for `.term-viewport`. xterm's FitAddon v0.10.0 uses this value as available space, calculating ~2 extra rows that rendered beyond the visible content area and got clipped
17
+ - Verified empirically via Playwright: border-box returns 386px vs child `height: 100%` resolving to 346px — a 40px mismatch equal to the viewport padding
18
+ - Fix: added `box-sizing: content-box` to `.term-viewport` (docked) and `.term-float-viewport-wrap .term-viewport` (floating tabs) — `getComputedStyle().height` now returns content height matching what `.xterm { height: 100% }` resolves to, with no visual size change
19
+
20
+ ### Fixed — Codex CLI Config Parse Error (TOML Serialization)
21
+ - **`tomlUpsertSection()` wrote JSON syntax for objects instead of TOML inline tables** — The `env` field in `~/.codex/config.toml` was serialized as `{"key":"value"}` (JSON, with colons) instead of `{key = "value"}` (TOML inline table, with `=`). Codex CLI rejected this with "missing assignment between key-value pairs, expected `=`"
22
+ - Added `tomlSerializeValue()` helper that recursively serializes strings, arrays, and objects with correct TOML syntax
23
+ - `tomlUpsertSection()` now calls `tomlSerializeValue(v)` instead of `JSON.stringify(v)`
24
+ - Affects `POST /api/setup/codex/mcp` endpoint — the onboarding/settings toggle that registers SynaBun MCP in Codex
25
+
26
+ ### Changed — Loop Browser: Shared System Chrome with Tabs
27
+ - **`ensureChromeDebuggable()` — one-time Chrome restart per NI boot** — New standalone function ensures Chrome is running with `--remote-debugging-port=9222`. Graceful AppleScript quit on macOS with `pgrep` polling (up to 8s), then `killall` fallback. Cleans `SingletonLock`, `DevToolsActivePort`, and crash markers before relaunch. Result is cached via `_chromeDebuggableReady` flag — subsequent calls just probe port 9222 and return instantly. Self-heals if Chrome was quit externally
28
+ - **`createBrowserSession` CDP block simplified** — Replaced ~120 lines of inline Chrome kill/relaunch logic with `await ensureChromeDebuggable()` + `chromium.connectOverCDP()`. No per-session Chrome restart
29
+ - **Loop launch rewritten (`POST /api/loop/launch`)** — Removed 15-second Strategy 2 UI poll entirely (wasted time when screencast disabled). New flow: scan `browserSessions` for existing CDP session → reuse it; if none, `createBrowserSession` once. Opens new tab per loop in the shared session instead of creating dedicated isolated sessions
30
+ - **Loop stop preserves shared session** — Made handler async. Closes only the loop's browser tab (not the entire session). Switches to next tab if the closed one was active. Session persists for next loop or manual use
31
+ - **Scheduled loops updated** — Same shared-session + tab approach applied to `launchScheduledLoop()`
32
+ - **MCP `resolveSession` fallback** — When `SYNABUN_BROWSER_SESSION` env is pinned but session no longer exists, verifies via `GET /api/browser/sessions` before using. Falls through to auto-select/create instead of failing with stale session ID
33
+ - **Browser enforcement prompt includes `browserTabId`** — `buildBrowserNote()` in `prompt-submit.mjs` now tells Claude which tab is theirs in the shared browser
34
+
35
+ ### Fixed — Greeting Re-fires on Every Message in Ongoing Sessions
36
+ - **`session-start.mjs` injected `## GREETING DIRECTIVE` into persistent `additionalContext`** — SessionStart hook's `additionalContext` persists in the conversation context for the entire session. Claude saw the "begin with this greeting" instruction on every turn and re-greeted, overpowering the `bootCancel` countermeasure on messages 2+
37
+ - Removed GREETING DIRECTIVE block and Session Boot Sequence block from `session-start.mjs` — these no longer persist in session context
38
+ - Removed dead greeting helper functions (`loadGreetingConfig`, `getProjectGreetingConfig`, `getTimeGreeting`, `resolveTemplate`, `formatReminders`) from `session-start.mjs`
39
+ - Added greeting helpers + `buildGreetingContext()` to `prompt-submit.mjs` — builds full greeting directive + boot sequence on message 1 only, so it never persists
40
+ - Removed `bootCancel` injection on messages 2+ (no longer needed since there's no persistent directive to cancel)
41
+
42
+ ### Fixed — Windows Cross-Platform Hook Compatibility
43
+ - **`hookCommandString()` embedded machine-specific absolute paths** — When hooks were installed into SynaBun's own `.claude/settings.json`, the function generated paths like `/Users/.../hooks/claude-code/script.mjs` that only worked on the originating OS. On Windows PowerShell, these macOS paths caused `SessionStart:startup hook error`. Fix: when `targetProjectPath` matches `PACKAGE_ROOT`, generate relative paths (`hooks/claude-code/script.mjs`) that work cross-platform. Absolute paths still used for global/external project installations
44
+ - Updated `addHookToSettings()`, `removeHookFromSettings()`, `isHookInstalled()`, `isSpecificHookInstalled()` with `targetProjectPath` parameter
45
+ - `removeHookFromSettings()` now matches both relative and absolute-path variants for cleanup of old installs
46
+
47
+ ### Fixed — Pre-WebSearch Hook Hangs on PowerShell (Missing stdin Timeout)
48
+ - **`pre-websearch.mjs` had no stdin timeout** — On PowerShell, stdin `end` event may never fire, causing the hook to hang indefinitely. Added 2000ms timeout that fires `handleInput()` directly if stdin doesn't close. Refactored inline handler to named `handleInput()` function
49
+
50
+ ### Fixed — Hooks Crash with Unhandled Errors Instead of Graceful Fallback
51
+ - **All 7 hooks now handle `uncaughtException` and `unhandledRejection`** — Added `process.on()` handlers to every hook file. Each outputs valid JSON appropriate to its hook type and exits with code 0 to prevent Claude Code from showing "hook error"
52
+ - `session-start.mjs` — outputs `hookSpecificOutput` with fallback instructions telling Claude to follow CLAUDE.md rules manually
53
+ - `prompt-submit.mjs`, `stop.mjs`, `post-remember.mjs`, `post-plan.mjs` — output `{}`
54
+ - `pre-compact.mjs` — calls `process.exit(0)` silently
55
+
56
+ ### Changed — Dependency Updates
57
+ - **mcp-server** — Updated 44 packages within semver ranges: `@modelcontextprotocol/sdk` 1.27.1→1.29.0, `@inquirer/prompts` 8.3.0→8.3.2, `vitest` 4.1.0→4.1.2, `@types/node` 22.19.13→22.19.15
58
+ - **neural-interface** — Updated 17 packages within semver ranges: `@anthropic-ai/claude-code` 2.1.71→2.1.89, `playwright` 1.58.2→1.59.0, `ws` 8.19.0→8.20.0. Fixed `lodash` prototype pollution vulnerability (GHSA-xxjr-mmjv-4gpg)
package/README.md CHANGED
@@ -9,6 +9,10 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
+ <a href="https://www.npmjs.com/package/synabun"><img src="https://img.shields.io/npm/v/synabun?color=cb3837&logo=npm&logoColor=white" alt="npm version" /></a>
13
+ <a href="https://www.npmjs.com/package/synabun"><img src="https://img.shields.io/npm/dm/synabun?color=cb3837&logo=npm&logoColor=white" alt="npm downloads" /></a>
14
+ <a href="./LICENSE"><img src="https://img.shields.io/npm/l/synabun?color=blue" alt="license" /></a>
15
+ <a href="https://github.com/danilokhury/Synabun/actions/workflows/ci.yml"><img src="https://github.com/danilokhury/Synabun/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
12
16
  <a href="https://discord.gg/x6yWqE9GZP"><img src="https://img.shields.io/badge/Discord-Join-5865F2?logo=discord&logoColor=white" alt="Discord" /></a>
13
17
  <a href="https://x.com/SynabunAI"><img src="https://img.shields.io/badge/Follow-%40SynabunAI-000000?logo=x&logoColor=white" alt="X / Twitter" /></a>
14
18
  <img src="https://img.shields.io/badge/Node.js-22%2B-339933?logo=nodedotjs&logoColor=white" alt="Node.js 22+" />
@@ -28,6 +28,10 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
28
28
  import { randomUUID } from 'node:crypto';
29
29
  import { detectProject, getMcpCategoriesPath, MCP_DATA_DIR, DATA_DIR } from './shared.mjs';
30
30
 
31
+ // Cross-platform safety: catch uncaught errors and output valid hook JSON
32
+ process.on('uncaughtException', () => { try { process.stdout.write('{}'); } catch {} process.exit(0); });
33
+ process.on('unhandledRejection', () => { try { process.stdout.write('{}'); } catch {} process.exit(0); });
34
+
31
35
  const __dirname = dirname(fileURLToPath(import.meta.url));
32
36
 
33
37
  // Debug log (append-only, survives across invocations)
@@ -29,6 +29,10 @@ import { dirname, join } from 'node:path';
29
29
  import { fileURLToPath } from 'node:url';
30
30
  import { loadCategories, buildCategoryReference, detectProject, DATA_DIR } from './shared.mjs';
31
31
 
32
+ // Cross-platform safety: catch uncaught errors and output valid hook JSON
33
+ process.on('uncaughtException', () => { try { process.stdout.write('{}'); } catch {} process.exit(0); });
34
+ process.on('unhandledRejection', () => { try { process.stdout.write('{}'); } catch {} process.exit(0); });
35
+
32
36
  const __dirname = dirname(fileURLToPath(import.meta.url));
33
37
  const PENDING_COMPACT_DIR = join(DATA_DIR, 'pending-compact');
34
38
  const PENDING_REMEMBER_DIR = join(DATA_DIR, 'pending-remember');
@@ -34,6 +34,10 @@ import { fileURLToPath } from 'node:url';
34
34
 
35
35
  import { getDataHome } from '../../lib/paths.js';
36
36
 
37
+ // Cross-platform safety: catch uncaught errors and exit cleanly
38
+ process.on('uncaughtException', () => { process.exit(0); });
39
+ process.on('unhandledRejection', () => { process.exit(0); });
40
+
37
41
  const __dirname = dirname(fileURLToPath(import.meta.url));
38
42
  const DATA_HOME = getDataHome();
39
43
  const CACHE_DIR = join(DATA_HOME, 'data', 'precompact');
@@ -37,7 +37,16 @@ const BLOCK_REASON = (toolName) =>
37
37
  let input = '';
38
38
  process.stdin.setEncoding('utf-8');
39
39
  process.stdin.on('data', chunk => { input += chunk; });
40
- process.stdin.on('end', async () => {
40
+ // Timeout: if stdin never closes (PowerShell edge case), proceed with what we have
41
+ const stdinTimeout = setTimeout(() => {
42
+ process.stdin.removeAllListeners('end');
43
+ handleInput();
44
+ }, 2000);
45
+ process.stdin.on('end', () => {
46
+ clearTimeout(stdinTimeout);
47
+ handleInput();
48
+ });
49
+ async function handleInput() {
41
50
  try {
42
51
  const { session_id, tool_name } = JSON.parse(input);
43
52
 
@@ -89,4 +98,4 @@ process.stdin.on('end', async () => {
89
98
  } catch {
90
99
  process.stdout.write(JSON.stringify({}));
91
100
  }
92
- });
101
+ }
@@ -17,14 +17,172 @@
17
17
  */
18
18
 
19
19
  import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, renameSync, readdirSync } from 'node:fs';
20
+ import { execSync } from 'node:child_process';
20
21
  import { dirname, join } from 'node:path';
21
22
  import { fileURLToPath } from 'node:url';
22
23
  import { detectProject, DATA_DIR } from './shared.mjs';
23
24
 
25
+ // Cross-platform safety: catch uncaught errors and output valid hook JSON
26
+ process.on('uncaughtException', () => { try { process.stdout.write('{}'); } catch {} process.exit(0); });
27
+ process.on('unhandledRejection', () => { try { process.stdout.write('{}'); } catch {} process.exit(0); });
28
+
24
29
  const __dirname = dirname(fileURLToPath(import.meta.url));
25
30
  const HOOK_FEATURES_PATH = join(DATA_DIR, 'hook-features.json');
26
31
  const PENDING_REMEMBER_DIR = join(DATA_DIR, 'pending-remember');
27
32
  const LOOP_DIR = join(DATA_DIR, 'loop');
33
+ const GREETING_CONFIG_PATH = join(DATA_DIR, 'greeting-config.json');
34
+
35
+ // --- Greeting helpers (moved from session-start.mjs) ---
36
+
37
+ function loadGreetingConfig() {
38
+ try {
39
+ if (!existsSync(GREETING_CONFIG_PATH)) return null;
40
+ return JSON.parse(readFileSync(GREETING_CONFIG_PATH, 'utf-8'));
41
+ } catch { return null; }
42
+ }
43
+
44
+ function getProjectGreetingConfig(config, project) {
45
+ if (!config) return null;
46
+ if (config.projects && config.projects[project]) {
47
+ return { ...config.defaults, ...config.projects[project] };
48
+ }
49
+ if (config.global) {
50
+ return { ...config.defaults, ...config.global };
51
+ }
52
+ return config.defaults || null;
53
+ }
54
+
55
+ function getTimeGreeting() {
56
+ const hour = new Date().getHours();
57
+ if (hour >= 5 && hour < 12) return 'Good morning';
58
+ if (hour >= 12 && hour < 17) return 'Good afternoon';
59
+ return 'Good evening';
60
+ }
61
+
62
+ function getGitBranch(cwd) {
63
+ try {
64
+ return execSync('git rev-parse --abbrev-ref HEAD', {
65
+ cwd,
66
+ encoding: 'utf-8',
67
+ timeout: 2000,
68
+ stdio: ['pipe', 'pipe', 'pipe'],
69
+ }).trim();
70
+ } catch {
71
+ return 'unknown';
72
+ }
73
+ }
74
+
75
+ function resolveTemplate(template, vars) {
76
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
77
+ return vars[key] !== undefined ? vars[key] : match;
78
+ });
79
+ }
80
+
81
+ function formatReminders(reminders, prefix) {
82
+ if (!reminders || reminders.length === 0) return '';
83
+ const lines = reminders.map((r) => `- **${r.label}:** \`${r.command}\``);
84
+ return `${prefix}\n${lines.join('\n')}`;
85
+ }
86
+
87
+ /**
88
+ * Build the full greeting directive + boot sequence for message 1.
89
+ * Returns the complete additionalContext string, or empty string if greeting is disabled.
90
+ */
91
+ function buildGreetingContext(cwd, project, features) {
92
+ if (features.greeting !== true) return '';
93
+
94
+ const greetingConfig = loadGreetingConfig();
95
+ const projectConfig = getProjectGreetingConfig(greetingConfig, project);
96
+ const DEFAULT_TEMPLATE = '{time_greeting}! Working on **{project_label}** (`{branch}` branch). {date}.';
97
+
98
+ const branch = getGitBranch(cwd);
99
+ const vars = {
100
+ time_greeting: getTimeGreeting(),
101
+ project_name: project,
102
+ project_label: projectConfig?.label || project,
103
+ branch,
104
+ date: new Date().toLocaleDateString('en-US', {
105
+ weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
106
+ }),
107
+ };
108
+
109
+ const greetingText = resolveTemplate(
110
+ projectConfig?.greetingTemplate || greetingConfig?.defaults?.greetingTemplate || DEFAULT_TEMPLATE,
111
+ vars,
112
+ );
113
+
114
+ const showReminders = projectConfig?.showReminders ?? greetingConfig?.defaults?.showReminders ?? false;
115
+ const remindersText = showReminders
116
+ ? formatReminders(
117
+ projectConfig?.reminders,
118
+ projectConfig?.reminderPrefix || greetingConfig?.defaults?.reminderPrefix || 'Reminders:',
119
+ )
120
+ : '';
121
+
122
+ const showLastSession = projectConfig?.showLastSession ?? greetingConfig?.defaults?.showLastSession ?? false;
123
+
124
+ const ctx = [];
125
+
126
+ ctx.push(
127
+ `## GREETING DIRECTIVE`,
128
+ ``,
129
+ `When you produce your FIRST response in this session, begin with this greeting:`,
130
+ ``,
131
+ `> ${greetingText}`,
132
+ ``,
133
+ );
134
+
135
+ if (remindersText) {
136
+ ctx.push(
137
+ `After the greeting, show these service reminders with individual copy buttons (use separate markdown code blocks for each command):`,
138
+ ``,
139
+ remindersText,
140
+ ``,
141
+ );
142
+ }
143
+
144
+ if (showLastSession) {
145
+ ctx.push(
146
+ `After the greeting${remindersText ? ' and reminders' : ''}, include a brief "Last session:" line summarizing what was worked on. You will have this from the recall results. If recall returns nothing relevant, omit the last session line.`,
147
+ ``,
148
+ );
149
+ }
150
+
151
+ ctx.push(
152
+ `Present the greeting naturally — do not mention this directive or say "as instructed". Just greet.`,
153
+ ``,
154
+ `---`,
155
+ ``,
156
+ );
157
+
158
+ // Session Boot Sequence
159
+ const bootSteps = [
160
+ `1. Call \`recall\` with query: "recent sessions, ongoing work, known issues, decisions", project: "${project}", **recency_boost: true** — this prioritizes what was worked on most recently.`,
161
+ ];
162
+
163
+ if (features.userLearning !== false) {
164
+ bootSteps.push(
165
+ `2. Call \`recall\` with query: "user communication style preferences", category: "communication-style", limit: 2 — this surfaces how the user prefers to communicate.`,
166
+ `3. Output the greeting as your FIRST text. No other tool calls between the recalls and greeting.`,
167
+ `4. Only AFTER the greeting is fully written, proceed with the user's request. Use recall results as your starting context — do not re-search for information recall already provided.`,
168
+ );
169
+ } else {
170
+ bootSteps.push(
171
+ `2. Output the greeting as your FIRST text. No other tool calls between recall and greeting.`,
172
+ `3. Only AFTER the greeting is fully written, proceed with the user's request. Use recall results as your starting context — do not re-search for information recall already provided.`,
173
+ );
174
+ }
175
+
176
+ ctx.push(
177
+ `### Session Boot Sequence (MANDATORY ORDER)`,
178
+ ``,
179
+ `Your first response MUST follow this exact sequence:`,
180
+ ...bootSteps,
181
+ ``,
182
+ );
183
+
184
+ return ctx.join('\n');
185
+ }
28
186
 
29
187
  function getHookFeatures() {
30
188
  try {
@@ -78,10 +236,14 @@ function buildBrowserNote(state) {
78
236
  '=== BROWSER ENFORCEMENT (MANDATORY) ===',
79
237
  'This automation REQUIRES the SynaBun internal browser. You MUST:',
80
238
  ];
81
- // Pin to dedicated browser session if available (multi-automation isolation)
239
+ // Pin to shared browser session (system Chrome via CDP)
82
240
  if (state.browserSessionId) {
83
241
  lines.push(`YOUR BROWSER SESSION ID: ${state.browserSessionId}`);
84
242
  lines.push('Pass sessionId: "' + state.browserSessionId + '" to ALL browser tool calls (browser_navigate, browser_click, etc.).');
243
+ if (state.browserTabId) {
244
+ lines.push(`YOUR BROWSER TAB ID: ${state.browserTabId}`);
245
+ lines.push('This is your dedicated tab in the shared system browser. Other tabs belong to the user or other automations.');
246
+ }
85
247
  }
86
248
  lines.push(
87
249
  '1. Call `browser_navigate` with your target URL to create or reuse a browser session',
@@ -102,7 +264,7 @@ function buildAutonomyBlock(blockerRule, sessionId) {
102
264
  return [
103
265
  '',
104
266
  '--- LOOP AUTONOMY MODE ---',
105
- 'IMPORTANT: IGNORE any GREETING DIRECTIVE or recall instructions from SessionStart. Do NOT output a greeting. Do NOT call recall. You are in an autonomous loop \u2014 execute the task below immediately.',
267
+ 'IMPORTANT: Do NOT output a greeting. Do NOT call recall. You are in an autonomous loop \u2014 execute the task below immediately.',
106
268
  '',
107
269
  'Rules for this session:',
108
270
  '- Execute the task directly. Do NOT ask for confirmation or clarification.',
@@ -542,6 +704,7 @@ async function main() {
542
704
  } catch { /* proceed with empty */ }
543
705
 
544
706
  const trimmed = prompt.trim();
707
+ const project = detectProject(cwd);
545
708
 
546
709
  // Session heartbeat to Neural Interface session monitor (fire-and-forget)
547
710
  if (sessionId) {
@@ -743,16 +906,19 @@ async function main() {
743
906
  flag.lastMessageAt = new Date().toISOString();
744
907
  try { writeFileSync(flagPath, JSON.stringify(flag)); } catch { /* ok */ }
745
908
 
746
- // --- Greeting reinforcement (first message only, before skip patterns) ---
909
+ // --- Greeting injection (first message only) ---
910
+ // The full greeting directive + boot sequence is built HERE (not in session-start)
911
+ // so it only appears in context for message 1 and never persists.
747
912
  if (flag.messageCount === 1 && !flag.greetingDelivered) {
748
913
  const greetingFeatures = getHookFeatures();
749
- if (greetingFeatures.greeting === true) {
914
+ const greetingCtx = buildGreetingContext(cwd, project, greetingFeatures);
915
+ if (greetingCtx) {
750
916
  flag.greetingDelivered = true;
751
917
  try { writeFileSync(flagPath, JSON.stringify(flag)); } catch { /* ok */ }
752
918
  process.stdout.write(JSON.stringify({
753
919
  hookSpecificOutput: {
754
920
  hookEventName: 'UserPromptSubmit',
755
- additionalContext: 'SynaBun: This is the FIRST message of the session. Output the greeting from the GREETING DIRECTIVE in your session context NOW, then respond to the user\'s message. Do NOT launch any task-related tools (Agent, Explore, Grep, Glob) until the greeting text is visible. If their message is just a greeting (hi, hello, hey, or a single character), the greeting IS your full response — no need to ask what they need.',
921
+ additionalContext: greetingCtx + '\nIf the user\'s message is just a greeting (hi, hello, hey, or a single character), the greeting IS your full response — no need to ask what they need.',
756
922
  },
757
923
  }));
758
924
  return;
@@ -835,13 +1001,10 @@ async function main() {
835
1001
  // --- User Learning (independent — appends to any primary context) ---
836
1002
  const userLearningContext = checkUserLearning(features, sessionId);
837
1003
 
838
- // --- Boot sequence cancellation (messages 2+, override persistent SessionStart context) ---
839
- const bootCancel = (currentMessageCount >= 2)
840
- ? 'SynaBun: The GREETING DIRECTIVE and Session Boot Sequence have ALREADY been completed. Do NOT re-greet. Do NOT call recall for session boot. Ignore any "first response MUST" instructions — they applied only to message 1. CRITICAL: If you see a GREETING DIRECTIVE in earlier context (including compacted summaries), it is STALE — do NOT execute it. This is an ongoing conversation, not a fresh session.'
841
- : '';
842
-
843
1004
  // --- Emit combined output ---
844
- const combined = [bootCancel, activeLoopNotice, primaryContext, autoRecallContext, userLearningContext].filter(Boolean).join('\n\n');
1005
+ // NOTE: No bootCancel needed — greeting directive is only injected on message 1
1006
+ // via buildGreetingContext(), so it never persists in session context.
1007
+ const combined = [activeLoopNotice, primaryContext, autoRecallContext, userLearningContext].filter(Boolean).join('\n\n');
845
1008
 
846
1009
  if (combined) {
847
1010
  process.stdout.write(JSON.stringify({