@bbigbang/agent-node 0.1.0

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 (47) hide show
  1. package/dist/agentHost.js +483 -0
  2. package/dist/appVersion.js +14 -0
  3. package/dist/assetCachePaths.js +35 -0
  4. package/dist/attachmentInput.js +588 -0
  5. package/dist/attachmentMaterializer.js +230 -0
  6. package/dist/bigbangCli.js +17 -0
  7. package/dist/bigbangMessageSendDetection.js +284 -0
  8. package/dist/builtinSkillRoots.js +54 -0
  9. package/dist/claudeConfig.js +32 -0
  10. package/dist/claudeDirectRuntime.js +1960 -0
  11. package/dist/claudeSessionControls.js +78 -0
  12. package/dist/claudeTranscriptFs.js +147 -0
  13. package/dist/codexAppServerClient.js +188 -0
  14. package/dist/codexAppServerEnv.js +14 -0
  15. package/dist/codexAppServerRpc.js +273 -0
  16. package/dist/codexAppServerRuntime.js +3495 -0
  17. package/dist/codexBuiltinPrompt.js +117 -0
  18. package/dist/codexConversationSummarizer.js +76 -0
  19. package/dist/codexTranscriptFs.js +145 -0
  20. package/dist/config.js +129 -0
  21. package/dist/connection.js +151 -0
  22. package/dist/dispatchQueueStore.js +39 -0
  23. package/dist/dreamEnv.js +1 -0
  24. package/dist/dreamMemoryFallback.js +118 -0
  25. package/dist/dreamToolPolicy.js +293 -0
  26. package/dist/droidMissionRunner.js +808 -0
  27. package/dist/executor.js +1078 -0
  28. package/dist/hostRuntime.js +1 -0
  29. package/dist/libraryAuthorityFs.js +74 -0
  30. package/dist/libraryMirror.js +183 -0
  31. package/dist/main.js +1659 -0
  32. package/dist/native-worker/native-worker.mjs +475 -0
  33. package/dist/nativeMissionAgentDispatch.js +463 -0
  34. package/dist/nativeMissionRunner.js +461 -0
  35. package/dist/nativeSkillMounts.js +204 -0
  36. package/dist/nativeWorkerHost.js +142 -0
  37. package/dist/nodeSink.js +142 -0
  38. package/dist/panelHttpFetch.js +334 -0
  39. package/dist/runtimeDrivers.js +62 -0
  40. package/dist/skillFs.js +229 -0
  41. package/dist/soloHost.js +165 -0
  42. package/dist/soloNodeSink.js +138 -0
  43. package/dist/terminalManager.js +254 -0
  44. package/dist/workspaceFs.js +1020 -0
  45. package/dist/workspaceGit.js +694 -0
  46. package/dist/workspaceInspect.js +22 -0
  47. package/package.json +49 -0
