@swarmclawai/swarmclaw 1.3.5 → 1.3.6

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/README.md CHANGED
@@ -204,6 +204,11 @@ Read the full setup guide in [`SWARMDOCK.md`](./SWARMDOCK.md), browse the public
204
204
 
205
205
  ## Release Notes
206
206
 
207
+ ### v1.3.6 Highlights
208
+
209
+ - **Knowledge hygiene visibility fix**: exact-duplicate archival now only applies when sources share the same visibility and origin fingerprint. Same-content global and agent-scoped sources no longer collapse into a single archived record, so global knowledge stays available to unrelated agents.
210
+ - **Release gate hardening**: the default test matrix now includes the 1.3.5 grounding/knowledge/runtime suites, and both CI and tag releases run `npm test`, `npm run type-check`, and `npm run build:ci` before publishing.
211
+
207
212
  ### v1.3.5 Highlights
208
213
 
209
214
  - **Knowledge grounding & citations**: agent responses are now grounded against knowledge sources at retrieval time. Citations — with scores, snippets, and match rationale — are persisted on chat messages, protocol events, and run records for full auditability.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
4
4
  "description": "Self-hosted AI runtime for OpenClaw, delegation, autonomy, runtime skills, crypto wallets, and chat platform connectors.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -62,7 +62,7 @@
62
62
  "benchmark:autonomy": "node ./scripts/benchmark-autonomy-harness.mjs",
63
63
  "benchmark:agent-regression": "node --import tsx ./scripts/run-agent-regression-suite.ts",
64
64
  "type-check": "tsc --noEmit",
65
- "test": "npm run test:cli && npm run test:setup && npm run test:openclaw",
65
+ "test": "npm run test:cli && npm run test:setup && npm run test:openclaw && npm run test:runtime",
66
66
  "format": "eslint --fix",
67
67
  "lint": "eslint",
68
68
  "lint:fix": "eslint --fix",
@@ -72,6 +72,7 @@
72
72
  "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs",
73
73
  "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts",
74
74
  "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
75
+ "test:runtime": "tsx --test src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts",
75
76
  "test:e2e": "tsx .workbench/browser-e2e/run.ts",
76
77
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
77
78
  "prepack": "npm run build:ci",
@@ -1,40 +1,23 @@
1
1
  import assert from 'node:assert/strict'
2
- import fs from 'node:fs'
3
- import os from 'node:os'
4
- import path from 'node:path'
5
- import { spawnSync } from 'node:child_process'
6
2
  import test from 'node:test'
3
+ import { runWithTempDataDir as runWithSharedTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
7
4
 
8
- const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
9
-
10
- function runWithTempDataDir(script: string) {
11
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-chat-disabled-'))
12
- try {
13
- const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
- cwd: repoRoot,
15
- env: {
16
- ...process.env,
17
- DATA_DIR: path.join(tempDir, 'data'),
18
- WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
- BROWSER_PROFILES_DIR: path.join(tempDir, 'browser-profiles'),
20
- },
21
- encoding: 'utf-8',
22
- })
23
- assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
24
- const lines = (result.stdout || '')
25
- .trim()
26
- .split('\n')
27
- .map((line) => line.trim())
28
- .filter(Boolean)
29
- const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
30
- return JSON.parse(jsonLine || '{}')
31
- } finally {
32
- fs.rmSync(tempDir, { recursive: true, force: true })
33
- }
5
+ function runWithTempDataDir<T = unknown>(script: string): T {
6
+ return runWithSharedTempDataDir<T>(script, {
7
+ prefix: 'swarmclaw-chat-disabled-',
8
+ dataDir: 'data',
9
+ browserProfilesDir: 'browser-profiles',
10
+ })
34
11
  }
35
12
 
