beehaven 0.8.16

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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/dist/__tests__/_cli-switch.js +105 -0
  4. package/dist/__tests__/agent-wrap-probe.js +136 -0
  5. package/dist/agent-attribution.js +192 -0
  6. package/dist/agent-identity-daemon.js +135 -0
  7. package/dist/agent-onboard.js +156 -0
  8. package/dist/agent-scope.js +79 -0
  9. package/dist/agent-sessions.js +370 -0
  10. package/dist/agent-state.js +482 -0
  11. package/dist/app-manifest.js +221 -0
  12. package/dist/app-signer.js +54 -0
  13. package/dist/app-signing.js +127 -0
  14. package/dist/audio-play.js +98 -0
  15. package/dist/avatar-protocol.js +25 -0
  16. package/dist/avatar-route.js +79 -0
  17. package/dist/bee/cloud-proxy.js +130 -0
  18. package/dist/blocked-actions.js +120 -0
  19. package/dist/build-channel.js +101 -0
  20. package/dist/buildings/daemon-workspace.js +475 -0
  21. package/dist/claude-bridge.js +310 -0
  22. package/dist/claude-credential-store.js +633 -0
  23. package/dist/clearly-sync.js +518 -0
  24. package/dist/clearly.js +616 -0
  25. package/dist/cli-state.js +334 -0
  26. package/dist/cli.js +6973 -0
  27. package/dist/commands/agent-skills.js +302 -0
  28. package/dist/commands/agent.js +1310 -0
  29. package/dist/commands/app.js +1123 -0
  30. package/dist/commands/auth.js +171 -0
  31. package/dist/commands/building-shell.js +392 -0
  32. package/dist/commands/canvas.js +932 -0
  33. package/dist/commands/claude.js +218 -0
  34. package/dist/commands/compliance.js +387 -0
  35. package/dist/commands/compose.js +507 -0
  36. package/dist/commands/composition.js +95 -0
  37. package/dist/commands/connect-session.js +409 -0
  38. package/dist/commands/conversations.js +160 -0
  39. package/dist/commands/costs.js +443 -0
  40. package/dist/commands/d1.js +253 -0
  41. package/dist/commands/daemon-cmd.js +162 -0
  42. package/dist/commands/deploy.js +164 -0
  43. package/dist/commands/design.js +617 -0
  44. package/dist/commands/dev-generate.js +125 -0
  45. package/dist/commands/dev.js +109 -0
  46. package/dist/commands/devices.js +135 -0
  47. package/dist/commands/explain-router.js +24 -0
  48. package/dist/commands/explain.js +882 -0
  49. package/dist/commands/explore.js +264 -0
  50. package/dist/commands/help.js +2418 -0
  51. package/dist/commands/init.js +408 -0
  52. package/dist/commands/install.js +129 -0
  53. package/dist/commands/library.js +229 -0
  54. package/dist/commands/marketplace.js +138 -0
  55. package/dist/commands/mcp-install.js +476 -0
  56. package/dist/commands/mention.js +257 -0
  57. package/dist/commands/omni.js +529 -0
  58. package/dist/commands/secrets.js +323 -0
  59. package/dist/commands/self-update.js +206 -0
  60. package/dist/commands/sheet.js +264 -0
  61. package/dist/commands/shell-output.js +309 -0
  62. package/dist/commands/shell.js +1048 -0
  63. package/dist/commands/shop.js +368 -0
  64. package/dist/commands/skills.js +311 -0
  65. package/dist/commands/slides.js +418 -0
  66. package/dist/commands/swarm.js +341 -0
  67. package/dist/commands/ticket.js +1301 -0
  68. package/dist/commands/tool.js +312 -0
  69. package/dist/commands/trash.js +106 -0
  70. package/dist/commands/treehouse.js +120 -0
  71. package/dist/commands/user.js +240 -0
  72. package/dist/commands/voice-tts.js +319 -0
  73. package/dist/commands/voice.js +166 -0
  74. package/dist/commands/watch-git.js +401 -0
  75. package/dist/commands/webhook.js +159 -0
  76. package/dist/commands/workspace.js +3354 -0
  77. package/dist/composition-repos.js +91 -0
  78. package/dist/daemon-avatar.js +130 -0
  79. package/dist/daemon-claude-code-auto-install.js +52 -0
  80. package/dist/daemon-client.js +363 -0
  81. package/dist/daemon-codex.js +349 -0
  82. package/dist/daemon-entry.js +101 -0
  83. package/dist/daemon-fetch-allowlist.js +194 -0
  84. package/dist/daemon-gemini.js +224 -0
  85. package/dist/daemon-mcp-clearly.js +353 -0
  86. package/dist/daemon-mcp-sandbox.js +127 -0
  87. package/dist/daemon-omni-company.js +338 -0
  88. package/dist/daemon-omni-hooks.js +375 -0
  89. package/dist/daemon-omni-identity.js +229 -0
  90. package/dist/daemon-omni-pipeline.js +251 -0
  91. package/dist/daemon-omni-policies.js +346 -0
  92. package/dist/daemon-omni-principal.js +580 -0
  93. package/dist/daemon-omni-tool-gate.js +116 -0
  94. package/dist/daemon-omni.js +1442 -0
  95. package/dist/daemon-path-transform.js +120 -0
  96. package/dist/daemon-write-sandbox.js +191 -0
  97. package/dist/daemon.js +4471 -0
  98. package/dist/dashboard.js +846 -0
  99. package/dist/dep-installer.js +286 -0
  100. package/dist/dev-mode.js +67 -0
  101. package/dist/doc-indexer.js +169 -0
  102. package/dist/e2e-crypto.js +94 -0
  103. package/dist/feature-flags.js +154 -0
  104. package/dist/file-browser.js +88 -0
  105. package/dist/first-party-hives.js +211 -0
  106. package/dist/folder-sync.js +226 -0
  107. package/dist/health.js +56 -0
  108. package/dist/ipc-auth.js +299 -0
  109. package/dist/ipc-client.js +495 -0
  110. package/dist/ipc-server.js +684 -0
  111. package/dist/keychain.js +224 -0
  112. package/dist/lib/admin-client.js +73 -0
  113. package/dist/lib/mobile-build-parser.js +156 -0
  114. package/dist/local-app-manager.js +660 -0
  115. package/dist/log-redaction.js +89 -0
  116. package/dist/logger.js +108 -0
  117. package/dist/manifest.js +143 -0
  118. package/dist/marketplace.js +793 -0
  119. package/dist/min-version-copy.js +51 -0
  120. package/dist/min-version.js +214 -0
  121. package/dist/public-surface.js +23 -0
  122. package/dist/relay-presets.js +89 -0
  123. package/dist/relay.js +2723 -0
  124. package/dist/repl.js +619 -0
  125. package/dist/setup-relay.js +363 -0
  126. package/dist/setup-service.js +536 -0
  127. package/dist/shared/bee-memory/index.js +6 -0
  128. package/dist/shared/bee-memory/types.js +682 -0
  129. package/dist/shared/claude-code/daemon-types.js +112 -0
  130. package/dist/shared/claude-code/index.js +2 -0
  131. package/dist/shared/claude-code/types.js +1 -0
  132. package/dist/shared/status/schema.js +133 -0
  133. package/dist/shell-session.js +198 -0
  134. package/dist/speak-lock.js +97 -0
  135. package/dist/spinner.js +57 -0
  136. package/dist/telemetry.js +166 -0
  137. package/dist/terminal/block-parser.js +207 -0
  138. package/dist/terminal/shell-integration.js +108 -0
  139. package/dist/transcript-tailer.js +215 -0
  140. package/dist/tts-qwen3.js +547 -0
  141. package/dist/types.js +4 -0
  142. package/dist/utm.js +38 -0
  143. package/dist/verb-surface.js +331 -0
  144. package/dist/version-utils.js +85 -0
  145. package/dist/watcher.js +207 -0
  146. package/package.json +88 -0
  147. package/python/qwen3_tts.py +230 -0
  148. package/scripts/stamp-build-channel.mjs +115 -0
  149. package/scripts/sync-installed.mjs +159 -0
  150. package/scripts/sync-skills.mjs +112 -0
  151. package/skills/README.md +24 -0
  152. package/skills/agent-doodle-space/SKILL.md +118 -0
  153. package/skills/agent-home/SKILL.md +98 -0
  154. package/skills/beehaven-cli/SKILL.md +144 -0
  155. package/skills/claude-shopper/README.md +90 -0
  156. package/skills/claude-shopper/SKILL.md +151 -0
  157. package/skills/clearly-agent/SKILL.md +183 -0
  158. package/skills/clearly-canvas/SKILL.md +222 -0
  159. package/skills/clearly-canvas/authoring.md +160 -0
  160. package/skills/clearly-canvas/communications.md +32 -0
  161. package/skills/clearly-canvas/composition-patterns.md +84 -0
  162. package/skills/clearly-canvas/example.mjs +78 -0
  163. package/skills/clearly-canvas/project-plans.md +156 -0
  164. package/skills/clearly-canvas/review.mjs +68 -0
  165. package/skills/clearly-workspace/SKILL.md +79 -0
  166. package/skills/clearly-workspace/survey.mjs +101 -0
  167. package/skills/codebase-map/SKILL.md +142 -0
  168. package/skills/design-craft/SKILL.md +300 -0
  169. package/skills/index.json +79 -0
  170. package/skills/layout-systems/SKILL.md +184 -0
  171. package/skills/pair-on-canvas/SKILL.md +92 -0
