@clearlysh/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.
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- 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
|
+
}
|