@bubblebrain-ai/bubble 0.0.18 → 0.0.20

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 (58) hide show
  1. package/dist/agent/internal-reminder-sanitizer.d.ts +1 -0
  2. package/dist/agent/internal-reminder-sanitizer.js +46 -0
  3. package/dist/agent.d.ts +9 -0
  4. package/dist/agent.js +305 -17
  5. package/dist/approval/controller.d.ts +6 -0
  6. package/dist/approval/controller.js +104 -11
  7. package/dist/debug-trace.js +4 -0
  8. package/dist/feishu/agent-host/run-driver.js +28 -0
  9. package/dist/hooks/config.d.ts +9 -0
  10. package/dist/hooks/config.js +278 -0
  11. package/dist/hooks/controller.d.ts +24 -0
  12. package/dist/hooks/controller.js +254 -0
  13. package/dist/hooks/index.d.ts +6 -0
  14. package/dist/hooks/index.js +4 -0
  15. package/dist/hooks/log.d.ts +14 -0
  16. package/dist/hooks/log.js +54 -0
  17. package/dist/hooks/runner.d.ts +5 -0
  18. package/dist/hooks/runner.js +225 -0
  19. package/dist/hooks/trust.d.ts +37 -0
  20. package/dist/hooks/trust.js +143 -0
  21. package/dist/hooks/types.d.ts +173 -0
  22. package/dist/hooks/types.js +46 -0
  23. package/dist/main.js +32 -0
  24. package/dist/memory/prompts.js +3 -1
  25. package/dist/model-catalog.js +2 -0
  26. package/dist/model-pricing.js +8 -0
  27. package/dist/network/chatgpt-transport.js +34 -9
  28. package/dist/network/provider-transport.d.ts +32 -0
  29. package/dist/network/provider-transport.js +265 -0
  30. package/dist/network/retry.d.ts +29 -0
  31. package/dist/network/retry.js +88 -0
  32. package/dist/network/system-proxy.d.ts +18 -0
  33. package/dist/network/system-proxy.js +175 -0
  34. package/dist/provider-anthropic.d.ts +1 -0
  35. package/dist/provider-anthropic.js +127 -52
  36. package/dist/provider-openai-codex.js +19 -29
  37. package/dist/session-log.js +3 -3
  38. package/dist/slash-commands/commands.js +84 -0
  39. package/dist/slash-commands/types.d.ts +2 -0
  40. package/dist/tools/edit-apply.js +63 -3
  41. package/dist/tools/edit.js +4 -4
  42. package/dist/tui/display-history.d.ts +4 -3
  43. package/dist/tui/display-history.js +34 -57
  44. package/dist/tui/display-sanitizer.d.ts +3 -0
  45. package/dist/tui/display-sanitizer.js +38 -0
  46. package/dist/tui/paste-placeholder.d.ts +1 -0
  47. package/dist/tui/paste-placeholder.js +7 -0
  48. package/dist/tui/run.d.ts +2 -0
  49. package/dist/tui/run.js +260 -155
  50. package/dist/tui/trace-groups.js +40 -4
  51. package/dist/tui/wordmark.d.ts +1 -0
  52. package/dist/tui/wordmark.js +56 -54
  53. package/dist/tui-ink/app.js +2 -1
  54. package/dist/tui-ink/trace-groups.js +40 -4
  55. package/dist/tui-opentui/app.js +2 -1
  56. package/dist/tui-opentui/trace-groups.js +40 -4
  57. package/dist/types.d.ts +27 -0
  58. package/package.json +1 -1