36
13
  test('executeSessionChatTurn persists a visible error for disabled agents', () => {
37
- const output = runWithTempDataDir(`
14
+ const output = runWithTempDataDir<{
15
+ error: string | null
16
+ text: string | null
17
+ persisted: boolean
18
+ lastRole: string | null
19
+ lastText: string | null
20
+ }>(`
38
21
  const storageMod = await import('@/lib/server/storage')
39
22
  const storage = storageMod.default || storageMod['module.exports'] || storageMod
40
23
  const threadMod = await import('@/lib/server/agents/agent-thread-session')
@@ -1,40 +1,13 @@
1
1
  import assert from 'node:assert/strict'
2
- import fs from 'node:fs'
3
- import os from 'node:os'
4
- import path from 'node:path'
5
- import { spawnSync } from 'node:child_process'
6
2
  import test from 'node:test'
7
-
8
- const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
9
-
10
- function runWithTempDataDir(script: string) {
11
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-chat-eval-history-'))
12
- try {
13
- const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
- cwd: repoRoot,
15
- env: {
16
- ...process.env,
17
- DATA_DIR: path.join(tempDir, 'data'),
18
- WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
- BROWSER_PROFILES_DIR: path.join(tempDir, 'browser-profiles'),
20
- },
21
- encoding: 'utf-8',
22
- })
23
- assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
24
- const lines = (result.stdout || '')
25
- .trim()
26
- .split('\n')
27
- .map((line) => line.trim())
28
- .filter(Boolean)
29
- const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
30
- return JSON.parse(jsonLine || '{}')
31
- } finally {
32
- fs.rmSync(tempDir, { recursive: true, force: true })
33
- }
34
- }
3
+ import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
35
4
 
36
5
  test('executeSessionChatTurn persists internal eval user turns for same-thread recall', () => {
37
- const output = runWithTempDataDir(`
6
+ const output = runWithTempDataDir<{
7
+ recallText: string | null
8
+ roles: string[]
9
+ texts: string[]
10
+ }>(`
38
11
  const storageMod = await import('@/lib/server/storage')
39
12
  const storage = storageMod.default || storageMod['module.exports'] || storageMod
40
13
  const providersMod = await import('@/lib/providers/index')
@@ -103,7 +76,11 @@ test('executeSessionChatTurn persists internal eval user turns for same-thread r
103
76
  roles: storedSession.messages.map((entry) => entry.role),
104
77
  texts: storedSession.messages.map((entry) => entry.text),
105
78
  }))
106
- `)
79
+ `, {
80
+ prefix: 'swarmclaw-chat-eval-history-',
81
+ dataDir: 'data',
82
+ browserProfilesDir: 'browser-profiles',
83
+ })
107
84
 
108
85
  assert.match(String(output.recallText || ''), /Sunbird/)
109
86
  assert.deepEqual(output.roles, ['user', 'assistant', 'user', 'assistant'])
@@ -1,40 +1,17 @@
1
1
  import assert from 'node:assert/strict'
2
- import fs from 'node:fs'
3
- import os from 'node:os'
4
- import path from 'node:path'
5
- import { spawnSync } from 'node:child_process'
6
2
  import test from 'node:test'
7
-
8
- const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
9
-
10
- function runWithTempDataDir(script: string) {
11
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-chat-grounding-'))
12
- try {
13
- const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
- cwd: repoRoot,
15
- env: {
16
- ...process.env,
17
- DATA_DIR: path.join(tempDir, 'data'),
18
- WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
- BROWSER_PROFILES_DIR: path.join(tempDir, 'browser-profiles'),
20
- },
21
- encoding: 'utf-8',
22
- })
23
- assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
24
- const lines = (result.stdout || '')
25
- .trim()
26
- .split('\n')
27
- .map((line) => line.trim())
28
- .filter(Boolean)
29
- const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
30
- return JSON.parse(jsonLine || '{}')
31
- } finally {
32
- fs.rmSync(tempDir, { recursive: true, force: true })
33
- }
34
- }
3
+ import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
35
4
 
36
5
  test('executeSessionChatTurn persists citations and retrieval traces on grounded assistant messages', () => {
37
- const output = runWithTempDataDir(`
6
+ const output = runWithTempDataDir<{
7
+ persisted: boolean
8
+ resultCitationCount: number
9
+ resultSelectorStatus: string | null
10
+ messageCitationCount: number
11
+ messageTraceHitCount: number
12
+ messageSelectorStatus: string | null
13
+ messageSourceTitle: string | null
14
+ }>(`
38
15
  const storageMod = await import('@/lib/server/storage')
39
16
  const providersMod = await import('@/lib/providers/index')
40
17
  const threadMod = await import('@/lib/server/agents/agent-thread-session')
@@ -115,7 +92,11 @@ test('executeSessionChatTurn persists citations and retrieval traces on grounded
115
92
  messageSelectorStatus: lastMessage?.retrievalTrace?.selectorStatus || null,
116
93
  messageSourceTitle: lastMessage?.citations?.[0]?.sourceTitle || null,
117
94
  }))
118
- `)
95
+ `, {
96
+ prefix: 'swarmclaw-chat-grounding-',
97
+ dataDir: 'data',
98
+ browserProfilesDir: 'browser-profiles',
99
+ })
119
100
 
