@owloops/browserbird 1.0.2 → 1.0.4
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/bin/browserbird +7 -1
- package/dist/db-BsYEYsul.mjs +1011 -0
- package/dist/index.mjs +4749 -0
- package/package.json +6 -3
- package/src/channel/blocks.ts +0 -485
- package/src/channel/coalesce.ts +0 -79
- package/src/channel/commands.ts +0 -216
- package/src/channel/handler.ts +0 -272
- package/src/channel/slack.ts +0 -573
- package/src/channel/types.ts +0 -59
- package/src/cli/banner.ts +0 -10
- package/src/cli/birds.ts +0 -396
- package/src/cli/config.ts +0 -77
- package/src/cli/doctor.ts +0 -63
- package/src/cli/index.ts +0 -5
- package/src/cli/jobs.ts +0 -166
- package/src/cli/logs.ts +0 -67
- package/src/cli/run.ts +0 -148
- package/src/cli/sessions.ts +0 -158
- package/src/cli/style.ts +0 -19
- package/src/config.ts +0 -291
- package/src/core/logger.ts +0 -78
- package/src/core/redact.ts +0 -75
- package/src/core/types.ts +0 -83
- package/src/core/uid.ts +0 -26
- package/src/core/utils.ts +0 -137
- package/src/cron/parse.ts +0 -146
- package/src/cron/scheduler.ts +0 -242
- package/src/daemon.ts +0 -169
- package/src/db/auth.ts +0 -49
- package/src/db/birds.ts +0 -357
- package/src/db/core.ts +0 -377
- package/src/db/index.ts +0 -10
- package/src/db/jobs.ts +0 -289
- package/src/db/logs.ts +0 -64
- package/src/db/messages.ts +0 -79
- package/src/db/path.ts +0 -30
- package/src/db/sessions.ts +0 -165
- package/src/jobs.ts +0 -140
- package/src/provider/claude.test.ts +0 -95
- package/src/provider/claude.ts +0 -196
- package/src/provider/opencode.test.ts +0 -169
- package/src/provider/opencode.ts +0 -248
- package/src/provider/session.ts +0 -65
- package/src/provider/spawn.ts +0 -173
- package/src/provider/stream.ts +0 -67
- package/src/provider/types.ts +0 -24
- package/src/server/auth.ts +0 -135
- package/src/server/health.ts +0 -87
- package/src/server/http.ts +0 -132
- package/src/server/index.ts +0 -6
- package/src/server/lifecycle.ts +0 -135
- package/src/server/routes.ts +0 -1199
- package/src/server/sse.ts +0 -54
- package/src/server/static.ts +0 -45
- package/src/server/vnc-proxy.ts +0 -75
package/src/config.ts
DELETED
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
/** @fileoverview Configuration loading from JSON with env: variable resolution. */
|
|
2
|
-
|
|
3
|
-
import { readFileSync, writeFileSync, renameSync, existsSync } from 'node:fs';
|
|
4
|
-
import { resolve } from 'node:path';
|
|
5
|
-
import type { Config } from './core/types.ts';
|
|
6
|
-
import { logger } from './core/logger.ts';
|
|
7
|
-
|
|
8
|
-
const VALID_PROVIDERS = new Set<string>(['claude', 'opencode']);
|
|
9
|
-
export const DEFAULTS: Config = {
|
|
10
|
-
timezone: 'UTC',
|
|
11
|
-
slack: {
|
|
12
|
-
botToken: '',
|
|
13
|
-
appToken: '',
|
|
14
|
-
requireMention: true,
|
|
15
|
-
coalesce: { debounceMs: 3000, bypassDms: true },
|
|
16
|
-
channels: ['*'],
|
|
17
|
-
quietHours: { enabled: false, start: '23:00', end: '08:00', timezone: 'UTC' },
|
|
18
|
-
},
|
|
19
|
-
agents: [
|
|
20
|
-
{
|
|
21
|
-
id: 'default',
|
|
22
|
-
name: 'BrowserBird',
|
|
23
|
-
provider: 'claude',
|
|
24
|
-
model: 'sonnet',
|
|
25
|
-
maxTurns: 50,
|
|
26
|
-
systemPrompt: 'You are responding in a Slack workspace. Be concise, helpful, and natural.',
|
|
27
|
-
channels: ['*'],
|
|
28
|
-
},
|
|
29
|
-
],
|
|
30
|
-
sessions: {
|
|
31
|
-
ttlHours: 24,
|
|
32
|
-
maxConcurrent: 5,
|
|
33
|
-
processTimeoutMs: 300_000,
|
|
34
|
-
},
|
|
35
|
-
database: {
|
|
36
|
-
retentionDays: 30,
|
|
37
|
-
},
|
|
38
|
-
browser: {
|
|
39
|
-
enabled: false,
|
|
40
|
-
mcpConfigPath: undefined,
|
|
41
|
-
vncPort: 5900,
|
|
42
|
-
novncPort: 6080,
|
|
43
|
-
novncHost: 'localhost',
|
|
44
|
-
},
|
|
45
|
-
birds: { maxAttempts: 3 },
|
|
46
|
-
web: { enabled: true, host: '127.0.0.1', port: 18800, corsOrigin: '' },
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Resolves `"env:VAR_NAME"` strings to their environment variable values.
|
|
51
|
-
* Throws if the env var is not set.
|
|
52
|
-
*/
|
|
53
|
-
function resolveEnvValue(value: unknown): unknown {
|
|
54
|
-
if (typeof value === 'string' && value.startsWith('env:')) {
|
|
55
|
-
const envKey = value.slice(4);
|
|
56
|
-
const envValue = process.env[envKey];
|
|
57
|
-
if (envValue == null) {
|
|
58
|
-
throw new Error(`Environment variable ${envKey} is not set (referenced as "${value}")`);
|
|
59
|
-
}
|
|
60
|
-
return envValue;
|
|
61
|
-
}
|
|
62
|
-
if (Array.isArray(value)) {
|
|
63
|
-
return value.map(resolveEnvValue);
|
|
64
|
-
}
|
|
65
|
-
if (value !== null && typeof value === 'object') {
|
|
66
|
-
return resolveEnvValues(value as Record<string, unknown>);
|
|
67
|
-
}
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function resolveEnvValues(obj: Record<string, unknown>): Record<string, unknown> {
|
|
72
|
-
const resolved: Record<string, unknown> = {};
|
|
73
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
74
|
-
resolved[key] = resolveEnvValue(value);
|
|
75
|
-
}
|
|
76
|
-
return resolved;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function deepMerge(
|
|
80
|
-
target: Record<string, unknown>,
|
|
81
|
-
source: Record<string, unknown>,
|
|
82
|
-
): Record<string, unknown> {
|
|
83
|
-
const result = { ...target };
|
|
84
|
-
for (const [key, value] of Object.entries(source)) {
|
|
85
|
-
const targetValue = target[key];
|
|
86
|
-
if (
|
|
87
|
-
value !== null &&
|
|
88
|
-
typeof value === 'object' &&
|
|
89
|
-
!Array.isArray(value) &&
|
|
90
|
-
targetValue !== null &&
|
|
91
|
-
typeof targetValue === 'object' &&
|
|
92
|
-
!Array.isArray(targetValue)
|
|
93
|
-
) {
|
|
94
|
-
result[key] = deepMerge(
|
|
95
|
-
targetValue as Record<string, unknown>,
|
|
96
|
-
value as Record<string, unknown>,
|
|
97
|
-
);
|
|
98
|
-
} else {
|
|
99
|
-
result[key] = value;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return result;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Loads configuration from a JSON file, merges with defaults, and resolves env: references.
|
|
107
|
-
* Searches for browserbird.json in the current directory, then falls back to defaults.
|
|
108
|
-
*/
|
|
109
|
-
export function loadConfig(configPath?: string): Config {
|
|
110
|
-
const filePath = configPath ?? resolve('browserbird.json');
|
|
111
|
-
|
|
112
|
-
if (!existsSync(filePath)) {
|
|
113
|
-
logger.warn(`no config file found at ${filePath}, using defaults`);
|
|
114
|
-
return DEFAULTS;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
logger.info(`loading config from ${filePath}`);
|
|
118
|
-
const raw = readFileSync(filePath, 'utf-8');
|
|
119
|
-
|
|
120
|
-
let parsed: Record<string, unknown>;
|
|
121
|
-
try {
|
|
122
|
-
parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
123
|
-
} catch {
|
|
124
|
-
throw new Error(`Failed to parse config file: ${filePath}`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const merged = deepMerge(DEFAULTS as unknown as Record<string, unknown>, parsed);
|
|
128
|
-
const resolved = resolveEnvValues(merged);
|
|
129
|
-
const config = resolved as unknown as Config;
|
|
130
|
-
|
|
131
|
-
validateConfig(config);
|
|
132
|
-
|
|
133
|
-
return config;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Validates merged config and throws on invalid values, warns on risky combinations. */
|
|
137
|
-
function validateConfig(config: Config): void {
|
|
138
|
-
if (!Array.isArray(config.agents) || config.agents.length === 0) {
|
|
139
|
-
throw new Error('at least one agent must be configured');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
for (const agent of config.agents) {
|
|
143
|
-
if (!agent.id || !agent.name) {
|
|
144
|
-
throw new Error('each agent must have an "id" and "name"');
|
|
145
|
-
}
|
|
146
|
-
if (!VALID_PROVIDERS.has(agent.provider)) {
|
|
147
|
-
throw new Error(
|
|
148
|
-
`agent "${agent.id}": unknown provider "${agent.provider}" (expected: ${[...VALID_PROVIDERS].join(', ')})`,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
if (!agent.model) {
|
|
152
|
-
throw new Error(`agent "${agent.id}": "model" is required`);
|
|
153
|
-
}
|
|
154
|
-
if (!Array.isArray(agent.channels) || agent.channels.length === 0) {
|
|
155
|
-
throw new Error(`agent "${agent.id}": "channels" must be a non-empty array`);
|
|
156
|
-
}
|
|
157
|
-
if (agent.fallbackModel && agent.fallbackModel === agent.model) {
|
|
158
|
-
throw new Error(
|
|
159
|
-
`agent "${agent.id}": fallbackModel cannot be the same as model ("${agent.model}")`,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const browserMode = process.env['BROWSER_MODE'] ?? 'persistent';
|
|
165
|
-
if (config.browser.enabled && browserMode === 'persistent' && config.sessions.maxConcurrent > 1) {
|
|
166
|
-
logger.warn(
|
|
167
|
-
'persistent browser mode with maxConcurrent > 1 will cause lock contention; use "isolated" or set maxConcurrent to 1',
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Reads and merges JSON config with DEFAULTS but skips env: resolution.
|
|
174
|
-
* Returns raw config data suitable for reading/modifying before writing back.
|
|
175
|
-
*/
|
|
176
|
-
export function loadRawConfig(configPath?: string): Record<string, unknown> {
|
|
177
|
-
const filePath = configPath ?? resolve('browserbird.json');
|
|
178
|
-
if (!existsSync(filePath)) {
|
|
179
|
-
return JSON.parse(JSON.stringify(DEFAULTS)) as Record<string, unknown>;
|
|
180
|
-
}
|
|
181
|
-
const raw = readFileSync(filePath, 'utf-8');
|
|
182
|
-
let parsed: Record<string, unknown>;
|
|
183
|
-
try {
|
|
184
|
-
parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
185
|
-
} catch {
|
|
186
|
-
throw new Error(`Failed to parse config file: ${filePath}`);
|
|
187
|
-
}
|
|
188
|
-
return deepMerge(DEFAULTS as unknown as Record<string, unknown>, parsed);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Checks whether both Slack tokens are present and resolvable.
|
|
193
|
-
* Literal strings must be non-empty; `"env:VAR"` references must point to a set env var.
|
|
194
|
-
*/
|
|
195
|
-
export function hasSlackTokens(configPath?: string): boolean {
|
|
196
|
-
const filePath = configPath ?? resolve('browserbird.json');
|
|
197
|
-
if (!existsSync(filePath)) return false;
|
|
198
|
-
|
|
199
|
-
let parsed: Record<string, unknown>;
|
|
200
|
-
try {
|
|
201
|
-
parsed = JSON.parse(readFileSync(filePath, 'utf-8')) as Record<string, unknown>;
|
|
202
|
-
} catch {
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const slack = parsed['slack'] as Record<string, unknown> | undefined;
|
|
207
|
-
if (!slack) return false;
|
|
208
|
-
|
|
209
|
-
return isTokenResolvable(slack['botToken']) && isTokenResolvable(slack['appToken']);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function isTokenResolvable(value: unknown): boolean {
|
|
213
|
-
if (typeof value !== 'string' || !value) return false;
|
|
214
|
-
if (value.startsWith('env:')) {
|
|
215
|
-
const envKey = value.slice(4);
|
|
216
|
-
return !!process.env[envKey];
|
|
217
|
-
}
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** Atomic write: writes to a .tmp file then renames over the target. */
|
|
222
|
-
export function saveConfig(configPath: string, data: Record<string, unknown>): void {
|
|
223
|
-
const tmp = configPath + '.tmp';
|
|
224
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
225
|
-
renameSync(tmp, configPath);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Reads an existing .env file, updates/appends entries, and writes atomically.
|
|
230
|
-
* Preserves comments, blank lines, and ordering of existing entries.
|
|
231
|
-
*/
|
|
232
|
-
export function saveEnvFile(envPath: string, vars: Record<string, string>): void {
|
|
233
|
-
const remaining = new Map(Object.entries(vars));
|
|
234
|
-
const lines: string[] = [];
|
|
235
|
-
|
|
236
|
-
if (existsSync(envPath)) {
|
|
237
|
-
const content = readFileSync(envPath, 'utf-8');
|
|
238
|
-
for (const line of content.split('\n')) {
|
|
239
|
-
const trimmed = line.trim();
|
|
240
|
-
if (!trimmed || trimmed.startsWith('#')) {
|
|
241
|
-
lines.push(line);
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
const eqIdx = trimmed.indexOf('=');
|
|
245
|
-
if (eqIdx === -1) {
|
|
246
|
-
lines.push(line);
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
250
|
-
if (remaining.has(key)) {
|
|
251
|
-
lines.push(`${key}=${remaining.get(key)}`);
|
|
252
|
-
remaining.delete(key);
|
|
253
|
-
} else {
|
|
254
|
-
lines.push(line);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
for (const [key, value] of remaining) {
|
|
260
|
-
lines.push(`${key}=${value}`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const finalContent = lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
264
|
-
const tmp = envPath + '.tmp';
|
|
265
|
-
writeFileSync(tmp, finalContent.endsWith('\n') ? finalContent : finalContent + '\n', 'utf-8');
|
|
266
|
-
renameSync(tmp, envPath);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Reads a .env file and injects entries into process.env.
|
|
271
|
-
* Handles comments, blank lines, and optionally quoted values.
|
|
272
|
-
*/
|
|
273
|
-
export function loadDotEnv(envPath: string): void {
|
|
274
|
-
if (!existsSync(envPath)) return;
|
|
275
|
-
const content = readFileSync(envPath, 'utf-8');
|
|
276
|
-
for (const line of content.split('\n')) {
|
|
277
|
-
const trimmed = line.trim();
|
|
278
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
279
|
-
const eqIdx = trimmed.indexOf('=');
|
|
280
|
-
if (eqIdx === -1) continue;
|
|
281
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
282
|
-
let value = trimmed.slice(eqIdx + 1).trim();
|
|
283
|
-
if (
|
|
284
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
285
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
286
|
-
) {
|
|
287
|
-
value = value.slice(1, -1);
|
|
288
|
-
}
|
|
289
|
-
process.env[key] = value;
|
|
290
|
-
}
|
|
291
|
-
}
|
package/src/core/logger.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/** @fileoverview Structured logger. Writes to stderr, respects NO_COLOR. */
|
|
2
|
-
|
|
3
|
-
import { styleText } from 'node:util';
|
|
4
|
-
|
|
5
|
-
function shouldUseColor(): boolean {
|
|
6
|
-
if (process.env['NO_COLOR'] !== undefined) return false;
|
|
7
|
-
if (process.env['TERM'] === 'dumb') return false;
|
|
8
|
-
if (process.argv.includes('--no-color')) return false;
|
|
9
|
-
return process.stderr.isTTY === true;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const useColor = shouldUseColor();
|
|
13
|
-
|
|
14
|
-
function style(format: string | string[], text: string): string {
|
|
15
|
-
if (!useColor) return text;
|
|
16
|
-
return styleText(format as Parameters<typeof styleText>[0], text);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const LOG_LEVELS = {
|
|
20
|
-
ERROR: 0,
|
|
21
|
-
WARN: 1,
|
|
22
|
-
INFO: 2,
|
|
23
|
-
DEBUG: 3,
|
|
24
|
-
} as const;
|
|
25
|
-
|
|
26
|
-
type LogLevel = (typeof LOG_LEVELS)[keyof typeof LOG_LEVELS];
|
|
27
|
-
|
|
28
|
-
let currentLevel: LogLevel = LOG_LEVELS.INFO;
|
|
29
|
-
|
|
30
|
-
function timestamp(): string {
|
|
31
|
-
return new Date().toISOString().slice(11, 23);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function write(prefix: string, message: string): void {
|
|
35
|
-
process.stderr.write(`${style('dim', timestamp())} ${prefix} ${message}\n`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const logger = {
|
|
39
|
-
error(message: string): void {
|
|
40
|
-
if (currentLevel >= LOG_LEVELS.ERROR) {
|
|
41
|
-
write(style('red', '[error]'), message);
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
|
|
45
|
-
warn(message: string): void {
|
|
46
|
-
if (currentLevel >= LOG_LEVELS.WARN) {
|
|
47
|
-
write(style('yellow', '[warn]'), message);
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
info(message: string): void {
|
|
52
|
-
if (currentLevel >= LOG_LEVELS.INFO) {
|
|
53
|
-
write(style('blue', '[info]'), message);
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
debug(message: string): void {
|
|
58
|
-
if (currentLevel >= LOG_LEVELS.DEBUG) {
|
|
59
|
-
write(style('dim', '[debug]'), message);
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
success(message: string): void {
|
|
64
|
-
if (currentLevel >= LOG_LEVELS.INFO) {
|
|
65
|
-
write(style('green', '[ok]'), message);
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
setLevel(level: 'error' | 'warn' | 'info' | 'debug'): void {
|
|
70
|
-
const map: Record<string, LogLevel> = {
|
|
71
|
-
error: LOG_LEVELS.ERROR,
|
|
72
|
-
warn: LOG_LEVELS.WARN,
|
|
73
|
-
info: LOG_LEVELS.INFO,
|
|
74
|
-
debug: LOG_LEVELS.DEBUG,
|
|
75
|
-
};
|
|
76
|
-
currentLevel = map[level] ?? LOG_LEVELS.INFO;
|
|
77
|
-
},
|
|
78
|
-
};
|
package/src/core/redact.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/** @fileoverview Output redaction: scrubs known secrets and token patterns from agent output. */
|
|
2
|
-
|
|
3
|
-
const REDACTED = '[redacted]';
|
|
4
|
-
|
|
5
|
-
const SENSITIVE_NAME_RE = /KEY|SECRET|TOKEN|PASSWORD/i;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Token prefix patterns. Each entry is [prefix, minLength] where minLength
|
|
9
|
-
* is the shortest plausible token including the prefix (avoids false positives
|
|
10
|
-
* on short strings that happen to start with a prefix).
|
|
11
|
-
*/
|
|
12
|
-
const TOKEN_PATTERNS: Array<[string, number]> = [
|
|
13
|
-
['xoxb-', 20],
|
|
14
|
-
['xapp-', 20],
|
|
15
|
-
['sk-ant-api', 20],
|
|
16
|
-
['sk-ant-oat', 20],
|
|
17
|
-
['sk-or-', 20],
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
let knownSecrets: string[] | undefined;
|
|
21
|
-
|
|
22
|
-
function collectSecrets(): string[] {
|
|
23
|
-
const secrets: string[] = [];
|
|
24
|
-
for (const [name, value] of Object.entries(process.env)) {
|
|
25
|
-
if (value && SENSITIVE_NAME_RE.test(name) && value.length >= 8) {
|
|
26
|
-
secrets.push(value);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
secrets.sort((a, b) => b.length - a.length);
|
|
30
|
-
return secrets;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function getSecrets(): string[] {
|
|
34
|
-
if (!knownSecrets) {
|
|
35
|
-
knownSecrets = collectSecrets();
|
|
36
|
-
}
|
|
37
|
-
return knownSecrets;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Re-collects secrets from process.env. Call after env changes (e.g. onboarding). */
|
|
41
|
-
export function refreshSecrets(): void {
|
|
42
|
-
knownSecrets = collectSecrets();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function escapeForRegex(s: string): string {
|
|
46
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function buildPatternRegex(): RegExp {
|
|
50
|
-
const parts = TOKEN_PATTERNS.map(([prefix, minLength]) => {
|
|
51
|
-
const escaped = escapeForRegex(prefix);
|
|
52
|
-
const remaining = minLength - prefix.length;
|
|
53
|
-
return `${escaped}[A-Za-z0-9_\\-]{${remaining},}`;
|
|
54
|
-
});
|
|
55
|
-
return new RegExp(parts.join('|'), 'g');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const patternRegex = buildPatternRegex();
|
|
59
|
-
|
|
60
|
-
/** Replaces known secret values and token patterns in text with [redacted]. */
|
|
61
|
-
export function redact(text: string): string {
|
|
62
|
-
if (!text) return text;
|
|
63
|
-
|
|
64
|
-
let result = text;
|
|
65
|
-
|
|
66
|
-
for (const secret of getSecrets()) {
|
|
67
|
-
if (result.includes(secret)) {
|
|
68
|
-
result = result.replaceAll(secret, REDACTED);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
result = result.replace(patternRegex, REDACTED);
|
|
73
|
-
|
|
74
|
-
return result;
|
|
75
|
-
}
|
package/src/core/types.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/** @fileoverview Shared interfaces and type definitions for BrowserBird. */
|
|
2
|
-
|
|
3
|
-
import type { ProviderName } from '../provider/types.ts';
|
|
4
|
-
|
|
5
|
-
export interface SlackConfig {
|
|
6
|
-
botToken: string;
|
|
7
|
-
appToken: string;
|
|
8
|
-
requireMention: boolean;
|
|
9
|
-
coalesce: {
|
|
10
|
-
debounceMs: number;
|
|
11
|
-
bypassDms: boolean;
|
|
12
|
-
};
|
|
13
|
-
channels: string[];
|
|
14
|
-
quietHours: {
|
|
15
|
-
enabled: boolean;
|
|
16
|
-
start: string;
|
|
17
|
-
end: string;
|
|
18
|
-
timezone: string;
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface AgentConfig {
|
|
23
|
-
id: string;
|
|
24
|
-
name: string;
|
|
25
|
-
provider: ProviderName;
|
|
26
|
-
model: string;
|
|
27
|
-
fallbackModel?: string;
|
|
28
|
-
maxTurns: number;
|
|
29
|
-
systemPrompt: string;
|
|
30
|
-
channels: string[];
|
|
31
|
-
processTimeoutMs?: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface SessionsConfig {
|
|
35
|
-
ttlHours: number;
|
|
36
|
-
maxConcurrent: number;
|
|
37
|
-
processTimeoutMs: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface BrowserConfig {
|
|
41
|
-
enabled: boolean;
|
|
42
|
-
mcpConfigPath: string | undefined;
|
|
43
|
-
vncPort: number;
|
|
44
|
-
novncPort: number;
|
|
45
|
-
novncHost: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface DatabaseConfig {
|
|
49
|
-
retentionDays: number;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface BirdsConfig {
|
|
53
|
-
maxAttempts: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface WebConfig {
|
|
57
|
-
enabled: boolean;
|
|
58
|
-
host: string;
|
|
59
|
-
port: number;
|
|
60
|
-
corsOrigin: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface Config {
|
|
64
|
-
timezone: string;
|
|
65
|
-
slack: SlackConfig;
|
|
66
|
-
agents: AgentConfig[];
|
|
67
|
-
sessions: SessionsConfig;
|
|
68
|
-
database: DatabaseConfig;
|
|
69
|
-
browser: BrowserConfig;
|
|
70
|
-
birds: BirdsConfig;
|
|
71
|
-
web: WebConfig;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export const COMMANDS = {
|
|
75
|
-
SESSIONS: 'sessions',
|
|
76
|
-
BIRDS: 'birds',
|
|
77
|
-
CONFIG: 'config',
|
|
78
|
-
LOGS: 'logs',
|
|
79
|
-
JOBS: 'jobs',
|
|
80
|
-
DOCTOR: 'doctor',
|
|
81
|
-
} as const;
|
|
82
|
-
|
|
83
|
-
export type Command = (typeof COMMANDS)[keyof typeof COMMANDS];
|
package/src/core/uid.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/** @fileoverview Prefixed short ID generation (PocketBase/Motebase pattern). */
|
|
2
|
-
|
|
3
|
-
import { randomBytes } from 'node:crypto';
|
|
4
|
-
|
|
5
|
-
const ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
6
|
-
|
|
7
|
-
export const UID_PREFIX = {
|
|
8
|
-
bird: 'br_',
|
|
9
|
-
flight: 'fl_',
|
|
10
|
-
session: 'ss_',
|
|
11
|
-
} as const;
|
|
12
|
-
|
|
13
|
-
export function generateUid(prefix: string): string {
|
|
14
|
-
const bytes = randomBytes(15);
|
|
15
|
-
let id = prefix;
|
|
16
|
-
for (let i = 0; i < 15; i++) {
|
|
17
|
-
id += ALPHABET[bytes[i]! % 36];
|
|
18
|
-
}
|
|
19
|
-
return id;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function shortUid(uid: string): string {
|
|
23
|
-
const i = uid.indexOf('_');
|
|
24
|
-
if (i === -1) return uid.slice(0, 10);
|
|
25
|
-
return uid.slice(0, i + 1 + 7);
|
|
26
|
-
}
|
package/src/core/utils.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/** @fileoverview Shared utilities: formatting, time ranges, and CLI table output. */
|
|
2
|
-
|
|
3
|
-
const BIRD_NAME_MAX_LENGTH = 50;
|
|
4
|
-
|
|
5
|
-
export function formatDuration(ms: number): string {
|
|
6
|
-
const totalSeconds = Math.round(ms / 1000);
|
|
7
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
8
|
-
const seconds = totalSeconds % 60;
|
|
9
|
-
if (minutes === 0) return `${seconds}s`;
|
|
10
|
-
return `${minutes}m ${seconds}s`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function deriveBirdName(prompt: string): string {
|
|
14
|
-
return prompt.trim().slice(0, BIRD_NAME_MAX_LENGTH);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Returns true if the current time in the given timezone falls within a HH:MM time range.
|
|
19
|
-
* Handles wrap-around ranges (e.g. 22:00-06:00 spanning midnight).
|
|
20
|
-
*/
|
|
21
|
-
export function isWithinTimeRange(
|
|
22
|
-
start: string,
|
|
23
|
-
end: string,
|
|
24
|
-
date: Date,
|
|
25
|
-
timezone: string,
|
|
26
|
-
): boolean {
|
|
27
|
-
const parts = new Intl.DateTimeFormat('en-US', {
|
|
28
|
-
timeZone: timezone,
|
|
29
|
-
hour: 'numeric',
|
|
30
|
-
minute: 'numeric',
|
|
31
|
-
hour12: false,
|
|
32
|
-
}).formatToParts(date);
|
|
33
|
-
|
|
34
|
-
const h = Number(parts.find((p) => p.type === 'hour')?.value ?? 0);
|
|
35
|
-
const m = Number(parts.find((p) => p.type === 'minute')?.value ?? 0);
|
|
36
|
-
const nowMinutes = h * 60 + m;
|
|
37
|
-
|
|
38
|
-
const startMinutes = parseHHMM(start);
|
|
39
|
-
const endMinutes = parseHHMM(end);
|
|
40
|
-
|
|
41
|
-
if (startMinutes <= endMinutes) {
|
|
42
|
-
return nowMinutes >= startMinutes && nowMinutes < endMinutes;
|
|
43
|
-
}
|
|
44
|
-
return nowMinutes >= startMinutes || nowMinutes < endMinutes;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function parseHHMM(s: string): number {
|
|
48
|
-
const [hh, mm] = s.split(':');
|
|
49
|
-
const h = Number(hh);
|
|
50
|
-
const m = Number(mm ?? 0);
|
|
51
|
-
if (!Number.isFinite(h) || !Number.isFinite(m)) return 0;
|
|
52
|
-
return h * 60 + m;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// eslint-disable-next-line no-control-regex
|
|
56
|
-
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
57
|
-
|
|
58
|
-
function visibleLength(s: string): number {
|
|
59
|
-
return s.replace(ANSI_RE, '').length;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Print a formatted table with auto-calculated column widths to stdout. */
|
|
63
|
-
export function printTable(
|
|
64
|
-
headers: string[],
|
|
65
|
-
rows: string[][],
|
|
66
|
-
maxWidths?: Array<number | undefined>,
|
|
67
|
-
): void {
|
|
68
|
-
const widths = headers.map((h) => h.length);
|
|
69
|
-
|
|
70
|
-
for (const row of rows) {
|
|
71
|
-
for (let i = 0; i < row.length; i++) {
|
|
72
|
-
const cellLen = visibleLength(row[i] ?? '');
|
|
73
|
-
const maxW = maxWidths?.[i];
|
|
74
|
-
const capped = maxW != null ? Math.min(cellLen, maxW) : cellLen;
|
|
75
|
-
widths[i] = Math.max(widths[i] ?? 0, capped);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function pad(s: string, width: number, max?: number): string {
|
|
80
|
-
const vis = visibleLength(s);
|
|
81
|
-
const truncated = max != null && vis > max ? s.slice(0, max - 3) + '...' : s;
|
|
82
|
-
const padLen = width - visibleLength(truncated);
|
|
83
|
-
return padLen > 0 ? truncated + ' '.repeat(padLen) : truncated;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const indent = ' ';
|
|
87
|
-
console.log(indent + headers.map((h, i) => pad(h, widths[i]!)).join(' '));
|
|
88
|
-
console.log(indent + widths.map((w) => '-'.repeat(w)).join(' '));
|
|
89
|
-
for (const row of rows) {
|
|
90
|
-
console.log(
|
|
91
|
-
indent + row.map((cell, i) => pad(cell ?? '', widths[i]!, maxWidths?.[i])).join(' '),
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function levenshtein(a: string, b: string): number {
|
|
97
|
-
const m = a.length;
|
|
98
|
-
const n = b.length;
|
|
99
|
-
const dp: number[][] = Array.from({ length: m + 1 }, () =>
|
|
100
|
-
Array.from({ length: n + 1 }, () => 0),
|
|
101
|
-
);
|
|
102
|
-
for (let i = 0; i <= m; i++) dp[i]![0] = i;
|
|
103
|
-
for (let j = 0; j <= n; j++) dp[0]![j] = j;
|
|
104
|
-
for (let i = 1; i <= m; i++) {
|
|
105
|
-
for (let j = 1; j <= n; j++) {
|
|
106
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
107
|
-
dp[i]![j] = Math.min(dp[i - 1]![j]! + 1, dp[i]![j - 1]! + 1, dp[i - 1]![j - 1]! + cost);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return dp[m]![n]!;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function unknownSubcommand(
|
|
114
|
-
subcommand: string,
|
|
115
|
-
command: string,
|
|
116
|
-
validCommands?: string[],
|
|
117
|
-
): void {
|
|
118
|
-
const label = command ? 'subcommand' : 'command';
|
|
119
|
-
process.stderr.write(`error: unknown ${label}: ${subcommand}\n`);
|
|
120
|
-
if (validCommands && validCommands.length > 0) {
|
|
121
|
-
let bestMatch = '';
|
|
122
|
-
let bestDist = Infinity;
|
|
123
|
-
for (const cmd of validCommands) {
|
|
124
|
-
const dist = levenshtein(subcommand, cmd);
|
|
125
|
-
if (dist < bestDist) {
|
|
126
|
-
bestDist = dist;
|
|
127
|
-
bestMatch = cmd;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (bestDist <= 2 && bestMatch) {
|
|
131
|
-
process.stderr.write(`did you mean '${bestMatch}'?\n`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
const helpCmd = command ? `browserbird ${command} --help` : 'browserbird --help';
|
|
135
|
-
process.stderr.write(`run '${helpCmd}' for usage\n`);
|
|
136
|
-
process.exitCode = 1;
|
|
137
|
-
}
|