@@ -0,0 +1,254 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { explainHookEvent, formatHooksStatus, getProjectHookFingerprint, loadHookConfig, } from "./config.js";
3
+ import { appendHookLog, readRecentHookLogs } from "./log.js";
4
+ import { runHookCommand } from "./runner.js";
5
+ import { trustProjectHooks, untrustProjectHooks } from "./trust.js";
6
+ import { BLOCKABLE_HOOK_EVENTS, } from "./types.js";
7
+ export class ExternalHookController {
8
+ options;
9
+ config;
10
+ disabledByDepth;
11
+ constructor(options) {
12
+ this.options = options;
13
+ this.config = loadHookConfig(options);
14
+ this.disabledByDepth = process.env.BUBBLE_HOOK_DEPTH === "1";
15
+ }
16
+ reload() {
17
+ this.config = loadHookConfig(this.options);
18
+ return this.config;
19
+ }
20
+ getConfig() {
21
+ return this.config;
22
+ }
23
+ status() {
24
+ return formatHooksStatus(this.config);
25
+ }
26
+ explain(eventName) {
27
+ return explainHookEvent(eventName, this.config);
28
+ }
29
+ logs(limit = 20) {
30
+ const entries = readRecentHookLogs(limit, this.options);
31
+ if (entries.length === 0)
32
+ return "No hook logs yet.";
33
+ return entries.map((entry) => {
34
+ const hook = entry.hookId ? ` ${entry.hookId}` : "";
35
+ const event = entry.eventName ? ` ${entry.eventName}` : "";
36
+ const decision = entry.decision ? ` ${entry.decision}` : "";
37
+ return `${entry.ts} [${entry.level}]${event}${hook}${decision} - ${entry.message}`;
38
+ }).join("\n");
39
+ }
40
+ trustProject() {
41
+ const fingerprint = getProjectHookFingerprint(this.options);
42
+ if (!fingerprint)
43
+ return "No project hooks are configured.";
44
+ trustProjectHooks(fingerprint, this.options);
45
+ this.reload();
46
+ return `Trusted project hooks for fingerprint ${fingerprint.fingerprint.slice(0, 12)}.`;
47
+ }
48
+ untrustProject() {
49
+ const key = this.config.projectTrust.projectKey;
50
+ if (!key)
51
+ return "No project hooks are configured.";
52
+ const changed = untrustProjectHooks(key, this.options);
53
+ this.reload();
54
+ return changed ? "Untrusted project hooks for this project." : "Project hooks were not trusted.";
55
+ }
56
+ async test(eventName, target) {
57
+ const result = await this.runEvent({
58
+ eventName,
59
+ cwd: this.options.cwd,
60
+ sessionId: this.options.sessionId,
61
+ agentRole: "driver",
62
+ target,
63
+ payload: {
64
+ test: true,
65
+ target,
66
+ },
67
+ });
68
+ return formatCombinedResult(result);
69
+ }
70
+ async runEvent(request, runOptions = {}) {
71
+ if (this.disabledByDepth) {
72
+ return emptyResult(request.eventName, ["Hooks disabled because BUBBLE_HOOK_DEPTH=1."]);
73
+ }
74
+ const agentRole = request.agentRole ?? "parent";
75
+ const candidates = this.config.rules.filter((rule) => {
76
+ if (!rule.enabled || !rule.trusted)
77
+ return false;
78
+ if (!rule.events.includes(request.eventName))
79
+ return false;
80
+ if (agentRole === "subagent" && !rule.inheritToSubagents)
81
+ return false;
82
+ return matchesRule(rule, request);
83
+ });
84
+ const diagnostics = [];
85
+ const results = [];
86
+ const modelContext = [];
87
+ const blockable = BLOCKABLE_HOOK_EVENTS.has(request.eventName);
88
+ let decision = "allow";
89
+ let reason;
90
+ let sourceHookId;
91
+ let source;
92
+ for (const rule of candidates) {
93
+ const start = {
94
+ type: "hook_start",
95
+ eventName: request.eventName,
96
+ hookId: rule.id,
97
+ source: rule.source,
98
+ };
99
+ runOptions.onProgress?.(start);
100
+ appendHookLog({
101
+ ts: new Date().toISOString(),
102
+ level: "info",
103
+ eventName: request.eventName,
104
+ hookId: rule.id,
105
+ message: "Hook started.",
106
+ }, this.options);
107
+ const envelope = buildEnvelope(rule, {
108
+ ...request,
109
+ sessionId: request.sessionId ?? this.options.sessionId,
110
+ agentRole,
111
+ });
112
+ const runnerResult = await runHookCommand(rule, envelope, { abortSignal: runOptions.abortSignal });
113
+ const single = {
114
+ hookId: rule.id,
115
+ eventName: request.eventName,
116
+ source: rule.source,
117
+ decision: runnerResult.decision,
118
+ reason: runnerResult.reason,
119
+ modelContext: runnerResult.modelContext,
120
+ exitCode: runnerResult.exitCode,
121
+ signal: runnerResult.signal,
122
+ elapsedMs: runnerResult.elapsedMs,
123
+ stdout: runnerResult.stdout,
124
+ stderr: runnerResult.stderr,
125
+ truncated: runnerResult.truncated,
126
+ error: runnerResult.error,
127
+ };
128
+ results.push(single);
129
+ modelContext.push(...runnerResult.modelContext);
130
+ if (runnerResult.error) {
131
+ const event = {
132
+ type: "hook_error",
133
+ eventName: request.eventName,
134
+ hookId: rule.id,
135
+ source: rule.source,
136
+ elapsedMs: runnerResult.elapsedMs,
137
+ decision: runnerResult.decision,
138
+ reason: runnerResult.reason,
139
+ error: runnerResult.error,
140
+ };
141
+ runOptions.onProgress?.(event);
142
+ appendHookLog({
143
+ ts: new Date().toISOString(),
144
+ level: runnerResult.decision === "deny" ? "error" : "warn",
145
+ eventName: request.eventName,
146
+ hookId: rule.id,
147
+ decision: runnerResult.decision,
148
+ message: runnerResult.error,
149
+ }, this.options);
150
+ }
151
+ else {
152
+ const event = {
153
+ type: "hook_end",
154
+ eventName: request.eventName,
155
+ hookId: rule.id,
156
+ source: rule.source,
157
+ elapsedMs: runnerResult.elapsedMs,
158
+ decision: runnerResult.decision,
159
+ reason: runnerResult.reason,
160
+ };
161
+ runOptions.onProgress?.(event);
162
+ appendHookLog({
163
+ ts: new Date().toISOString(),
164
+ level: "info",
165
+ eventName: request.eventName,
166
+ hookId: rule.id,
167
+ decision: runnerResult.decision,
168
+ message: runnerResult.reason ?? "Hook completed.",
169
+ }, this.options);
170
+ }
171
+ if (runnerResult.decision === "deny" && !blockable) {
172
+ diagnostics.push(`${rule.id} returned deny for observe-only event ${request.eventName}; denial ignored.`);
173
+ continue;
174
+ }
175
+ if (runnerResult.decision === "deny") {
176
+ decision = "deny";
177
+ reason = runnerResult.reason ?? `Denied by hook ${rule.id}.`;
178
+ sourceHookId = rule.id;
179
+ source = rule.source;
180
+ break;
181
+ }
182
+ }
183
+ return {
184
+ eventName: request.eventName,
185
+ decision,
186
+ reason,
187
+ sourceHookId,
188
+ source,
189
+ modelContext,
190
+ results,
191
+ diagnostics,
192
+ matched: candidates.length,
193
+ };
194
+ }
195
+ }
196
+ function matchesRule(rule, request) {
197
+ if (!rule.matcher)
198
+ return true;
199
+ const target = request.target ?? JSON.stringify(request.payload ?? {});
200
+ return new RegExp(rule.matcher).test(target);
201
+ }
202
+ function buildEnvelope(rule, request) {
203
+ const payload = { ...(request.payload ?? {}) };
204
+ const redacted = [];
205
+ const full = request.fullPayload ?? {};
206
+ for (const [key, value] of Object.entries(full)) {
207
+ if (rule.include.includes("all") || rule.include.includes(key)) {
208
+ payload[key] = value;
209
+ }
210
+ else {
211
+ redacted.push(key);
212
+ }
213
+ }
214
+ return {
215
+ schemaVersion: 1,
216
+ eventName: request.eventName,
217
+ eventId: randomUUID(),
218
+ timestamp: new Date().toISOString(),
219
+ cwd: request.cwd,
220
+ sessionId: request.sessionId,
221
+ runId: request.runId,
222
+ agentRole: request.agentRole,
223
+ subAgentId: request.subAgentId,
224
+ target: request.target,
225
+ payload,
226
+ redacted,
227
+ };
228
+ }
229
+ function emptyResult(eventName, diagnostics = []) {
230
+ return {
231
+ eventName,
232
+ decision: "allow",
233
+ modelContext: [],
234
+ results: [],
235
+ diagnostics,
236
+ matched: 0,
237
+ };
238
+ }
239
+ function formatCombinedResult(result) {
240
+ const lines = [
241
+ `Hook test ${result.eventName}: ${result.decision}${result.reason ? ` - ${result.reason}` : ""}`,
242
+ `Matched: ${result.matched}`,
243
+ ];
244
+ for (const item of result.results) {
245
+ const suffix = item.reason ? ` - ${item.reason}` : item.error ? ` - ${item.error}` : "";
246
+ lines.push(` ${item.decision} ${item.hookId} (${item.elapsedMs}ms)${suffix}`);
247
+ }
248
+ if (result.diagnostics.length) {
249
+ lines.push("Diagnostics:");
250
+ for (const diagnostic of result.diagnostics)
251
+ lines.push(` ${diagnostic}`);
252
+ }
253
+ return lines.join("\n");
254
+ }
@@ -0,0 +1,6 @@
1
+ export { ExternalHookController } from "./controller.js";
2
+ export type { ExternalHookControllerOptions, HookRunOptions } from "./controller.js";
3
+ export { loadHookConfig, formatHooksStatus, explainHookEvent } from "./config.js";
4
+ export { runHookCommand } from "./runner.js";
5
+ export { HOOK_EVENT_NAMES, BLOCKABLE_HOOK_EVENTS, isHookEventName, normalizeHookInput, truncateHookText, } from "./types.js";
6
+ export type { HookAgentRole, HookCombinedResult, HookDecision, HookEventName, HookProgressEvent, HookRunRequest, } from "./types.js";
@@ -0,0 +1,4 @@
1
+ export { ExternalHookController } from "./controller.js";
2
+ export { loadHookConfig, formatHooksStatus, explainHookEvent } from "./config.js";
3
+ export { runHookCommand } from "./runner.js";
4
+ export { HOOK_EVENT_NAMES, BLOCKABLE_HOOK_EVENTS, isHookEventName, normalizeHookInput, truncateHookText, } from "./types.js";
@@ -0,0 +1,14 @@
1
+ export interface HookLogEntry {
2
+ ts: string;
3
+ level: "info" | "warn" | "error";
4
+ eventName?: string;
5
+ hookId?: string;
6
+ decision?: string;
7
+ message: string;
8
+ detail?: Record<string, unknown>;
9
+ }
10
+ export interface HookLogOptions {
11
+ bubbleHome?: string;
12
+ }
13
+ export declare function appendHookLog(entry: HookLogEntry, options?: HookLogOptions): void;
14
+ export declare function readRecentHookLogs(limit?: number, options?: HookLogOptions): HookLogEntry[];
@@ -0,0 +1,54 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getBubbleHome } from "../bubble-home.js";
4
+ const MAX_RECENT_LOG_BYTES = 512 * 1024;
5
+ export function appendHookLog(entry, options = {}) {
6
+ const bubbleHome = options.bubbleHome ?? getBubbleHome();
7
+ const dir = join(bubbleHome, "hooks");
8
+ try {
9
+ mkdirSync(dir, { recursive: true });
10
+ const file = join(dir, `${entry.ts.slice(0, 10)}.jsonl`);
11
+ appendFileSync(file, JSON.stringify(entry) + "\n", "utf-8");
12
+ }
13
+ catch {
14
+ // Hook logging must never affect the agent run.
15
+ }
16
+ }
17
+ export function readRecentHookLogs(limit = 30, options = {}) {
18
+ const bubbleHome = options.bubbleHome ?? getBubbleHome();
19
+ const dir = join(bubbleHome, "hooks");
20
+ if (!existsSync(dir))
21
+ return [];
22
+ try {
23
+ const files = readdirSync(dir)
24
+ .filter((file) => file.endsWith(".jsonl"))
25
+ .sort()
26
+ .reverse();
27
+ const lines = [];
28
+ for (const file of files) {
29
+ const path = join(dir, file);
30
+ const size = statSync(path).size;
31
+ const text = readFileSync(path, "utf-8");
32
+ const chunk = size > MAX_RECENT_LOG_BYTES
33
+ ? text.slice(Math.max(0, text.length - MAX_RECENT_LOG_BYTES))
34
+ : text;
35
+ lines.unshift(...chunk.trimEnd().split("\n").filter(Boolean));
36
+ if (lines.length >= limit)
37
+ break;
38
+ }
39
+ return lines
40
+ .slice(-limit)
41
+ .map((line) => {
42
+ try {
43
+ return JSON.parse(line);
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ })
49
+ .filter((entry) => !!entry);
50
+ }
51
+ catch {
52
+ return [];
53
+ }
54
+ }
@@ -0,0 +1,5 @@
1
+ import type { HookEventEnvelope, HookRunnerResult, LoadedHookRule } from "./types.js";
2
+ export interface HookRunnerOptions {
3
+ abortSignal?: AbortSignal;
4
+ }
5
+ export declare function runHookCommand(rule: LoadedHookRule, envelope: HookEventEnvelope, options?: HookRunnerOptions): Promise<HookRunnerResult>;
@@ -0,0 +1,225 @@
1
+ import { spawn } from "node:child_process";
2
+ import { performance } from "node:perf_hooks";
3
+ import { truncateHookText } from "./types.js";
4
+ const TERMINATE_GRACE_MS = 250;
5
+ export async function runHookCommand(rule, envelope, options = {}) {
6
+ const startedAt = performance.now();
7
+ let stdout = "";
8
+ let stderr = "";
9
+ let truncated = false;
10
+ let timeout;
11
+ let settled = false;
12
+ return new Promise((resolve) => {
13
+ const finish = (result) => {
14
+ if (settled)
15
+ return;
16
+ settled = true;
17
+ if (timeout)
18
+ clearTimeout(timeout);
19
+ resolve({
20
+ ...result,
21
+ elapsedMs: Math.round(performance.now() - startedAt),
22
+ stdout,
23
+ stderr,
24
+ truncated,
25
+ });
26
+ };
27
+ const child = spawn(rule.command.command, rule.command.args ?? [], {
28
+ cwd: rule.command.cwd ?? envelope.cwd,
29
+ env: buildHookEnv(rule, envelope),
30
+ shell: false,
31
+ detached: true,
32
+ stdio: ["pipe", "pipe", "pipe"],
33
+ });
34
+ const terminate = (signal = "SIGTERM") => {
35
+ if (!child.pid)
36
+ return;
37
+ try {
38
+ process.kill(-child.pid, signal);
39
+ }
40
+ catch {
41
+ try {
42
+ child.kill(signal);
43
+ }
44
+ catch {
45
+ // ignore
46
+ }
47
+ }
48
+ };
49
+ const hardTerminateSoon = () => {
50
+ setTimeout(() => terminate("SIGKILL"), TERMINATE_GRACE_MS).unref?.();
51
+ };
52
+ const abortListener = () => {
53
+ terminate();
54
+ hardTerminateSoon();
55
+ finish({
56
+ decision: rule.onError === "block" ? "deny" : "allow",
57
+ reason: rule.onError === "block" ? "Hook aborted." : undefined,
58
+ modelContext: [],
59
+ error: "Hook aborted.",
60
+ });
61
+ };
62
+ options.abortSignal?.addEventListener("abort", abortListener, { once: true });
63
+ timeout = setTimeout(() => {
64
+ terminate();
65
+ hardTerminateSoon();
66
+ finish({
67
+ decision: rule.onError === "block" ? "deny" : "allow",
68
+ reason: rule.onError === "block" ? `Hook timed out after ${rule.timeoutMs}ms.` : undefined,
69
+ modelContext: [],
70
+ error: `Hook timed out after ${rule.timeoutMs}ms.`,
71
+ });
72
+ }, rule.timeoutMs);
73
+ child.on("error", (error) => {
74
+ finish({
75
+ decision: rule.onError === "block" ? "deny" : "allow",
76
+ reason: rule.onError === "block" ? error.message : undefined,
77
+ modelContext: [],
78
+ error: error.message,
79
+ });
80
+ });
81
+ child.stdout?.on("data", (chunk) => {
82
+ if (truncated)
83
+ return;
84
+ stdout += chunk.toString("utf-8");
85
+ if (Buffer.byteLength(stdout, "utf-8") > rule.maxOutputBytes) {
86
+ stdout = truncateByBytes(stdout, rule.maxOutputBytes);
87
+ truncated = true;
88
+ terminate();
89
+ hardTerminateSoon();
90
+ }
91
+ });
92
+ child.stderr?.on("data", (chunk) => {
93
+ if (Buffer.byteLength(stderr, "utf-8") > rule.maxOutputBytes)
94
+ return;
95
+ stderr += chunk.toString("utf-8");
96
+ if (Buffer.byteLength(stderr, "utf-8") > rule.maxOutputBytes) {
97
+ stderr = truncateByBytes(stderr, rule.maxOutputBytes);
98
+ truncated = true;
99
+ }
100
+ });
101
+ child.on("close", (exitCode, signal) => {
102
+ options.abortSignal?.removeEventListener("abort", abortListener);
103
+ const parsed = parseHookOutput(stdout);
104
+ const parseError = stdout.trim() && !parsed ? "Hook stdout was not valid JSON." : undefined;
105
+ const parsedContext = parsed ? extractModelContext(parsed, rule.exposeToModel) : [];
106
+ const parsedDecision = parsed?.decision === "block" ? "deny" : parsed?.decision;
107
+ const reason = typeof parsed?.reason === "string"
108
+ ? parsed.reason
109
+ : exitCode === 2
110
+ ? firstNonEmpty(stderr, stdout, "Hook denied the event.")
111
+ : undefined;
112
+ if (truncated) {
113
+ finish({
114
+ decision: rule.onError === "block" ? "deny" : "allow",
115
+ reason: rule.onError === "block" ? "Hook output exceeded the configured limit." : undefined,
116
+ modelContext: [],
117
+ exitCode,
118
+ signal,
119
+ error: "Hook output exceeded the configured limit.",
120
+ });
121
+ return;
122
+ }
123
+ if (parseError) {
124
+ finish({
125
+ decision: rule.onError === "block" ? "deny" : "allow",
126
+ reason: rule.onError === "block" ? parseError : undefined,
127
+ modelContext: [],
128
+ exitCode,
129
+ signal,
130
+ error: parseError,
131
+ });
132
+ return;
133
+ }
134
+ if (exitCode === 2 || parsedDecision === "deny") {
135
+ finish({
136
+ decision: "deny",
137
+ reason: reason ?? "Hook denied the event.",
138
+ modelContext: parsedContext,
139
+ exitCode,
140
+ signal,
141
+ });
142
+ return;
143
+ }
144
+ if (exitCode && exitCode !== 0) {
145
+ const error = firstNonEmpty(stderr, stdout, `Hook exited with code ${exitCode}.`);
146
+ finish({
147
+ decision: rule.onError === "block" ? "deny" : "allow",
148
+ reason: rule.onError === "block" ? error : undefined,
149
+ modelContext: parsedContext,
150
+ exitCode,
151
+ signal,
152
+ error,
153
+ });
154
+ return;
155
+ }
156
+ finish({
157
+ decision: "allow",
158
+ reason,
159
+ modelContext: parsedContext,
160
+ exitCode,
161
+ signal,
162
+ });
163
+ });
164
+ child.stdin?.end(JSON.stringify(envelope));
165
+ });
166
+ }
167
+ function buildHookEnv(rule, envelope) {
168
+ const base = {};
169
+ for (const key of ["PATH", "HOME", "TMPDIR", "LANG", "LC_ALL", "SHELL"]) {
170
+ if (process.env[key])
171
+ base[key] = process.env[key];
172
+ }
173
+ return {
174
+ ...base,
175
+ ...(rule.command.env ?? {}),
176
+ BUBBLE_HOOK_DEPTH: "1",
177
+ BUBBLE_HOOK_ID: rule.id,
178
+ BUBBLE_HOOK_EVENT: envelope.eventName,
179
+ BUBBLE_HOOK_EVENT_ID: envelope.eventId,
180
+ BUBBLE_HOOK_CWD: envelope.cwd,
181
+ };
182
+ }
183
+ function parseHookOutput(stdout) {
184
+ const trimmed = stdout.trim();
185
+ if (!trimmed)
186
+ return {};
187
+ try {
188
+ const parsed = JSON.parse(trimmed);
189
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
190
+ return undefined;
191
+ return parsed;
192
+ }
193
+ catch {
194
+ return undefined;
195
+ }
196
+ }
197
+ function extractModelContext(parsed, ruleExposeToModel) {
198
+ const visible = parsed.visibleToModel === true || parsed.exposeToModel === true || ruleExposeToModel;
199
+ if (!visible)
200
+ return [];
201
+ const raw = parsed.modelContext ?? parsed.context;
202
+ if (typeof raw === "string")
203
+ return [truncateHookText(raw, 4000)];
204
+ if (Array.isArray(raw)) {
205
+ return raw
206
+ .filter((item) => typeof item === "string")
207
+ .map((item) => truncateHookText(item, 4000))
208
+ .slice(0, 8);
209
+ }
210
+ return [];
211
+ }
212
+ function truncateByBytes(value, maxBytes) {
213
+ const buffer = Buffer.from(value, "utf-8");
214
+ if (buffer.length <= maxBytes)
215
+ return value;
216
+ return buffer.subarray(0, maxBytes).toString("utf-8");
217
+ }
218
+ function firstNonEmpty(...values) {
219
+ for (const value of values) {
220
+ const trimmed = value.trim();
221
+ if (trimmed)
222
+ return truncateHookText(trimmed, 1000);
223
+ }
224
+ return "";
225
+ }
@@ -0,0 +1,37 @@
1
+ import type { LoadedHookRule } from "./types.js";
2
+ export interface ProjectHookFingerprint {
3
+ projectKey: string;
4
+ cwdRealpath: string;
5
+ fingerprint: string;
6
+ projectSettingsPath: string;
7
+ ruleCount: number;
8
+ files: Array<{
9
+ path: string;
10
+ sha256: string;
11
+ }>;
12
+ }
13
+ interface TrustedProjectHooks {
14
+ cwdRealpath: string;
15
+ fingerprint: string;
16
+ trustedAt: string;
17
+ projectSettingsPath: string;
18
+ ruleCount: number;
19
+ }
20
+ interface TrustStore {
21
+ version: 1;
22
+ projects: Record<string, TrustedProjectHooks>;
23
+ }
24
+ export interface TrustStoreOptions {
25
+ bubbleHome?: string;
26
+ }
27
+ export declare function getHookTrustPath(options?: TrustStoreOptions): string;
28
+ export declare function buildProjectHookFingerprint(cwd: string, projectSettingsPath: string, projectRules: LoadedHookRule[]): ProjectHookFingerprint;
29
+ export declare function readHookTrustStore(options?: TrustStoreOptions): TrustStore;
30
+ export declare function writeHookTrustStore(store: TrustStore, options?: TrustStoreOptions): void;
31
+ export declare function isProjectHookFingerprintTrusted(fingerprint: ProjectHookFingerprint | undefined, options?: TrustStoreOptions): {
32
+ trusted: boolean;
33
+ trustedFingerprint?: string;
34
+ };
35
+ export declare function trustProjectHooks(fingerprint: ProjectHookFingerprint, options?: TrustStoreOptions): void;
36
+ export declare function untrustProjectHooks(projectKey: string, options?: TrustStoreOptions): boolean;
37
+ export {};