120
101
  assert.equal(output.persisted, true)
121
102
  assert.equal(output.resultCitationCount >= 1, true)
@@ -1,40 +1,24 @@
1
1
  import assert from 'node:assert/strict'
2
- import fs from 'node:fs'
3
- import os from 'node:os'
4
- import path from 'node:path'
5
- import { spawnSync } from 'node:child_process'
6
2
  import test from 'node:test'
7
-
8
- const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
9
-
10
- function runWithTempDataDir(script: string) {
11
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-chat-session-sync-'))
12
- try {
13
- const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
- cwd: repoRoot,
15
- env: {
16
- ...process.env,
17
- DATA_DIR: path.join(tempDir, 'data'),
18
- WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
- BROWSER_PROFILES_DIR: path.join(tempDir, 'browser-profiles'),
20
- },
21
- encoding: 'utf-8',
22
- })
23
- assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
24
- const lines = (result.stdout || '')
25
- .trim()
26
- .split('\n')
27
- .map((line) => line.trim())
28
- .filter(Boolean)
29
- const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
30
- return JSON.parse(jsonLine || '{}')
31
- } finally {
32
- fs.rmSync(tempDir, { recursive: true, force: true })
33
- }
3
+ import { runWithTempDataDir as runWithSharedTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
4
+
5
+ function runWithTempDataDir<T = unknown>(script: string): T {
6
+ return runWithSharedTempDataDir<T>(script, {
7
+ prefix: 'swarmclaw-chat-session-sync-',
8
+ dataDir: 'data',
9
+ browserProfilesDir: 'browser-profiles',
10
+ })
34
11
  }
35
12
 
36
13
  test('executeSessionChatTurn syncs updated agent runtime fields onto its thread session', () => {
37
- const output = runWithTempDataDir(`
14
+ const output = runWithTempDataDir<{
15
+ provider: string | null
16
+ model: string | null
17
+ extensions: string[]
18
+ heartbeatEnabled: boolean | null
19
+ heartbeatIntervalSec: number | null
20
+ connectorContext: Record<string, unknown> | null
21
+ }>(`
38
22
  const storageMod = await import('@/lib/server/storage')
39
23
  const storage = storageMod.default || storageMod['module.exports'] || storageMod
40
24
  const providersMod = await import('@/lib/providers/index')
@@ -126,7 +110,12 @@ test('executeSessionChatTurn syncs updated agent runtime fields onto its thread
126
110
  })
127
111
 
128
112
  test('executeSessionChatTurn keeps tool-only heartbeats off the visible main-thread history and clears stale connector state', () => {
129
- const output = runWithTempDataDir(`
113
+ const output = runWithTempDataDir<{
114
+ connectorContext: Record<string, unknown> | null
115
+ messageCount: number
116
+ lastMessageText: string | null
117
+ heartbeatKinds: number
118
+ }>(`
130
119
  const storageMod = await import('@/lib/server/storage')
131
120
  const providersMod = await import('@/lib/providers/index')
132
121
  const execMod = await import('@/lib/server/chat-execution/chat-execution')
@@ -232,7 +221,11 @@ test('executeSessionChatTurn keeps tool-only heartbeats off the visible main-thr
232
221
  })
233
222
 
234
223
  test('executeSessionChatTurn hides internal main-loop followup output from the visible transcript', () => {
235
- const output = runWithTempDataDir(`
224
+ const output = runWithTempDataDir<{
225
+ messageCount: number
226
+ lastMessageText: string | null
227
+ hasStreamingArtifacts: boolean
228
+ }>(`
236
229
  const storageMod = await import('@/lib/server/storage')
237
230
  const providersMod = await import('@/lib/providers/index')
238
231
  const execMod = await import('@/lib/server/chat-execution/chat-execution')
@@ -324,7 +317,10 @@ test('executeSessionChatTurn hides internal main-loop followup output from the v
324
317
  })
325
318
 
326
319
  test('executeSessionChatTurn forces external connector sessions onto session-scoped memory', () => {
327
- const output = runWithTempDataDir(`
320
+ const output = runWithTempDataDir<{
321
+ memoryScopeMode: string | null
322
+ connectorContext: { isOwnerConversation?: boolean } | null
323
+ }>(`
328
324
  const storageMod = await import('@/lib/server/storage')
329
325
  const providersMod = await import('@/lib/providers/index')
330
326
  const execMod = await import('@/lib/server/chat-execution/chat-execution')
@@ -418,7 +414,10 @@ test('executeSessionChatTurn forces external connector sessions onto session-sco
418
414
  })
419
415
 
420
416
  test('executeSessionChatTurn applies lifecycle hooks for model resolution and message persistence', () => {
421
- const output = runWithTempDataDir(`
417
+ const output = runWithTempDataDir<{
418
+ lastMessageText: string
419
+ marks: string[]
420
+ }>(`
422
421
  const storageMod = await import('@/lib/server/storage')
423
422
  const providersMod = await import('@/lib/providers/index')
424
423
  const extMod = await import('@/lib/server/extensions')
@@ -213,3 +213,49 @@ test('runKnowledgeHygieneMaintenance reindexes stale file sources and archives e
213
213
  assert.ok(output.recentActionKinds.includes('archive'))
214
214
  assert.ok(output.recentActionKinds.includes('reindex') || output.recentActionKinds.includes('sync'))
215
215
  })
216
+
217
+ test('runKnowledgeHygieneMaintenance keeps same-content sources separate when visibility differs', () => {
218
+ const output = runWithTempDataDir<{
219
+ globalArchived: boolean
220
+ agentArchived: boolean
221
+ agent1Hits: number
222
+ agent2Hits: number
223
+ }>(`
224
+ const knowledgeMod = await import('./src/lib/server/knowledge-sources.ts')
225
+ const knowledge = knowledgeMod.default || knowledgeMod
226
+
227
+ const globalSource = await knowledge.createKnowledgeSource({
228
+ kind: 'manual',
229
+ title: 'Global Policy',
230
+ content: 'identical content',
231
+ scope: 'global',
232
+ })
233
+
234
+ const agentSource = await knowledge.createKnowledgeSource({
235
+ kind: 'manual',
236
+ title: 'Scoped Policy',
237
+ content: 'identical content',
238
+ scope: 'agent',
239
+ agentIds: ['agent-1'],
240
+ })
241
+
242
+ await knowledge.runKnowledgeHygieneMaintenance()
243
+
244
+ const globalDetail = await knowledge.getKnowledgeSourceDetail(globalSource.source.id)
245
+ const agentDetail = await knowledge.getKnowledgeSourceDetail(agentSource.source.id)
246
+ const agent1Hits = await knowledge.searchKnowledgeHits({ query: 'identical', viewerAgentId: 'agent-1' })
247
+ const agent2Hits = await knowledge.searchKnowledgeHits({ query: 'identical', viewerAgentId: 'agent-2' })
248
+
249
+ console.log(JSON.stringify({
250
+ globalArchived: !!globalDetail?.source?.archivedAt,
251
+ agentArchived: !!agentDetail?.source?.archivedAt,
252
+ agent1Hits: agent1Hits.length,
253
+ agent2Hits: agent2Hits.length,
254
+ }))
255
+ `, { prefix: 'swarmclaw-knowledge-visibility-' })
256
+
257
+ assert.equal(output.globalArchived, false)
258
+ assert.equal(output.agentArchived, false)
259
+ assert.equal(output.agent1Hits, 2)
260
+ assert.equal(output.agent2Hits, 1)
261
+ })
@@ -1056,6 +1056,38 @@ function sameSourceOrigin(left: KnowledgeSource, right: KnowledgeSource): boolea
1056
1056
  return false
1057
1057
  }
1058
1058
 
1059
+ function duplicateOriginFingerprint(source: KnowledgeSource): string {
1060
+ if (source.sourceUrl) return `url:${source.sourceUrl}`
1061
+ if (source.sourcePath) return `path:${source.sourcePath}`
1062
+ return `kind:${source.kind}`
1063
+ }
1064
+
1065
+ function duplicateGroupKey(source: KnowledgeSource): string | null {
1066
+ if (!source.sourceHash) return null
1067
+ const sortedAgentIds = [...source.agentIds].sort()
1068
+ const sortedTags = [...source.tags].map((tag) => tag.toLowerCase()).sort()
1069
+ return [
1070
+ source.sourceHash,
1071
+ source.kind,
1072
+ source.scope,
1073
+ sortedAgentIds.join(','),
1074
+ sortedTags.join(','),
1075
+ duplicateOriginFingerprint(source),
1076
+ ].join('|')
1077
+ }
1078
+
1079
+ function collectDuplicateGroups(sources: KnowledgeSource[]): Map<string, KnowledgeSource[]> {
1080
+ const duplicateGroups = new Map<string, KnowledgeSource[]>()
1081
+ for (const source of sources) {
1082
+ const groupKey = duplicateGroupKey(source)
1083
+ if (!groupKey) continue
1084
+ const group = duplicateGroups.get(groupKey) || []
1085
+ group.push(source)
1086
+ duplicateGroups.set(groupKey, group)
1087
+ }
1088
+ return duplicateGroups
1089
+ }
1090
+
1059
1091
  function canonicalSourceForGroup(group: KnowledgeSource[]): KnowledgeSource {
1060
1092
  return [...group].sort((left, right) => {
1061
1093
  const archiveDelta = Number(sourceIsExcludedByDefault(left)) - Number(sourceIsExcludedByDefault(right))
@@ -1072,14 +1104,7 @@ function buildHygieneSummary(sources: KnowledgeSource[]): KnowledgeHygieneSummar
1072
1104
  const pushFinding = (finding: KnowledgeHygieneFinding) => {
1073
1105
  if (findings.length < MAX_HYGIENE_FINDINGS) findings.push(finding)
1074
1106
  }
1075
-
1076
- const duplicateGroups = new Map<string, KnowledgeSource[]>()
1077
- for (const source of sources) {
1078
- if (!source.sourceHash) continue
1079
- const group = duplicateGroups.get(source.sourceHash) || []
1080
- group.push(source)
1081
- duplicateGroups.set(source.sourceHash, group)
1082
- }
1107
+ const duplicateGroups = collectDuplicateGroups(sources)
1083
1108
 
1084
1109
  for (const source of sources) {
1085
1110
  if (sourceIsArchived(source)) {
@@ -1185,14 +1210,7 @@ export async function getKnowledgeHygieneSummary(): Promise<KnowledgeHygieneSumm
1185
1210
  export async function runKnowledgeHygieneMaintenance(): Promise<KnowledgeHygieneSummary> {
1186
1211
  await ensureLegacyKnowledgeBackfill()
1187
1212
  const sources = listStoredSources()
1188
-
1189
- const duplicateGroups = new Map<string, KnowledgeSource[]>()
1190
- for (const source of sources) {
1191
- if (!source.sourceHash) continue
1192
- const group = duplicateGroups.get(source.sourceHash) || []
1193
- group.push(source)
1194
- duplicateGroups.set(source.sourceHash, group)
1195
- }
1213
+ const duplicateGroups = collectDuplicateGroups(sources)
1196
1214
 
1197
1215
  for (const source of sources) {
1198
1216
  if (sourceIsExcludedByDefault(source)) continue
@@ -10,19 +10,32 @@ export function runWithTempDataDir<T = unknown>(
10
10
  script: string,
11
11
  options: {
12
12
  prefix?: string
13
+ dataDir?: string
14
+ workspaceDir?: string
15
+ browserProfilesDir?: string
13
16
  } = {},
14
17
  ): T {
15
18
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), options.prefix || 'swarmclaw-test-'))
16
- const workspaceDir = path.join(tempDir, 'workspace')
19
+ const resolveTempPath = (value: string | undefined, fallback: string): string =>
20
+ path.isAbsolute(value || '') ? String(value) : path.join(tempDir, value || fallback)
21
+ const dataDir = resolveTempPath(options.dataDir, '')
22
+ const workspaceDir = resolveTempPath(options.workspaceDir, 'workspace')
23
+ const browserProfilesDir = options.browserProfilesDir
24
+ ? resolveTempPath(options.browserProfilesDir, 'browser-profiles')
25
+ : null
26
+
27
+ fs.mkdirSync(dataDir, { recursive: true })
17
28
  fs.mkdirSync(workspaceDir, { recursive: true })
29
+ if (browserProfilesDir) fs.mkdirSync(browserProfilesDir, { recursive: true })
18
30
 
19
31
  try {
20
32
  const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
21
33
  cwd: repoRoot,
22
34
  env: {
23
35
  ...process.env,
24
- DATA_DIR: tempDir,
36
+ DATA_DIR: dataDir,
25
37
  WORKSPACE_DIR: workspaceDir,
38
+ ...(browserProfilesDir ? { BROWSER_PROFILES_DIR: browserProfilesDir } : {}),
26
39
  },
27
40
  encoding: 'utf-8',
28
41
  })