@@ -0,0 +1,483 @@
1
+ import fs from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { BindingRuntime, createRun, finishRun, getUiMode, log, } from '@bbigbang/runtime-acp';
4
+ import { ClaudeDirectRuntime } from './claudeDirectRuntime.js';
5
+ import { CodexAppServerRuntime } from './codexAppServerRuntime.js';
6
+ import { buildHostAssetCacheRoot } from './assetCachePaths.js';
7
+ import { NodeSink } from './nodeSink.js';
8
+ export class HostDispatchRejectedError extends Error {
9
+ code;
10
+ constructor(code, message) {
11
+ super(message);
12
+ this.name = 'HostDispatchRejectedError';
13
+ this.code = code;
14
+ }
15
+ }
16
+ export class AgentHost {
17
+ hostKey;
18
+ sessionKey;
19
+ hostInstanceId;
20
+ nodeProcessInstanceId;
21
+ workspaceRoot;
22
+ assetCacheRoot;
23
+ runtime;
24
+ db;
25
+ send;
26
+ hooks;
27
+ maxInboxSize;
28
+ state = 'idle';
29
+ inbox = [];
30
+ processing = false;
31
+ currentRunId = null;
32
+ lastWakeAt = null;
33
+ lastSleepAt = Date.now();
34
+ lastError = null;
35
+ closeRequested = false;
36
+ closeWaiters = [];
37
+ constructor(params) {
38
+ this.hostKey = params.hostKey;
39
+ this.sessionKey = params.sessionKey;
40
+ this.hostInstanceId = params.hostInstanceId ?? randomUUID();
41
+ this.nodeProcessInstanceId = params.config.processInstanceId;
42
+ this.workspaceRoot = params.workspaceRoot;
43
+ this.assetCacheRoot = params.assetCacheRoot?.trim()
44
+ || (params.assetAccessConfig
45
+ ? buildHostAssetCacheRoot(params.assetAccessConfig.agentId, params.assetAccessConfig.conversationId, this.hostInstanceId)
46
+ : null);
47
+ this.db = params.db;
48
+ this.send = params.send;
49
+ this.hooks = params.hooks ?? {};
50
+ this.maxInboxSize = Math.max(1, params.config.hostInboxLimit);
51
+ const onAssetMaterialized = params.assetAccessConfig
52
+ ? (event) => {
53
+ this.send({
54
+ type: 'asset.materialized',
55
+ assetId: event.assetId,
56
+ agentId: params.assetAccessConfig.agentId,
57
+ conversationId: params.assetAccessConfig.conversationId,
58
+ nodeProcessInstanceId: this.nodeProcessInstanceId,
59
+ hostKey: this.hostKey,
60
+ hostInstanceId: this.hostInstanceId,
61
+ runId: event.runId ?? null,
62
+ localPath: event.localPath,
63
+ materializedAt: event.materializedAt,
64
+ });
65
+ }
66
+ : undefined;
67
+ if (params.agentType === 'claude_sdk') {
68
+ this.runtime = new ClaudeDirectRuntime({
69
+ db: params.db,
70
+ sessionKey: params.sessionKey,
71
+ bindingKey: params.bindingKey,
72
+ workspaceRoot: params.workspaceRoot,
73
+ env: params.env,
74
+ model: params.model,
75
+ reasoningEffort: params.reasoningEffort,
76
+ claudePermissionMode: params.claudePermissionMode,
77
+ disabledToolKinds: params.disabledToolKinds,
78
+ channelBridgeMcpEntry: params.channelBridgeMcpEntry,
79
+ agentSurfaceMode: params.config.agentSurfaceMode,
80
+ toolAuth: params.toolAuth,
81
+ workspaceLockManager: params.workspaceLockManager,
82
+ assetAccessConfig: params.assetAccessConfig,
83
+ assetCacheRoot: this.assetCacheRoot,
84
+ onAssetMaterialized,
85
+ queryImpl: params.claudeQueryImpl,
86
+ });
87
+ }
88
+ else if (params.agentType === 'codex_app_server') {
89
+ this.runtime = new CodexAppServerRuntime({
90
+ db: params.db,
91
+ sessionKey: params.sessionKey,
92
+ bindingKey: params.bindingKey,
93
+ toolAuth: params.toolAuth,
94
+ workspaceRoot: params.workspaceRoot,
95
+ agentCommand: params.agentCommand,
96
+ agentArgs: params.agentArgs,
97
+ env: params.env,
98
+ model: params.model,
99
+ reasoningEffort: params.reasoningEffort,
100
+ serviceTier: params.codexServiceTier,
101
+ disabledToolKinds: params.disabledToolKinds,
102
+ channelBridgeMcpEntry: params.channelBridgeMcpEntry,
103
+ agentSurfaceMode: params.config.agentSurfaceMode,
104
+ assetAccessConfig: params.assetAccessConfig,
105
+ assetCacheRoot: this.assetCacheRoot,
106
+ onAssetMaterialized,
107
+ workspaceLockManager: params.workspaceLockManager,
108
+ });
109
+ }
110
+ else {
111
+ this.runtime = new BindingRuntime({
112
+ db: params.db,
113
+ config: params.config,
114
+ toolAuth: params.toolAuth,
115
+ sessionKey: params.sessionKey,
116
+ bindingKey: params.bindingKey,
117
+ workspaceRoot: params.workspaceRoot,
118
+ agentCommand: params.agentCommand,
119
+ agentArgs: params.agentArgs,
120
+ env: params.env,
121
+ disabledToolKinds: params.disabledToolKinds,
122
+ channelBridgeMcpEntry: params.channelBridgeMcpEntry,
123
+ workspaceLockManager: params.workspaceLockManager,
124
+ });
125
+ }
126
+ }
127
+ getState() {
128
+ return this.state;
129
+ }
130
+ getHostInstanceId() {
131
+ return this.hostInstanceId;
132
+ }
133
+ getCurrentRunId() {
134
+ return this.currentRunId;
135
+ }
136
+ getLastWakeAt() {
137
+ return this.lastWakeAt;
138
+ }
139
+ getLastSleepAt() {
140
+ return this.lastSleepAt;
141
+ }
142
+ getInboxSize() {
143
+ return this.inbox.length;
144
+ }
145
+ getLastError() {
146
+ return this.lastError;
147
+ }
148
+ getWorkspaceRoot() {
149
+ return this.workspaceRoot;
150
+ }
151
+ hasPendingApproval() {
152
+ return this.runtime.hasPendingPermission();
153
+ }
154
+ isIdleExpired(now, idleTimeoutMs) {
155
+ if (this.state !== 'idle')
156
+ return false;
157
+ if (this.currentRunId)
158
+ return false;
159
+ if (this.inbox.length > 0)
160
+ return false;
161
+ if (this.hasPendingApproval())
162
+ return false;
163
+ if (!this.lastSleepAt)
164
+ return false;
165
+ return now - this.lastSleepAt >= idleTimeoutMs;
166
+ }
167
+ async dispatch(msg) {
168
+ return new Promise((resolve, reject) => {
169
+ if (this.closeRequested) {
170
+ reject(new HostDispatchRejectedError('HOST_CLOSED', `Host ${this.hostKey} closed`));
171
+ return;
172
+ }
173
+ if (this.state === 'failed') {
174
+ reject(new HostDispatchRejectedError('HOST_FAILED', this.lastError ?? `Host ${this.hostKey} is failed`));
175
+ return;
176
+ }
177
+ if (this.inbox.length >= this.maxInboxSize) {
178
+ reject(new HostDispatchRejectedError('HOST_INBOX_OVERFLOW', `Host ${this.hostKey} inbox overflow (limit=${this.maxInboxSize})`));
179
+ return;
180
+ }
181
+ if (this.processing || this.currentRunId) {
182
+ log.info('[agent-host] queued dispatch in inbox', {
183
+ hostKey: this.hostKey,
184
+ runId: msg.runId,
185
+ conversationId: msg.conversationId,
186
+ inboxSize: this.inbox.length + 1,
187
+ });
188
+ }
189
+ this.inbox.push({ msg, resolve, reject });
190
+ void this.processInbox();
191
+ });
192
+ }
193
+ async processInbox() {
194
+ if (this.processing || this.state === 'failed')
195
+ return;
196
+ this.processing = true;
197
+ try {
198
+ while (this.inbox.length > 0) {
199
+ const pending = this.inbox.shift();
200
+ try {
201
+ await this.runDispatch(pending.msg);
202
+ pending.resolve();
203
+ }
204
+ catch (error) {
205
+ const err = error instanceof Error ? error : new Error(String(error));
206
+ pending.reject(err);
207
+ if (this.getState() === 'failed') {
208
+ this.failPendingInbox(err);
209
+ break;
210
+ }
211
+ }
212
+ }
213
+ }
214
+ finally {
215
+ this.processing = false;
216
+ if (this.getState() !== 'failed' && !this.currentRunId) {
217
+ this.state = 'idle';
218
+ this.lastSleepAt = Date.now();
219
+ }
220
+ this.notifyCloseWaitersIfDrained();
221
+ }
222
+ }
223
+ async runDispatch(msg) {
224
+ const { runId, conversationId, prompt } = msg;
225
+ let deliveryAckSent = false;
226
+ const sendDeliveryAck = async () => {
227
+ if (deliveryAckSent || !msg.delivery)
228
+ return;
229
+ deliveryAckSent = true;
230
+ this.send({
231
+ type: 'run.delivery.ack',
232
+ deliveryId: msg.delivery.deliveryId,
233
+ roundId: msg.delivery.roundId,
234
+ runId,
235
+ conversationId,
236
+ ackedThroughSeq: msg.delivery.ackedThroughSeq,
237
+ mode: 'dispatch',
238
+ });
239
+ };
240
+ const sink = new NodeSink(runId, conversationId, this.send, {
241
+ onPermissionRequest: () => {
242
+ this.hooks.onAwaitingApproval?.(msg);
243
+ },
244
+ });
245
+ if (msg.dispatchMode === 'resume') {
246
+ log.info('[agent-host] waking existing host', {
247
+ hostKey: this.hostKey,
248
+ sessionKey: this.sessionKey,
249
+ lastWakeAt: this.lastWakeAt,
250
+ lastSleepAt: this.lastSleepAt,
251
+ });
252
+ }
253
+ const existingRun = this.db
254
+ .prepare(`SELECT run_id as runId FROM runs WHERE run_id = ?`)
255
+ .get(runId);
256
+ if (!existingRun) {
257
+ createRun(this.db, { runId, sessionKey: this.sessionKey, promptText: prompt });
258
+ }
259
+ const hadPreviousWake = this.lastWakeAt != null;
260
+ this.state = 'active';
261
+ this.currentRunId = runId;
262
+ this.lastWakeAt = Date.now();
263
+ this.lastSleepAt = null;
264
+ this.lastError = null;
265
+ this.writeRunContext(msg);
266
+ this.hooks.onRunStart?.(msg);
267
+ const nowMs = Date.now();
268
+ this.send({
269
+ type: 'run.event',
270
+ runId,
271
+ conversationId,
272
+ event: { type: 'turn.begin', turnId: runId, startedAt: nowMs, promptText: prompt },
273
+ });
274
+ this.send({
275
+ type: 'run.event',
276
+ runId,
277
+ conversationId,
278
+ event: { type: 'conversation.status', conversationId, status: 'active' },
279
+ });
280
+ try {
281
+ this.runtime.updateConfig?.({
282
+ model: msg.model,
283
+ reasoningEffort: msg.reasoningEffort,
284
+ claudePermissionMode: msg.claudePermissionMode,
285
+ codexServiceTier: msg.codexServiceTier,
286
+ disabledToolKinds: msg.disabledToolKinds,
287
+ envVars: msg.envVars,
288
+ });
289
+ const uiMode = getUiMode(this.db, `node:${conversationId}:-:node_user`) ?? 'summary';
290
+ const shouldInjectResumeContext = msg.dispatchMode === 'resume' && !hadPreviousWake;
291
+ const recoveryContextText = msg.dispatchMode === 'resume' && !shouldInjectResumeContext
292
+ ? msg.resumeContextText
293
+ : undefined;
294
+ const result = await this.runtime.prompt({
295
+ runId,
296
+ promptText: prompt,
297
+ sink,
298
+ uiMode,
299
+ dreamMode: msg.dispatchMode === 'dream',
300
+ disabledToolKinds: [...(msg.disabledToolKinds ?? [])],
301
+ attachments: msg.attachments,
302
+ promptResources: msg.attachments,
303
+ runtimeOverrides: msg.runtimeOverrides,
304
+ systemPromptText: msg.systemPromptText,
305
+ contextText: msg.contextText,
306
+ resumeContextText: shouldInjectResumeContext ? msg.resumeContextText : undefined,
307
+ recoveryContextText,
308
+ actorUserId: 'node_user',
309
+ onPrepared: async (prepared) => {
310
+ this.send({
311
+ type: 'run.debug.snapshot',
312
+ runId,
313
+ conversationId,
314
+ sessionKey: this.sessionKey,
315
+ acpSessionId: prepared.sessionId,
316
+ isFreshSession: prepared.isFreshSession,
317
+ isExact: prepared.isFreshSession || Boolean(prepared.effectiveSystemPromptText),
318
+ effectiveSystemPromptText: prepared.effectiveSystemPromptText,
319
+ effectiveContextText: prepared.effectiveContextText,
320
+ });
321
+ },
322
+ onDelivered: sendDeliveryAck,
323
+ });
324
+ finishRun(this.db, { runId, stopReason: result.stopReason });
325
+ this.state = 'idle';
326
+ log.info('[agent-host] run finished', {
327
+ hostKey: this.hostKey,
328
+ runId,
329
+ conversationId,
330
+ stopReason: result.stopReason,
331
+ dispatchMode: msg.dispatchMode,
332
+ inboxSize: this.inbox.length,
333
+ });
334
+ this.send({ type: 'run.end', runId, conversationId, stopReason: result.stopReason });
335
+ }
336
+ catch (error) {
337
+ const errMsg = String(error?.message ?? error);
338
+ this.state = 'failed';
339
+ this.lastError = errMsg;
340
+ log.warn('[agent-host] run error', {
341
+ hostKey: this.hostKey,
342
+ runId,
343
+ conversationId,
344
+ error: errMsg,
345
+ });
346
+ finishRun(this.db, { runId, error: errMsg });
347
+ this.send({ type: 'run.end', runId, conversationId, error: errMsg });
348
+ throw error instanceof Error ? error : new Error(errMsg);
349
+ }
350
+ finally {
351
+ this.hooks.onRunFinish?.(msg);
352
+ this.currentRunId = null;
353
+ if (this.state !== 'failed') {
354
+ this.state = 'idle';
355
+ this.lastSleepAt = Date.now();
356
+ }
357
+ }
358
+ }
359
+ failPendingInbox(cause) {
360
+ while (this.inbox.length > 0) {
361
+ const pending = this.inbox.shift();
362
+ this.send({
363
+ type: 'run.end',
364
+ runId: pending.msg.runId,
365
+ conversationId: pending.msg.conversationId,
366
+ error: cause.message,
367
+ });
368
+ pending.reject(cause);
369
+ }
370
+ }
371
+ async cancelRun(runId) {
372
+ if (this.currentRunId !== runId)
373
+ return false;
374
+ return this.runtime.cancelCurrentRun(runId);
375
+ }
376
+ async steerRun(runId, promptText, attachments) {
377
+ if (this.currentRunId !== runId)
378
+ return false;
379
+ if (!this.runtime.steerCurrentRun)
380
+ return false;
381
+ return this.runtime.steerCurrentRun(runId, promptText, attachments);
382
+ }
383
+ async handlePermissionResponse(requestId, decision, selectedActionId, responseText, answers) {
384
+ return this.runtime.respondToPermission(requestId, decision, selectedActionId, responseText, answers);
385
+ }
386
+ async getClaudeControls() {
387
+ if (!this.runtime.getClaudeControls) {
388
+ throw new Error('Claude controls are unavailable for this runtime.');
389
+ }
390
+ return this.runtime.getClaudeControls();
391
+ }
392
+ async setClaudeMode(modeId) {
393
+ if (!this.runtime.setClaudeMode) {
394
+ throw new Error('Claude mode control is unavailable for this runtime.');
395
+ }
396
+ return this.runtime.setClaudeMode(modeId);
397
+ }
398
+ async setClaudeModel(modelId) {
399
+ if (!this.runtime.setClaudeModel) {
400
+ throw new Error('Claude model control is unavailable for this runtime.');
401
+ }
402
+ return this.runtime.setClaudeModel(modelId);
403
+ }
404
+ async executeClaudeCommand(commandName, args) {
405
+ if (!this.runtime.executeClaudeCommand) {
406
+ throw new Error('Claude commands are unavailable for this runtime.');
407
+ }
408
+ return this.runtime.executeClaudeCommand(commandName, args);
409
+ }
410
+ close() {
411
+ this.closeRequested = true;
412
+ this.failPendingInbox(new Error(`Host ${this.hostKey} closed`));
413
+ void Promise.resolve(this.runtime.close()).finally(() => {
414
+ this.cleanupAssetCacheRoot();
415
+ });
416
+ }
417
+ async closeAndWait(timeoutMs = 4_500) {
418
+ this.closeRequested = true;
419
+ this.failPendingInbox(new Error(`Host ${this.hostKey} closed`));
420
+ await Promise.resolve(this.runtime.close()).finally(() => {
421
+ this.cleanupAssetCacheRoot();
422
+ });
423
+ if (!this.processing && !this.currentRunId)
424
+ return;
425
+ await new Promise((resolve, reject) => {
426
+ const onDrained = () => {
427
+ clearTimeout(timer);
428
+ resolve();
429
+ };
430
+ const timer = setTimeout(() => {
431
+ this.closeWaiters = this.closeWaiters.filter((waiter) => waiter !== onDrained);
432
+ reject(new Error(`Timed out waiting for host ${this.hostKey} to close`));
433
+ }, timeoutMs);
434
+ this.closeWaiters.push(onDrained);
435
+ this.notifyCloseWaitersIfDrained();
436
+ });
437
+ }
438
+ notifyCloseWaitersIfDrained() {
439
+ if (!this.closeRequested || this.processing || this.currentRunId)
440
+ return;
441
+ const waiters = this.closeWaiters.splice(0);
442
+ for (const waiter of waiters)
443
+ waiter();
444
+ }
445
+ cleanupAssetCacheRoot() {
446
+ if (!this.assetCacheRoot)
447
+ return;
448
+ try {
449
+ fs.rmSync(this.assetCacheRoot, { recursive: true, force: true });
450
+ }
451
+ finally {
452
+ this.send({
453
+ type: 'asset.materialization.cleared',
454
+ nodeProcessInstanceId: this.nodeProcessInstanceId,
455
+ hostKey: this.hostKey,
456
+ hostInstanceId: this.hostInstanceId,
457
+ clearedAt: Date.now(),
458
+ reason: 'host_closed',
459
+ });
460
+ }
461
+ }
462
+ writeRunContext(msg) {
463
+ if (!this.assetCacheRoot)
464
+ return;
465
+ try {
466
+ fs.mkdirSync(this.assetCacheRoot, { recursive: true });
467
+ fs.writeFileSync(`${this.assetCacheRoot}/run-context.json`, JSON.stringify({
468
+ runId: msg.runId,
469
+ turnId: msg.runId,
470
+ traceId: msg.runId,
471
+ conversationId: msg.conversationId,
472
+ updatedAt: Date.now(),
473
+ }), 'utf8');
474
+ }
475
+ catch (error) {
476
+ log.warn('[agent-host] failed to write run context for channel bridge', {
477
+ hostKey: this.hostKey,
478
+ runId: msg.runId,
479
+ error: String(error?.message ?? error),
480
+ });
481
+ }
482
+ }
483
+ }
@@ -0,0 +1,14 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ export function readAgentNodeVersion() {
5
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
6
+ const packageJsonPath = path.resolve(currentDir, '../package.json');
7
+ try {
8
+ const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
9
+ return typeof parsed.version === 'string' && parsed.version.trim() ? parsed.version.trim() : '0.0.0';
10
+ }
11
+ catch {
12
+ return '0.0.0';
13
+ }
14
+ }
@@ -0,0 +1,35 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ const ASSET_CACHE_ROOT_DIRNAME = 'bigbang-assets';
4
+ export function buildConversationAssetCacheRoot(agentId, conversationId) {
5
+ return path.join(os.tmpdir(), ASSET_CACHE_ROOT_DIRNAME, sanitizeAssetCacheSegment(agentId), sanitizeAssetCacheSegment(conversationId));
6
+ }
7
+ export function buildHostAssetCacheRoot(agentId, conversationId, hostInstanceId) {
8
+ return path.join(buildConversationAssetCacheRoot(agentId, conversationId), 'hosts', sanitizeAssetCacheSegment(hostInstanceId));
9
+ }
10
+ export function resolveScopedAssetCachePath(params) {
11
+ const preferredLocalPath = normalizeRelativeAssetPath(params.preferredLocalPath);
12
+ const fallbackRelativePath = path.join('general', `${params.assetId}-${sanitizeLocalAssetFilename(params.filename ?? params.assetId)}`);
13
+ return path.join(params.cacheRoot, preferredLocalPath ?? fallbackRelativePath);
14
+ }
15
+ function sanitizeAssetCacheSegment(value) {
16
+ const normalized = value.trim() || 'unknown';
17
+ return encodeURIComponent(normalized).replace(/%/g, '_');
18
+ }
19
+ function normalizeRelativeAssetPath(value) {
20
+ if (!value)
21
+ return null;
22
+ const normalized = path.normalize(value.trim());
23
+ if (!normalized || normalized === '.' || path.isAbsolute(normalized))
24
+ return null;
25
+ if (normalized === '..' || normalized.startsWith(`..${path.sep}`))
26
+ return null;
27
+ return normalized;
28
+ }
29
+ function sanitizeLocalAssetFilename(filename) {
30
+ return filename
31
+ .replace(/[^A-Za-z0-9._-]+/g, '_')
32
+ .replace(/_+/g, '_')
33
+ .replace(/^_+|_+$/g, '')
34
+ || 'asset';
35
+ }