@@ -0,0 +1,166 @@
1
+ // ============================================================================
2
+ // Beehaven — System Telemetry & Git Status
3
+ // Collects machine metrics and git repo state for remote monitoring.
4
+ // All shell commands use execFileSync (no shell interpolation).
5
+ // ============================================================================
6
+ import { cpus, totalmem, freemem, loadavg, uptime, platform, arch } from 'os';
7
+ import { execFileSync } from 'child_process';
8
+ import { existsSync } from 'fs';
9
+ /** Collect system telemetry using Node.js built-in APIs + minimal shell commands */
10
+ export function collectSystemTelemetry() {
11
+ const totalMem = totalmem();
12
+ const freeMem = freemem();
13
+ const load = loadavg();
14
+ return {
15
+ cpu: {
16
+ cores: cpus().length,
17
+ loadAvg1m: +load[0].toFixed(2),
18
+ },
19
+ memory: {
20
+ totalGB: +(totalMem / 1073741824).toFixed(1),
21
+ freeGB: +(freeMem / 1073741824).toFixed(1),
22
+ usedPct: +((1 - freeMem / totalMem) * 100).toFixed(1),
23
+ },
24
+ disk: collectDiskUsage(),
25
+ uptime: Math.floor(uptime()),
26
+ platform: platform(),
27
+ arch: arch(),
28
+ };
29
+ }
30
+ /** Get disk usage for the home directory via `df` */
31
+ function collectDiskUsage() {
32
+ try {
33
+ const output = execFileSync('df', ['-k', '.'], {
34
+ timeout: 5000,
35
+ encoding: 'utf8',
36
+ stdio: ['pipe', 'pipe', 'pipe'],
37
+ });
38
+ const lines = output.trim().split('\n');
39
+ if (lines.length < 2)
40
+ return null;
41
+ // df output: Filesystem 1K-blocks Used Available Use% Mounted
42
+ const parts = lines[1].split(/\s+/);
43
+ const totalKB = parseInt(parts[1], 10);
44
+ const usedKB = parseInt(parts[2], 10);
45
+ const availKB = parseInt(parts[3], 10);
46
+ if (isNaN(totalKB))
47
+ return null;
48
+ return {
49
+ totalGB: +(totalKB / 1048576).toFixed(1),
50
+ freeGB: +(availKB / 1048576).toFixed(1),
51
+ usedPct: +((usedKB / totalKB) * 100).toFixed(1),
52
+ };
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ /** Collect git status for a working directory */
59
+ export function collectGitStatus(cwd) {
60
+ if (!existsSync(cwd))
61
+ return null;
62
+ try {
63
+ // Check if it's a git repo
64
+ execFileSync('git', ['rev-parse', '--git-dir'], {
65
+ cwd,
66
+ timeout: 3000,
67
+ encoding: 'utf8',
68
+ stdio: ['pipe', 'pipe', 'pipe'],
69
+ });
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ try {
75
+ // Branch name
76
+ let branch = 'HEAD';
77
+ try {
78
+ branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
79
+ cwd,
80
+ timeout: 3000,
81
+ encoding: 'utf8',
82
+ stdio: ['pipe', 'pipe', 'pipe'],
83
+ }).trim();
84
+ }
85
+ catch {
86
+ /* detached HEAD */
87
+ }
88
+ // Ahead/behind
89
+ let ahead = 0;
90
+ let behind = 0;
91
+ try {
92
+ const abOutput = execFileSync('git', ['rev-list', '--left-right', '--count', `HEAD...@{upstream}`], {
93
+ cwd,
94
+ timeout: 3000,
95
+ encoding: 'utf8',
96
+ stdio: ['pipe', 'pipe', 'pipe'],
97
+ }).trim();
98
+ const [a, b] = abOutput.split(/\s+/);
99
+ ahead = parseInt(a, 10) || 0;
100
+ behind = parseInt(b, 10) || 0;
101
+ }
102
+ catch {
103
+ /* no upstream */
104
+ }
105
+ // Status counts
106
+ let staged = 0;
107
+ let modified = 0;
108
+ let untracked = 0;
109
+ try {
110
+ const status = execFileSync('git', ['status', '--porcelain', '-uall'], {
111
+ cwd,
112
+ timeout: 5000,
113
+ encoding: 'utf8',
114
+ stdio: ['pipe', 'pipe', 'pipe'],
115
+ });
116
+ for (const line of status.split('\n')) {
117
+ if (!line)
118
+ continue;
119
+ const x = line[0];
120
+ const y = line[1];
121
+ if (x === '?' && y === '?')
122
+ untracked++;
123
+ else if (x !== ' ' && x !== '?')
124
+ staged++;
125
+ if (y !== ' ' && y !== '?')
126
+ modified++;
127
+ }
128
+ }
129
+ catch {
130
+ /* empty repo */
131
+ }
132
+ // Last commit
133
+ let lastCommitHash = '';
134
+ let lastCommitMsg = '';
135
+ let lastCommitAge = 0;
136
+ try {
137
+ const log = execFileSync('git', ['log', '-1', '--format=%H%n%s%n%ct'], {
138
+ cwd,
139
+ timeout: 3000,
140
+ encoding: 'utf8',
141
+ stdio: ['pipe', 'pipe', 'pipe'],
142
+ }).trim();
143
+ const [hash, msg, timestamp] = log.split('\n');
144
+ lastCommitHash = hash?.slice(0, 8) || '';
145
+ lastCommitMsg = msg?.slice(0, 100) || '';
146
+ lastCommitAge = Math.floor(Date.now() / 1000) - (parseInt(timestamp, 10) || 0);
147
+ }
148
+ catch {
149
+ /* no commits */
150
+ }
151
+ return {
152
+ branch,
153
+ ahead,
154
+ behind,
155
+ staged,
156
+ modified,
157
+ untracked,
158
+ lastCommitHash,
159
+ lastCommitMsg,
160
+ lastCommitAge,
161
+ };
162
+ }
163
+ catch {
164
+ return null;
165
+ }
166
+ }
@@ -0,0 +1,207 @@
1
+ // ============================================================================
2
+ // TerminalBlockParser — raw PTY byte stream → command "blocks"
3
+ // ----------------------------------------------------------------------------
4
+ // Turns a shell's PTY output into structured command blocks using OSC 633 / 133
5
+ // semantic prompt markers (the protocol Warp / iTerm2 / VS Code use). It is fed
6
+ // the SAME bytes that stream to xterm as `pty-output`; it never consumes or
7
+ // mutates them, so the live terminal is completely unaffected. On each finished
8
+ // command (a `D` marker) it emits a structured block to mint as a canvas frame.
9
+ //
10
+ // Markers (emitted by our injected shell integration — see shell-integration.ts):
11
+ // OSC 633;A prompt start
12
+ // OSC 633;B prompt end (user input begins)
13
+ // OSC 633;C output start (pre-exec)
14
+ // OSC 633;E;<cmdline> explicit command line (survives ; and newlines)
15
+ // OSC 633;D;<exit> command finished + exit code
16
+ // OSC 7;file://host/pwd cwd report
17
+ // OSC 133;A/B/C/D are accepted as a fallback for shells that already ship their
18
+ // own integration. Everything else in the stream (colors, cursor moves, foreign
19
+ // OSC) is treated as opaque output content.
20
+ // ============================================================================
21
+ const ESC = '\x1b';
22
+ const BEL = '\x07';
23
+ /** Strip the common ANSI escape families so block output renders as clean text. */
24
+ export function stripAnsi(s) {
25
+ return s
26
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '') // CSI (colors, cursor)
27
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '') // OSC (BEL- or ST-terminated)
28
+ .replace(/\x1b[()][0-9A-Za-z]/g, '') // charset select
29
+ .replace(/\x1b[=>]/g, '') // keypad mode
30
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, ''); // stray control bytes (keep \t \n \r)
31
+ }
32
+ export class TerminalBlockParser {
33
+ onBlock;
34
+ now;
35
+ buf = '';
36
+ phase = 'idle';
37
+ cwd = '';
38
+ altScreen = false;
39
+ cur = null;
40
+ /**
41
+ * @param onBlock called once per completed command block
42
+ * @param now injectable clock (ms) — override in tests
43
+ */
44
+ constructor(onBlock, now = () => Date.now()) {
45
+ this.onBlock = onBlock;
46
+ this.now = now;
47
+ }
48
+ /** Feed a chunk of raw PTY output. Safe to call with partial escape sequences. */
49
+ write(chunk) {
50
+ if (!chunk)
51
+ return;
52
+ this.buf += chunk;
53
+ this.drain();
54
+ }
55
+ drain() {
56
+ const buf = this.buf;
57
+ let i = 0;
58
+ let text = ''; // plain/opaque bytes accumulated since the last flush
59
+ const flush = () => {
60
+ if (text && this.cur && this.phase === 'running' && !this.altScreen) {
61
+ this.cur.raw += text;
62
+ }
63
+ text = '';
64
+ };
65
+ while (i < buf.length) {
66
+ const esc = buf.indexOf(ESC, i);
67
+ if (esc === -1) {
68
+ text += buf.slice(i);
69
+ break;
70
+ }
71
+ text += buf.slice(i, esc);
72
+ // Commit pending output to the open block BEFORE any marker changes state
73
+ // (a D marker closes the block and reads its raw output right here).
74
+ flush();
75
+ const next = buf[esc + 1];
76
+ if (next === undefined) {
77
+ // Lone trailing ESC — could start any sequence; wait for more bytes.
78
+ this.buf = buf.slice(esc);
79
+ return;
80
+ }
81
+ if (next === ']') {
82
+ // OSC — terminated by BEL or ST (ESC \).
83
+ const bel = buf.indexOf(BEL, esc + 2);
84
+ const st = buf.indexOf(ESC + '\\', esc + 2);
85
+ let end = -1;
86
+ let stLen = 1;
87
+ if (bel !== -1 && (st === -1 || bel < st)) {
88
+ end = bel;
89
+ stLen = 1;
90
+ }
91
+ else if (st !== -1) {
92
+ end = st;
93
+ stLen = 2;
94
+ }
95
+ if (end === -1) {
96
+ // Incomplete OSC — buffer from ESC and wait for the rest.
97
+ this.buf = buf.slice(esc);
98
+ return;
99
+ }
100
+ const body = buf.slice(esc + 2, end);
101
+ if (!this.handleOsc(body)) {
102
+ // Foreign OSC — keep it verbatim in the raw output.
103
+ text += buf.slice(esc, end + stLen);
104
+ }
105
+ i = end + stLen;
106
+ continue;
107
+ }
108
+ if (next === '[') {
109
+ // CSI — special-case the alt-screen toggles (TUI apps own the grid),
110
+ // otherwise it's opaque output; pass the ESC byte through and rescan.
111
+ if (buf.startsWith('[?1049h', esc + 1)) {
112
+ this.altScreen = true;
113
+ if (this.cur)
114
+ this.cur.touchedAlt = true;
115
+ i = esc + 8;
116
+ continue;
117
+ }
118
+ if (buf.startsWith('[?1049l', esc + 1)) {
119
+ this.altScreen = false;
120
+ i = esc + 8;
121
+ continue;
122
+ }
123
+ text += ESC;
124
+ i = esc + 1;
125
+ continue;
126
+ }
127
+ // Any other escape — opaque; pass the ESC byte through.
128
+ text += ESC;
129
+ i = esc + 1;
130
+ }
131
+ flush();
132
+ this.buf = '';
133
+ }
134
+ /** Handle one OSC body. Returns true if it was one of OUR markers. */
135
+ handleOsc(body) {
136
+ // OSC 7 — cwd report: "7;file://host/path"
137
+ if (body.startsWith('7;')) {
138
+ const m = body.slice(2).match(/^file:\/\/[^/]*(\/.*)$/);
139
+ if (m) {
140
+ try {
141
+ this.cwd = decodeURIComponent(m[1]);
142
+ }
143
+ catch {
144
+ this.cwd = m[1];
145
+ }
146
+ }
147
+ return true;
148
+ }
149
+ // OSC 633 (VS Code) / 133 (FinalTerm) semantic prompt markers.
150
+ let rest = null;
151
+ if (body.startsWith('633;'))
152
+ rest = body.slice(4);
153
+ else if (body.startsWith('133;'))
154
+ rest = body.slice(4);
155
+ if (rest === null)
156
+ return false;
157
+ const code = rest[0];
158
+ // Everything after "X;" is the argument (may itself contain ';' or newlines).
159
+ const arg = rest.length > 2 && rest[1] === ';' ? rest.slice(2) : '';
160
+ switch (code) {
161
+ case 'A': // prompt start
162
+ this.phase = 'idle';
163
+ return true;
164
+ case 'B': // prompt end — user input begins
165
+ this.phase = 'input';
166
+ return true;
167
+ case 'C': // output start — open a block
168
+ this.cur = { command: '', raw: '', startedAt: this.now(), touchedAlt: false };
169
+ this.phase = 'running';
170
+ return true;
171
+ case 'E': // explicit command line
172
+ if (this.cur)
173
+ this.cur.command = arg;
174
+ return true;
175
+ case 'D': {
176
+ // command finished
177
+ if (this.cur) {
178
+ const raw = this.cur.raw;
179
+ const block = {
180
+ command: this.cur.command.trim(),
181
+ cwd: this.cwd,
182
+ exitCode: arg ? parseInt(arg, 10) || 0 : 0,
183
+ output: stripAnsi(raw).replace(/[ \t\r\n]+$/, ''),
184
+ raw,
185
+ startedAt: this.cur.startedAt,
186
+ durationMs: Math.max(0, this.now() - this.cur.startedAt),
187
+ };
188
+ const touchedAlt = this.cur.touchedAlt;
189
+ this.cur = null;
190
+ this.phase = 'idle';
191
+ // Suppress blocks for alt-screen apps (vim/htop/interactive claude) —
192
+ // the live xterm viewport owns those; there is no linear output to block-ify.
193
+ if (!touchedAlt) {
194
+ try {
195
+ this.onBlock(block);
196
+ }
197
+ catch {
198
+ /* never break the stream */
199
+ }
200
+ }
201
+ }
202
+ return true;
203
+ }
204
+ }
205
+ return true; // recognized prefix, unknown sub-code — swallow
206
+ }
207
+ }
@@ -0,0 +1,108 @@
1
+ // ============================================================================
2
+ // Shell integration — inject OSC 633 semantic prompt markers into a spawned
3
+ // shell so TerminalBlockParser can slice the PTY stream into command blocks.
4
+ // ----------------------------------------------------------------------------
5
+ // We spawn the shell ourselves, so we own its startup: point it at a temp rc
6
+ // that FIRST sources the user's real rc (aliases, PATH, prompt, everything
7
+ // unchanged) and THEN installs precmd/preexec hooks emitting the markers.
8
+ // • zsh → ZDOTDIR redirection (the VS Code / iTerm2 approach); we restore the
9
+ // real ZDOTDIR inside so subshells + completion are unaffected.
10
+ // • bash → --rcfile; a DEBUG trap (preexec) + PROMPT_COMMAND (precmd).
11
+ // • anything else (sh, …) → no integration; the live terminal still works,
12
+ // blocks just aren't parsed.
13
+ // Emitted markers: 633;A prompt-start · 633;B prompt-end · 633;C output-start ·
14
+ // 633;E;<cmd> command line · 633;D;<exit> done · OSC 7 cwd.
15
+ //
16
+ // NOTE: these scripts are built with plain concatenation (not template literals)
17
+ // on purpose — a `${...}` in a JS template is interpolation, and shell syntax is
18
+ // full of `${VAR:-default}`. Backslashes are doubled so the FILE contains literal
19
+ // `\e`/`\a` for the shell's printf to turn into ESC/BEL.
20
+ // ============================================================================
21
+ import { mkdirSync, writeFileSync, rmSync } from 'fs';
22
+ import { join } from 'path';
23
+ import { tmpdir, homedir } from 'os';
24
+ const ROOT = join(tmpdir(), 'beehaven-term');
25
+ // zsh hooks. `precmd_functions` is PREPENDED (not add-zsh-hook) so _bh_precmd
26
+ // captures the real $? before any user precmd hook can clobber it.
27
+ const ZSH_HOOKS = [
28
+ 'autoload -Uz add-zsh-hook 2>/dev/null',
29
+ "_bh_preexec() { printf '\\e]633;C\\a'; printf '\\e]633;E;%s\\a' \"$1\"; }",
30
+ '_bh_precmd() { printf \'\\e]633;D;%s\\a\' "$?"; printf \'\\e]7;file://%s%s\\a\' "${HOST:-localhost}" "$PWD"; printf \'\\e]633;A\\a\'; }',
31
+ 'typeset -ga precmd_functions preexec_functions',
32
+ 'precmd_functions=(_bh_precmd ${precmd_functions:#_bh_precmd})',
33
+ 'preexec_functions=(${preexec_functions:#_bh_preexec} _bh_preexec)',
34
+ 'PS1="$PS1"$\'\\e]633;B\\a\'',
35
+ '',
36
+ ].join('\n');
37
+ function bashRc(home) {
38
+ return [
39
+ '[ -f "' + home + '/.bashrc" ] && source "' + home + '/.bashrc"',
40
+ '_bh_last_cmd=""',
41
+ '_bh_preexec() {',
42
+ ' [ -n "$COMP_LINE" ] && return', // skip during completion
43
+ ' [ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return',
44
+ ' [ "$BASH_COMMAND" = "$_bh_last_cmd" ] && return', // DEBUG fires per simple-command; dedupe
45
+ ' _bh_last_cmd="$BASH_COMMAND"',
46
+ " printf '\\e]633;C\\a'; printf '\\e]633;E;%s\\a' \"$BASH_COMMAND\"",
47
+ '}',
48
+ '_bh_precmd() {',
49
+ ' local ec=$?',
50
+ ' _bh_last_cmd=""',
51
+ ' printf \'\\e]633;D;%s\\a\' "$ec"',
52
+ ' printf \'\\e]7;file://%s%s\\a\' "${HOSTNAME:-localhost}" "$PWD"',
53
+ " printf '\\e]633;A\\a'",
54
+ '}',
55
+ "trap '_bh_preexec' DEBUG",
56
+ 'PROMPT_COMMAND="_bh_precmd${PROMPT_COMMAND:+; $PROMPT_COMMAND}"',
57
+ 'PS1="$PS1"$\'\\e]633;B\\a\'',
58
+ '',
59
+ ].join('\n');
60
+ }
61
+ /** Detect the shell family from an absolute shell path. */
62
+ function shellKind(shell) {
63
+ if (/zsh$/.test(shell))
64
+ return 'zsh';
65
+ if (/bash$/.test(shell))
66
+ return 'bash';
67
+ return 'other';
68
+ }
69
+ /**
70
+ * Materialize the integration for a session. Returns `{ env, args }` to merge
71
+ * into the PTY spawn, plus the temp `dir` to clean up on exit. Never throws — on
72
+ * any failure returns a no-op so the shell still spawns (without block parsing).
73
+ */
74
+ export function writeIntegration(sessionId, shell) {
75
+ const kind = shellKind(shell);
76
+ const noop = { env: {}, args: [], dir: null };
77
+ if (kind === 'other')
78
+ return noop;
79
+ try {
80
+ const dir = join(ROOT, sessionId);
81
+ mkdirSync(dir, { recursive: true });
82
+ if (kind === 'zsh') {
83
+ const real = process.env.ZDOTDIR || homedir();
84
+ const src = (name) => '[ -f "' + real + '/' + name + '" ] && source "' + real + '/' + name + '"\n';
85
+ writeFileSync(join(dir, '.zshenv'), src('.zshenv'));
86
+ writeFileSync(join(dir, '.zprofile'), src('.zprofile'));
87
+ writeFileSync(join(dir, '.zlogin'), src('.zlogin'));
88
+ writeFileSync(join(dir, '.zshrc'), 'export ZDOTDIR="' + real + '"\n' + src('.zshrc') + ZSH_HOOKS);
89
+ return { env: { ZDOTDIR: dir, BH_USER_ZDOTDIR: real }, args: [], dir };
90
+ }
91
+ // bash
92
+ const rc = join(dir, 'bashrc');
93
+ writeFileSync(rc, bashRc(homedir()));
94
+ return { env: {}, args: ['--rcfile', rc, '-i'], dir };
95
+ }
96
+ catch {
97
+ return noop;
98
+ }
99
+ }
100
+ /** Remove a session's integration temp dir. Safe to call more than once. */
101
+ export function cleanupIntegration(sessionId) {
102
+ try {
103
+ rmSync(join(ROOT, sessionId), { recursive: true, force: true });
104
+ }
105
+ catch {
106
+ /* best-effort */
107
+ }
108
+ }